mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-31 04:04:19 -04:00
Manga Reader Refresh (#1137)
* Refactored manga reader to use a regular image element for all cases except for split page rendering * Fixed a weird issue where ordering of routes broke redireciton in one case. * Added comments to a lot of the enums and refactored READER_MODE to be ReaderMode and much more clearer on function. * Added bookmark effect on image renderer * Implemented keyboard shortcut modal * Introduced the new layout mode into the manga reader, updated preferences, and updated bookmark to work for said functionality. Need to implement renderer now * Hooked in ability to show double pages but all the css is broken. Committing for help from Robbie. * Fixed an issue where Language tag in metadata edit wasn't being updated * Fixed up some styling on mobile for edit series detail * Some css fixes * Hooked in ability to set background color on reader (not implemented in reader). Optimized some code in ArchiveService to avoid extra memory allocations. * Hooked in background color, generated the migration * Fixed a bug when paging to cover images, full height would be used instead of full-width for cover images * New option in reader to show screen hints (on by default). You can disable in user preferences which will stop showing pagination overlay hints * Lots of fixes for double rendering mode * Bumped the amount of cached pages to 8 * Fixed an issue where dropdowns weren't being locked on form manipulation
This commit is contained in:
parent
3dedbb1465
commit
2a4d0d1cd1
@ -310,7 +310,7 @@ namespace API.Tests.Services
|
|||||||
[InlineData(new [] {"001.txt", "002.txt", "a.jpg"}, "Test.zip", "a.jpg")]
|
[InlineData(new [] {"001.txt", "002.txt", "a.jpg"}, "Test.zip", "a.jpg")]
|
||||||
public void FindCoverImageFilename(string[] filenames, string archiveName, string expected)
|
public void FindCoverImageFilename(string[] filenames, string archiveName, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, _archiveService.FindCoverImageFilename(archiveName, filenames));
|
Assert.Equal(expected, ArchiveService.FindCoverImageFilename(archiveName, filenames));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ using API.Data;
|
|||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
|
using AutoMapper;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -14,10 +15,12 @@ namespace API.Controllers
|
|||||||
public class UsersController : BaseApiController
|
public class UsersController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
public UsersController(IUnitOfWork unitOfWork)
|
public UsersController(IUnitOfWork unitOfWork, IMapper mapper)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
@ -71,7 +74,10 @@ namespace API.Controllers
|
|||||||
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
|
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
|
||||||
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
|
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
|
||||||
existingPreferences.AutoCloseMenu = preferencesDto.AutoCloseMenu;
|
existingPreferences.AutoCloseMenu = preferencesDto.AutoCloseMenu;
|
||||||
|
existingPreferences.ShowScreenHints = preferencesDto.ShowScreenHints;
|
||||||
existingPreferences.ReaderMode = preferencesDto.ReaderMode;
|
existingPreferences.ReaderMode = preferencesDto.ReaderMode;
|
||||||
|
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
||||||
|
existingPreferences.BackgroundColor = string.IsNullOrEmpty(preferencesDto.BackgroundColor) ? "#000000" : preferencesDto.BackgroundColor;
|
||||||
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
|
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
|
||||||
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
|
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
|
||||||
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
|
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
|
||||||
@ -90,5 +96,13 @@ namespace API.Controllers
|
|||||||
|
|
||||||
return BadRequest("There was an issue saving preferences.");
|
return BadRequest("There was an issue saving preferences.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("get-preferences")]
|
||||||
|
public async Task<ActionResult<UserPreferencesDto>> GetPreferences()
|
||||||
|
{
|
||||||
|
return _mapper.Map<UserPreferencesDto>(
|
||||||
|
await _unitOfWork.UserRepository.GetPreferencesAsync(User.GetUsername()));
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,18 +5,74 @@ namespace API.DTOs
|
|||||||
{
|
{
|
||||||
public class UserPreferencesDto
|
public class UserPreferencesDto
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manga Reader Option: What direction should the next/prev page buttons go
|
||||||
|
/// </summary>
|
||||||
public ReadingDirection ReadingDirection { get; set; }
|
public ReadingDirection ReadingDirection { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Manga Reader Option: How should the image be scaled to screen
|
||||||
|
/// </summary>
|
||||||
public ScalingOption ScalingOption { get; set; }
|
public ScalingOption ScalingOption { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Manga Reader Option: Which side of a split image should we show first
|
||||||
|
/// </summary>
|
||||||
public PageSplitOption PageSplitOption { get; set; }
|
public PageSplitOption PageSplitOption { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Manga Reader Option: How the manga reader should perform paging or reading of the file
|
||||||
|
/// <example>
|
||||||
|
/// Webtoon uses scrolling to page, LeftRight uses paging by clicking left/right side of reader, UpDown uses paging
|
||||||
|
/// by clicking top/bottom sides of reader.
|
||||||
|
/// </example>
|
||||||
|
/// </summary>
|
||||||
public ReaderMode ReaderMode { get; set; }
|
public ReaderMode ReaderMode { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Manga Reader Option: How many pages to display in the reader at once
|
||||||
|
/// </summary>
|
||||||
|
public LayoutMode LayoutMode { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Manga Reader Option: Background color of the reader
|
||||||
|
/// </summary>
|
||||||
|
public string BackgroundColor { get; set; } = "#000000";
|
||||||
|
/// <summary>
|
||||||
|
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
||||||
|
/// </summary>
|
||||||
public bool AutoCloseMenu { get; set; }
|
public bool AutoCloseMenu { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowScreenHints { get; set; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// Book Reader Option: Should the background color be dark
|
||||||
|
/// </summary>
|
||||||
public bool BookReaderDarkMode { get; set; } = false;
|
public bool BookReaderDarkMode { get; set; } = false;
|
||||||
|
/// <summary>
|
||||||
|
/// Book Reader Option: Override extra Margin
|
||||||
|
/// </summary>
|
||||||
public int BookReaderMargin { get; set; }
|
public int BookReaderMargin { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Book Reader Option: Override line-height
|
||||||
|
/// </summary>
|
||||||
public int BookReaderLineSpacing { get; set; }
|
public int BookReaderLineSpacing { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Book Reader Option: Override font size
|
||||||
|
/// </summary>
|
||||||
public int BookReaderFontSize { get; set; }
|
public int BookReaderFontSize { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override
|
||||||
|
/// </summary>
|
||||||
public string BookReaderFontFamily { get; set; }
|
public string BookReaderFontFamily { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Book Reader Option: Allows tapping on side of screens to paginate
|
||||||
|
/// </summary>
|
||||||
public bool BookReaderTapToPaginate { get; set; }
|
public bool BookReaderTapToPaginate { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Book Reader Option: What direction should the next/prev page buttons go
|
||||||
|
/// </summary>
|
||||||
public ReadingDirection BookReaderReadingDirection { get; set; }
|
public ReadingDirection BookReaderReadingDirection { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// UI Site Global Setting: The UI theme the user should use.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Should default to Dark</remarks>
|
||||||
public SiteTheme Theme { get; set; }
|
public SiteTheme Theme { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1454
API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs
generated
Normal file
1454
API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,38 @@
|
|||||||
|
using API.Entities.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class MangaReaderBackgroundAndLayoutMode : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "BackgroundColor",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
type: "TEXT",
|
||||||
|
defaultValue: "#000000",
|
||||||
|
nullable: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "LayoutMode",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: LayoutMode.Single);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BackgroundColor",
|
||||||
|
table: "AppUserPreferences");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LayoutMode",
|
||||||
|
table: "AppUserPreferences");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1457
API/Data/Migrations/20220307153053_ScreenHints.Designer.cs
generated
Normal file
1457
API/Data/Migrations/20220307153053_ScreenHints.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
API/Data/Migrations/20220307153053_ScreenHints.cs
Normal file
26
API/Data/Migrations/20220307153053_ScreenHints.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class ScreenHints : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "ShowScreenHints",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ShowScreenHints",
|
||||||
|
table: "AppUserPreferences");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -165,6 +165,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<bool>("AutoCloseMenu")
|
b.Property<bool>("AutoCloseMenu")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BackgroundColor")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<bool>("BookReaderDarkMode")
|
b.Property<bool>("BookReaderDarkMode")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
@ -186,6 +189,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<bool>("BookReaderTapToPaginate")
|
b.Property<bool>("BookReaderTapToPaginate")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("LayoutMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("PageSplitOption")
|
b.Property<int>("PageSplitOption")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
@ -198,6 +204,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<int>("ScalingOption")
|
b.Property<int>("ScalingOption")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ShowScreenHints")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int?>("ThemeId")
|
b.Property<int?>("ThemeId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
@ -25,12 +25,23 @@ 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>
|
||||||
public bool AutoCloseMenu { get; set; } = true;
|
public bool AutoCloseMenu { get; set; } = true;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowScreenHints { get; set; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// Manga Reader Option: How many pages to display in the reader at once
|
||||||
|
/// </summary>
|
||||||
|
public LayoutMode LayoutMode { get; set; } = LayoutMode.Single;
|
||||||
|
/// <summary>
|
||||||
|
/// Manga Reader Option: Background color of the reader
|
||||||
|
/// </summary>
|
||||||
|
public string BackgroundColor { get; set; } = "#000000";
|
||||||
|
/// <summary>
|
||||||
/// Book Reader Option: Should the background color be dark
|
/// Book Reader Option: Should the background color be dark
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool BookReaderDarkMode { get; set; } = true;
|
public bool BookReaderDarkMode { get; set; } = true;
|
||||||
|
11
API/Entities/Enums/LayoutMode.cs
Normal file
11
API/Entities/Enums/LayoutMode.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace API.Entities.Enums;
|
||||||
|
|
||||||
|
public enum LayoutMode
|
||||||
|
{
|
||||||
|
[Description("Single")]
|
||||||
|
Single = 1,
|
||||||
|
[Description("Double")]
|
||||||
|
Double = 2
|
||||||
|
}
|
@ -5,13 +5,10 @@ namespace API.Entities.Enums
|
|||||||
public enum ReaderMode
|
public enum ReaderMode
|
||||||
{
|
{
|
||||||
[Description("Left and Right")]
|
[Description("Left and Right")]
|
||||||
// ReSharper disable once InconsistentNaming
|
LeftRight = 0,
|
||||||
MANGA_LR = 0,
|
|
||||||
[Description("Up and Down")]
|
[Description("Up and Down")]
|
||||||
// ReSharper disable once InconsistentNaming
|
UpDown = 1,
|
||||||
MANGA_UP = 1,
|
|
||||||
[Description("Webtoon")]
|
[Description("Webtoon")]
|
||||||
// ReSharper disable once InconsistentNaming
|
Webtoon = 2
|
||||||
WEBTOON = 2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,13 @@ namespace API.Extensions
|
|||||||
/// <returns>Sorted Enumerable</returns>
|
/// <returns>Sorted Enumerable</returns>
|
||||||
public static IEnumerable<T> OrderByNatural<T>(this IEnumerable<T> items, Func<T, string> selector, StringComparer stringComparer = null)
|
public static IEnumerable<T> OrderByNatural<T>(this IEnumerable<T> items, Func<T, string> selector, StringComparer stringComparer = null)
|
||||||
{
|
{
|
||||||
var maxDigits = items
|
var list = items.ToList();
|
||||||
|
var maxDigits = list
|
||||||
.SelectMany(i => Regex.Matches(selector(i))
|
.SelectMany(i => Regex.Matches(selector(i))
|
||||||
.Select(digitChunk => (int?)digitChunk.Value.Length))
|
.Select(digitChunk => (int?)digitChunk.Value.Length))
|
||||||
.Max() ?? 0;
|
.Max() ?? 0;
|
||||||
|
|
||||||
return items.OrderBy(i => Regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture);
|
return list.OrderBy(i => Regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,6 @@ namespace API.Services
|
|||||||
ArchiveLibrary CanOpen(string archivePath);
|
ArchiveLibrary CanOpen(string archivePath);
|
||||||
bool ArchiveNeedsFlattening(ZipArchive archive);
|
bool ArchiveNeedsFlattening(ZipArchive archive);
|
||||||
Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder);
|
Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder);
|
||||||
string FindCoverImageFilename(string archivePath, IList<string> entryNames);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -127,8 +126,8 @@ namespace API.Services
|
|||||||
public static string FindFolderEntry(IEnumerable<string> entryFullNames)
|
public static string FindFolderEntry(IEnumerable<string> entryFullNames)
|
||||||
{
|
{
|
||||||
var result = entryFullNames
|
var result = entryFullNames
|
||||||
.OrderByNatural(Path.GetFileNameWithoutExtension)
|
|
||||||
.Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)))
|
.Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)))
|
||||||
|
.OrderByNatural(Path.GetFileNameWithoutExtension)
|
||||||
.FirstOrDefault(Parser.Parser.IsCoverImage);
|
.FirstOrDefault(Parser.Parser.IsCoverImage);
|
||||||
|
|
||||||
return string.IsNullOrEmpty(result) ? null : result;
|
return string.IsNullOrEmpty(result) ? null : result;
|
||||||
@ -144,8 +143,8 @@ namespace API.Services
|
|||||||
// First check if there are any files that are not in a nested folder before just comparing by filename. This is needed
|
// First check if there are any files that are not in a nested folder before just comparing by filename. This is needed
|
||||||
// because NaturalSortComparer does not work with paths and doesn't seem 001.jpg as before chapter 1/001.jpg.
|
// because NaturalSortComparer does not work with paths and doesn't seem 001.jpg as before chapter 1/001.jpg.
|
||||||
var fullNames = entryFullNames
|
var fullNames = entryFullNames
|
||||||
.OrderByNatural(c => c.GetFullPathWithoutExtension())
|
|
||||||
.Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)) && Parser.Parser.IsImage(path))
|
.Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)) && Parser.Parser.IsImage(path))
|
||||||
|
.OrderByNatural(c => c.GetFullPathWithoutExtension())
|
||||||
.ToList();
|
.ToList();
|
||||||
if (fullNames.Count == 0) return null;
|
if (fullNames.Count == 0) return null;
|
||||||
|
|
||||||
@ -201,9 +200,8 @@ namespace API.Services
|
|||||||
case ArchiveLibrary.Default:
|
case ArchiveLibrary.Default:
|
||||||
{
|
{
|
||||||
using var archive = ZipFile.OpenRead(archivePath);
|
using var archive = ZipFile.OpenRead(archivePath);
|
||||||
var entryNames = archive.Entries.Select(e => e.FullName).ToList();
|
|
||||||
|
|
||||||
var entryName = FindCoverImageFilename(archivePath, entryNames);
|
var entryName = FindCoverImageFilename(archivePath, archive.Entries.Select(e => e.FullName));
|
||||||
var entry = archive.Entries.Single(e => e.FullName == entryName);
|
var entry = archive.Entries.Single(e => e.FullName == entryName);
|
||||||
|
|
||||||
using var stream = entry.Open();
|
using var stream = entry.Open();
|
||||||
@ -242,7 +240,7 @@ namespace API.Services
|
|||||||
/// <param name="archivePath"></param>
|
/// <param name="archivePath"></param>
|
||||||
/// <param name="entryNames"></param>
|
/// <param name="entryNames"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public string FindCoverImageFilename(string archivePath, IList<string> entryNames)
|
public static string FindCoverImageFilename(string archivePath, IEnumerable<string> entryNames)
|
||||||
{
|
{
|
||||||
var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames, Path.GetFileName(archivePath));
|
var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames, Path.GetFileName(archivePath));
|
||||||
return entryName;
|
return entryName;
|
||||||
|
@ -79,6 +79,12 @@ public class SeriesService : ISeriesService
|
|||||||
series.Metadata.SummaryLocked = true;
|
series.Metadata.SummaryLocked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata.Language)
|
||||||
|
{
|
||||||
|
series.Metadata.Language = updateSeriesMetadataDto.SeriesMetadata?.Language;
|
||||||
|
series.Metadata.LanguageLocked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
series.Metadata.CollectionTags ??= new List<CollectionTag>();
|
series.Metadata.CollectionTags ??= new List<CollectionTag>();
|
||||||
UpdateRelatedList(updateSeriesMetadataDto.CollectionTags, series, allCollectionTags, (tag) =>
|
UpdateRelatedList(updateSeriesMetadataDto.CollectionTags, series, allCollectionTags, (tag) =>
|
||||||
|
8
UI/Web/package-lock.json
generated
8
UI/Web/package-lock.json
generated
@ -9173,6 +9173,14 @@
|
|||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ngx-color-picker": {
|
||||||
|
"version": "12.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-12.0.0.tgz",
|
||||||
|
"integrity": "sha512-SY5KoZka/uq2MNhUAKfJXQjjS2TFvKDJHbsCxfnjKjS/VHx8VVeTJpnt5wuuewzRzLxfOm5y2Fw8/HTPEPtRkA==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ngx-file-drop": {
|
"ngx-file-drop": {
|
||||||
"version": "13.0.0",
|
"version": "13.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-13.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-13.0.0.tgz",
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"lazysizes": "^5.3.2",
|
"lazysizes": "^5.3.2",
|
||||||
"ng-circle-progress": "^1.6.0",
|
"ng-circle-progress": "^1.6.0",
|
||||||
|
"ngx-color-picker": "^12.0.0",
|
||||||
"ngx-file-drop": "^13.0.0",
|
"ngx-file-drop": "^13.0.0",
|
||||||
"ngx-toastr": "^14.2.1",
|
"ngx-toastr": "^14.2.1",
|
||||||
"rxjs": "~7.5.4",
|
"rxjs": "~7.5.4",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { MemberService } from '../_services/member.service';
|
import { MemberService } from '../_services/member.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -12,6 +12,7 @@ export class LibraryAccessGuard implements CanActivate {
|
|||||||
|
|
||||||
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||||
const libraryId = parseInt(state.url.split('library/')[1], 10);
|
const libraryId = parseInt(state.url.split('library/')[1], 10);
|
||||||
|
if (isNaN(libraryId)) return of(false);
|
||||||
return this.memberService.hasLibraryAccess(libraryId);
|
return this.memberService.hasLibraryAccess(libraryId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
|
import { LayoutMode } from 'src/app/manga-reader/_models/layout-mode';
|
||||||
import { PageSplitOption } from './page-split-option';
|
import { PageSplitOption } from './page-split-option';
|
||||||
import { READER_MODE } from './reader-mode';
|
import { ReaderMode } from './reader-mode';
|
||||||
import { ReadingDirection } from './reading-direction';
|
import { ReadingDirection } from './reading-direction';
|
||||||
import { ScalingOption } from './scaling-option';
|
import { ScalingOption } from './scaling-option';
|
||||||
import { SiteTheme } from './site-theme';
|
import { SiteTheme } from './site-theme';
|
||||||
@ -10,8 +11,11 @@ export interface Preferences {
|
|||||||
readingDirection: ReadingDirection;
|
readingDirection: ReadingDirection;
|
||||||
scalingOption: ScalingOption;
|
scalingOption: ScalingOption;
|
||||||
pageSplitOption: PageSplitOption;
|
pageSplitOption: PageSplitOption;
|
||||||
readerMode: READER_MODE;
|
readerMode: ReaderMode;
|
||||||
autoCloseMenu: boolean;
|
autoCloseMenu: boolean;
|
||||||
|
layoutMode: LayoutMode;
|
||||||
|
backgroundColor: string;
|
||||||
|
showScreenHints: boolean;
|
||||||
|
|
||||||
// Book Reader
|
// Book Reader
|
||||||
bookReaderDarkMode: boolean;
|
bookReaderDarkMode: boolean;
|
||||||
@ -29,4 +33,5 @@ export interface Preferences {
|
|||||||
export const readingDirections = [{text: 'Left to Right', value: ReadingDirection.LeftToRight}, {text: 'Right to Left', value: ReadingDirection.RightToLeft}];
|
export const readingDirections = [{text: 'Left to Right', value: ReadingDirection.LeftToRight}, {text: 'Right to Left', value: ReadingDirection.RightToLeft}];
|
||||||
export const scalingOptions = [{text: 'Automatic', value: ScalingOption.Automatic}, {text: 'Fit to Height', value: ScalingOption.FitToHeight}, {text: 'Fit to Width', value: ScalingOption.FitToWidth}, {text: 'Original', value: ScalingOption.Original}];
|
export const scalingOptions = [{text: 'Automatic', value: ScalingOption.Automatic}, {text: 'Fit to Height', value: ScalingOption.FitToHeight}, {text: 'Fit to Width', value: ScalingOption.FitToWidth}, {text: 'Original', value: ScalingOption.Original}];
|
||||||
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: READER_MODE.MANGA_LR}, {text: 'Up to Down', value: READER_MODE.MANGA_UD}, {text: 'Webtoon', value: READER_MODE.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}];
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
export enum READER_MODE {
|
/**
|
||||||
|
* The pagination method used by the reader
|
||||||
|
*/
|
||||||
|
export enum ReaderMode {
|
||||||
/**
|
/**
|
||||||
* Manga default left/right to page
|
* Manga default left/right to page
|
||||||
*/
|
*/
|
||||||
MANGA_LR = 0,
|
LeftRight = 0,
|
||||||
/**
|
/**
|
||||||
* Manga up and down to page
|
* Manga up and down to page
|
||||||
*/
|
*/
|
||||||
MANGA_UD = 1,
|
UpDown = 1,
|
||||||
/**
|
/**
|
||||||
* Webtoon reading (scroll) with optional areas to tap
|
* Webtoon reading (scroll) with optional areas to tap
|
||||||
*/
|
*/
|
||||||
WEBTOON = 2
|
Webtoon = 2
|
||||||
}
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Direction the user is reading. Maps to the pagination method. Not applicable with ReaderMode.Webtoon
|
||||||
|
*/
|
||||||
export enum ReadingDirection {
|
export enum ReadingDirection {
|
||||||
LeftToRight = 0,
|
LeftToRight = 0,
|
||||||
RightToLeft = 1
|
RightToLeft = 1
|
||||||
|
@ -1,6 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* How the image should scale to the screen size
|
||||||
|
*/
|
||||||
export enum ScalingOption {
|
export enum ScalingOption {
|
||||||
|
/**
|
||||||
|
* Fit the image into the height of screen
|
||||||
|
*/
|
||||||
FitToHeight = 0,
|
FitToHeight = 0,
|
||||||
|
/**
|
||||||
|
* Fit the image into the width of screen
|
||||||
|
*/
|
||||||
FitToWidth = 1,
|
FitToWidth = 1,
|
||||||
|
/**
|
||||||
|
* Apply no logic and render the image as is
|
||||||
|
*/
|
||||||
Original = 2,
|
Original = 2,
|
||||||
|
/**
|
||||||
|
* Ask the reader to attempt to choose the best ScalingOption for the user
|
||||||
|
*/
|
||||||
Automatic = 3
|
Automatic = 3
|
||||||
}
|
}
|
||||||
|
@ -162,6 +162,20 @@ export class AccountService implements OnDestroy {
|
|||||||
return this.httpClient.post(this.baseUrl + 'account/update', model);
|
return this.httpClient.post(this.baseUrl + 'account/update', model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will get latest preferences for a user and cache them into user store
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getPreferences() {
|
||||||
|
return this.httpClient.get<Preferences>(this.baseUrl + 'users/get-preferences').pipe(map(pref => {
|
||||||
|
if (this.currentUser !== undefined || this.currentUser != null) {
|
||||||
|
this.currentUser.preferences = pref;
|
||||||
|
this.setCurrentUser(this.currentUser);
|
||||||
|
}
|
||||||
|
return pref;
|
||||||
|
}), takeUntil(this.onDestroy));
|
||||||
|
}
|
||||||
|
|
||||||
updatePreferences(userPreferences: Preferences) {
|
updatePreferences(userPreferences: Preferences) {
|
||||||
return this.httpClient.post<Preferences>(this.baseUrl + 'users/update-preferences', userPreferences).pipe(map(settings => {
|
return this.httpClient.post<Preferences>(this.baseUrl + 'users/update-preferences', userPreferences).pipe(map(settings => {
|
||||||
if (this.currentUser !== undefined || this.currentUser != null) {
|
if (this.currentUser !== undefined || this.currentUser != null) {
|
||||||
|
@ -114,11 +114,11 @@ export class ReaderService {
|
|||||||
/**
|
/**
|
||||||
* Captures current body color and forces background color to be black. Call @see resetOverrideStyles() on destroy of component to revert changes
|
* Captures current body color and forces background color to be black. Call @see resetOverrideStyles() on destroy of component to revert changes
|
||||||
*/
|
*/
|
||||||
setOverrideStyles() {
|
setOverrideStyles(backgroundColor: string = 'black') {
|
||||||
const bodyNode = document.querySelector('body');
|
const bodyNode = document.querySelector('body');
|
||||||
if (bodyNode !== undefined && bodyNode !== null) {
|
if (bodyNode !== undefined && bodyNode !== null) {
|
||||||
this.originalBodyColor = bodyNode.style.background;
|
this.originalBodyColor = bodyNode.style.background;
|
||||||
bodyNode.setAttribute('style', 'background-color: black !important');
|
bodyNode.setAttribute('style', 'background-color: ' + backgroundColor + ' !important');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import { ThemeTestComponent } from './theme-test/theme-test.component';
|
|||||||
// TODO: Once we modularize the components, use this and measure performance impact: https://angular.io/guide/lazy-loading-ngmodules#preloading-modules
|
// TODO: Once we modularize the components, use this and measure performance impact: https://angular.io/guide/lazy-loading-ngmodules#preloading-modules
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
|
{path: '', component: UserLoginComponent},
|
||||||
{
|
{
|
||||||
path: 'admin',
|
path: 'admin',
|
||||||
canActivate: [AdminGuard],
|
canActivate: [AdminGuard],
|
||||||
@ -69,8 +70,7 @@ const routes: Routes = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{path: 'theme', component: ThemeTestComponent},
|
{path: 'theme', component: ThemeTestComponent},
|
||||||
|
|
||||||
{path: '', component: UserLoginComponent},
|
|
||||||
{path: 'login', component: UserLoginComponent}, // TODO: move this to registration module
|
{path: 'login', component: UserLoginComponent}, // TODO: move this to registration module
|
||||||
{path: '**', component: UserLoginComponent, pathMatch: 'full'}
|
{path: '**', component: UserLoginComponent, pathMatch: 'full'}
|
||||||
];
|
];
|
||||||
|
@ -8,7 +8,6 @@ import { NavService } from './_services/nav.service';
|
|||||||
import { filter } from 'rxjs/operators';
|
import { filter } from 'rxjs/operators';
|
||||||
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { ThemeService } from './theme.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@ -59,7 +58,7 @@ export class AppComponent implements OnInit {
|
|||||||
if (user) {
|
if (user) {
|
||||||
this.messageHub.createHubConnection(user, this.accountService.hasAdminRole(user));
|
this.messageHub.createHubConnection(user, this.accountService.hasAdminRole(user));
|
||||||
this.libraryService.getLibraryNames().pipe(take(1)).subscribe(() => {/* No Operation */});
|
this.libraryService.getLibraryNames().pipe(take(1)).subscribe(() => {/* No Operation */});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setDocHeight() {
|
setDocHeight() {
|
||||||
|
@ -35,6 +35,7 @@ import { RegistrationModule } from './registration/registration.module';
|
|||||||
import { GroupedTypeaheadComponent } from './grouped-typeahead/grouped-typeahead.component';
|
import { GroupedTypeaheadComponent } from './grouped-typeahead/grouped-typeahead.component';
|
||||||
import { ThemeTestComponent } from './theme-test/theme-test.component';
|
import { ThemeTestComponent } from './theme-test/theme-test.component';
|
||||||
import { PipeModule } from './pipe/pipe.module';
|
import { PipeModule } from './pipe/pipe.module';
|
||||||
|
import { ColorPickerModule } from 'ngx-color-picker';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -79,6 +80,8 @@ import { PipeModule } from './pipe/pipe.module';
|
|||||||
ReadingListModule,
|
ReadingListModule,
|
||||||
RegistrationModule,
|
RegistrationModule,
|
||||||
|
|
||||||
|
ColorPickerModule, // User preferences
|
||||||
|
|
||||||
NgbAccordionModule, // ThemeTest Component only
|
NgbAccordionModule, // ThemeTest Component only
|
||||||
PipeModule,
|
PipeModule,
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import { PageBookmark } from 'src/app/_models/page-bookmark';
|
|||||||
import { Series } from 'src/app/_models/series';
|
import { Series } from 'src/app/_models/series';
|
||||||
import { ImageService } from 'src/app/_services/image.service';
|
import { ImageService } from 'src/app/_services/image.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';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-bookmarks-modal',
|
selector: 'app-bookmarks-modal',
|
||||||
@ -28,7 +27,7 @@ export class BookmarksModalComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(public imageService: ImageService, private readerService: ReaderService,
|
constructor(public imageService: ImageService, private readerService: ReaderService,
|
||||||
public modal: NgbActiveModal, private downloadService: DownloadService,
|
public modal: NgbActiveModal, private downloadService: DownloadService,
|
||||||
private toastr: ToastrService, private seriesService: SeriesService) { }
|
private toastr: ToastrService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.init();
|
this.init();
|
||||||
|
@ -112,7 +112,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-4 pe-2">
|
<div class="col-md-4 col-sm-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="language" class="form-label">Language</label>
|
<label for="language" class="form-label">Language</label>
|
||||||
<app-typeahead (selectedData)="updateLanguage($event)" [settings]="languageSettings"
|
<app-typeahead (selectedData)="updateLanguage($event)" [settings]="languageSettings"
|
||||||
@ -127,7 +127,7 @@
|
|||||||
</app-typeahead>
|
</app-typeahead>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 pe-2">
|
<div class="col-md-4 col-sm-12 pe-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="age-rating" class="form-label">Age Rating</label>
|
<label for="age-rating" class="form-label">Age Rating</label>
|
||||||
<div class="input-group {{metadata.ageRatingLocked ? 'lock-active' : ''}}">
|
<div class="input-group {{metadata.ageRatingLocked ? 'lock-active' : ''}}">
|
||||||
@ -138,7 +138,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4 col-sm-12">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="publication-status" class="form-label">Publication Status</label>
|
<label for="publication-status" class="form-label">Publication Status</label>
|
||||||
<div class="input-group {{metadata.publicationStatusLocked ? 'lock-active' : ''}}">
|
<div class="input-group {{metadata.publicationStatusLocked ? 'lock-active' : ''}}">
|
||||||
|
@ -160,13 +160,11 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.editSeriesForm.get('ageRating')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
this.editSeriesForm.get('ageRating')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||||
this.metadata.ageRating = parseInt(val + '', 10);
|
this.metadata.ageRating = parseInt(val + '', 10);
|
||||||
if (!this.editSeriesForm.get('ageRating')?.touched) return;
|
|
||||||
this.metadata.ageRatingLocked = true;
|
this.metadata.ageRatingLocked = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.editSeriesForm.get('publicationStatus')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
this.editSeriesForm.get('publicationStatus')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||||
this.metadata.publicationStatus = parseInt(val + '', 10);
|
this.metadata.publicationStatus = parseInt(val + '', 10);
|
||||||
if (!this.editSeriesForm.get('publicationStatus')?.touched) return;
|
|
||||||
this.metadata.publicationStatusLocked = true;
|
this.metadata.publicationStatusLocked = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
14
UI/Web/src/app/manga-reader/_models/layout-mode.ts
Normal file
14
UI/Web/src/app/manga-reader/_models/layout-mode.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* How to layout pages for reading
|
||||||
|
*/
|
||||||
|
export enum LayoutMode {
|
||||||
|
/**
|
||||||
|
* Renders a single page on the renderer. Cover images will follow splitting logic.
|
||||||
|
*/
|
||||||
|
Single = 1,
|
||||||
|
/**
|
||||||
|
* Renders 2 pages side by side on the renderer. Cover images will not split and take up both panes.
|
||||||
|
*/
|
||||||
|
Double = 2,
|
||||||
|
|
||||||
|
}
|
@ -50,4 +50,4 @@
|
|||||||
50% {
|
50% {
|
||||||
filter: opacity(0.25);
|
filter: opacity(0.25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,13 @@
|
|||||||
{{subtitle}}
|
{{subtitle}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-left: auto; padding-right: 3%;">
|
<div style="margin-left: auto; padding-right: 3%;">
|
||||||
|
<button class="btn btn-icon btn-small" title="Shortcuts" (click)="openShortcutModal()">
|
||||||
|
<i class="fa-regular fa-rectangle-list" aria-hidden="true"></i>
|
||||||
|
<span class="visually-hidden">Keyboard Shortcuts Modal</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-icon btn-small" role="checkbox" [attr.aria-checked]="pageBookmarked" title="{{pageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}" (click)="bookmarkPage()"><i class="{{pageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i><span class="visually-hidden">{{pageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}</span></button>
|
<button class="btn btn-icon btn-small" role="checkbox" [attr.aria-checked]="pageBookmarked" title="{{pageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}" (click)="bookmarkPage()"><i class="{{pageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i><span class="visually-hidden">{{pageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -23,37 +29,49 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div (click)="toggleMenu()" class="reading-area">
|
<div (click)="toggleMenu()" class="reading-area" [ngStyle]="{'background-color': backgroundColor}">
|
||||||
<!-- TODO: Change this logic to only render for split pages and use image for all else-->
|
<ng-container *ngIf="readerMode !== ReaderMode.Webtoon">
|
||||||
<canvas style="display: none;" #content class="{{getFittingOptionClass()}} {{readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}"
|
<div [ngClass]="{'d-none': !renderWithCanvas }">
|
||||||
ondragstart="return false;" onselectstart="return false;">
|
<canvas #content class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}}"
|
||||||
</canvas>
|
ondragstart="return false;" onselectstart="return false;">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
<div [ngClass]="{'d-none': renderWithCanvas, 'center-double': layoutMode === LayoutMode.Double && !isCoverImage(), 'fit-to-height-double-offset': this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.HEIGHT && layoutMode === LayoutMode.Double && !isCoverImage() && utilityService.getActiveBreakpoint() >= Breakpoint.Tablet}">
|
||||||
|
<img [src]="canvasImage.src" id="image-1"
|
||||||
|
class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}">
|
||||||
|
|
||||||
|
<ng-container *ngIf="layoutMode === LayoutMode.Double && !isCoverImage()">
|
||||||
|
<img [src]="canvasImage2.src" id="image-2" class="image-2 {{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}">
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="readerMode === ReaderMode.Webtoon">
|
||||||
|
<div class="webtoon-images" *ngIf="readerMode === ReaderMode.Webtoon && !isLoading && !inSetup">
|
||||||
|
<app-infinite-scroller [pageNum]="pageNum"
|
||||||
|
[bufferPages]="5"
|
||||||
|
[goToPage]="goToPageEvent"
|
||||||
|
(pageNumberChange)="handleWebtoonPageChange($event)"
|
||||||
|
[totalPages]="maxPages"
|
||||||
|
[urlProvider]="getPageUrl"
|
||||||
|
(loadNextChapter)="loadNextChapter()"
|
||||||
|
(loadPrevChapter)="loadPrevChapter()"
|
||||||
|
[bookmarkPage]="showBookmarkEffectEvent"
|
||||||
|
[fullscreenToggled]="fullscreenEvent"></app-infinite-scroller>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<div *ngIf="isCoverImage && shouldRenderAsFitSplit()">
|
|
||||||
<img [src]="canvasImage.src" class="{{getFittingOptionClass()}} {{readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}">
|
<ng-container *ngIf="readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown">
|
||||||
</div>
|
<div class="pagination-area {{readerMode === ReaderMode.LeftRight ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')">
|
||||||
<div class="webtoon-images" *ngIf="readerMode === READER_MODE.WEBTOON && !isLoading && !inSetup">
|
|
||||||
<app-infinite-scroller [pageNum]="pageNum"
|
|
||||||
[bufferPages]="5"
|
|
||||||
[goToPage]="goToPageEvent"
|
|
||||||
(pageNumberChange)="handleWebtoonPageChange($event)"
|
|
||||||
[totalPages]="maxPages"
|
|
||||||
[urlProvider]="getPageUrl"
|
|
||||||
(loadNextChapter)="loadNextChapter()"
|
|
||||||
(loadPrevChapter)="loadPrevChapter()"
|
|
||||||
[bookmarkPage]="showBookmarkEffectEvent"
|
|
||||||
[fullscreenToggled]="fullscreenEvent"></app-infinite-scroller>
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD">
|
|
||||||
<div class="pagination-area {{readerMode === READER_MODE.MANGA_LR ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')">
|
|
||||||
<div *ngIf="showClickOverlay">
|
<div *ngIf="showClickOverlay">
|
||||||
<i class="fa fa-angle-{{readingDirection === ReadingDirection.LeftToRight ? 'double-' : ''}}{{readerMode === READER_MODE.MANGA_LR ? 'right' : 'down'}}"
|
<i class="fa fa-angle-{{readingDirection === ReadingDirection.LeftToRight ? 'double-' : ''}}{{readerMode === ReaderMode.LeftRight ? 'right' : 'down'}}"
|
||||||
title="Next Page" aria-hidden="true"></i>
|
title="Next Page" aria-hidden="true"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pagination-area {{readerMode === READER_MODE.MANGA_LR ? 'left' : 'top'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')">
|
<div class="pagination-area {{readerMode === ReaderMode.LeftRight ? 'left' : 'top'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')">
|
||||||
<div *ngIf="showClickOverlay">
|
<div *ngIf="showClickOverlay">
|
||||||
<i class="fa fa-angle-{{readingDirection === ReadingDirection.RightToLeft ? 'double-' : ''}}{{readerMode === READER_MODE.MANGA_LR ? 'left' : 'up'}}"
|
<i class="fa fa-angle-{{readingDirection === ReadingDirection.RightToLeft ? 'double-' : ''}}{{readerMode === ReaderMode.LeftRight ? 'left' : 'up'}}"
|
||||||
title="Previous Page" aria-hidden="true"></i>
|
title="Previous Page" aria-hidden="true"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -82,7 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row pt-4 ms-2 me-2">
|
<div class="row pt-4 ms-2 me-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<button class="btn btn-icon" (click)="setReadingDirection();resetMenuCloseTimer();" [disabled]="readerMode === READER_MODE.WEBTOON || readerMode === READER_MODE.MANGA_UD" aria-describedby="reading-direction" title="Reading Direction: {{readingDirection === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}">
|
<button class="btn btn-icon" (click)="setReadingDirection();resetMenuCloseTimer();" [disabled]="readerMode === ReaderMode.Webtoon || readerMode === ReaderMode.UpDown" aria-describedby="reading-direction" title="Reading Direction: {{readingDirection === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}">
|
||||||
<i class="fa fa-angle-double-{{readingDirection === ReadingDirection.LeftToRight ? 'right' : 'left'}}" aria-hidden="true"></i>
|
<i class="fa fa-angle-double-{{readingDirection === ReadingDirection.LeftToRight ? 'right' : 'left'}}" aria-hidden="true"></i>
|
||||||
<span id="reading-direction" class="visually-hidden">{{readingDirection === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}</span>
|
<span id="reading-direction" class="visually-hidden">{{readingDirection === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}</span>
|
||||||
</button>
|
</button>
|
||||||
@ -142,11 +160,29 @@
|
|||||||
<label for="autoCloseMenu" class="form-check-label">Auto Close Menu</label>
|
<label for="autoCloseMenu" class="form-check-label">Auto Close Menu</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="form-check">
|
<div class="mb-3">
|
||||||
<input id="autoCloseMenu" type="checkbox" aria-label="Admin" class="form-check-input" formControlName="autoCloseMenu">
|
<label id="auto-close-label" class="form-label"></label>
|
||||||
|
<div class="mb-3">
|
||||||
|
<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">
|
||||||
|
<label class="form-check-label" for="auto-close">Auto Close Menu</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-2 mb-2">
|
||||||
|
<div class="col-6">
|
||||||
|
<label for="layout-mode" class="form-label">Layout Mode</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<select class="form-control" id="page-fitting" formControlName="layoutMode">
|
||||||
|
<option value="1">Single</option>
|
||||||
|
<option value="2">Double</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,10 +14,6 @@ $pointer-offset: 5px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// .btn-icon {
|
|
||||||
// color: white;
|
|
||||||
// }
|
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
@ -32,9 +28,6 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 48%;
|
left: 48%;
|
||||||
@ -62,14 +55,21 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fitting Options
|
// Fitting Options
|
||||||
|
// .full-height {
|
||||||
|
// position: absolute;
|
||||||
|
// margin: auto;
|
||||||
|
// top: 0px;
|
||||||
|
// left: 0;
|
||||||
|
// right: 0;
|
||||||
|
// bottom: 0px;
|
||||||
|
// height: 100%;
|
||||||
|
// }
|
||||||
|
|
||||||
.full-height {
|
.full-height {
|
||||||
position: absolute;
|
width: auto;
|
||||||
margin: auto;
|
margin: 0 auto;
|
||||||
top: 0px;
|
height: 100vh;
|
||||||
left: 0;
|
vertical-align: top;
|
||||||
right: 0;
|
|
||||||
bottom: 0px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.original {
|
.original {
|
||||||
@ -84,8 +84,29 @@ canvas {
|
|||||||
|
|
||||||
.full-width {
|
.full-width {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&.double {
|
||||||
|
width: 50%
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-2 {
|
||||||
|
margin-left: 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.center-double {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fit-to-height-double-offset {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -255,4 +276,4 @@ canvas {
|
|||||||
50% {
|
50% {
|
||||||
border: 5px solid var(--primary-color);
|
border: 5px solid var(--primary-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import { ScalingOption } from '../_models/preferences/scaling-option';
|
|||||||
import { PageSplitOption } from '../_models/preferences/page-split-option';
|
import { PageSplitOption } from '../_models/preferences/page-split-option';
|
||||||
import { BehaviorSubject, forkJoin, ReplaySubject, Subject } from 'rxjs';
|
import { BehaviorSubject, forkJoin, ReplaySubject, Subject } from 'rxjs';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
import { Breakpoint, KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
||||||
import { CircularArray } from '../shared/data-structures/circular-array';
|
import { CircularArray } from '../shared/data-structures/circular-array';
|
||||||
import { MemberService } from '../_services/member.service';
|
import { MemberService } from '../_services/member.service';
|
||||||
import { Stack } from '../shared/data-structures/stack';
|
import { Stack } from '../shared/data-structures/stack';
|
||||||
@ -21,12 +21,15 @@ import { trigger, state, style, transition, animate } from '@angular/animations'
|
|||||||
import { ChapterInfo } from './_models/chapter-info';
|
import { ChapterInfo } from './_models/chapter-info';
|
||||||
import { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums';
|
import { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums';
|
||||||
import { pageSplitOptions, scalingOptions } from '../_models/preferences/preferences';
|
import { pageSplitOptions, scalingOptions } from '../_models/preferences/preferences';
|
||||||
import { READER_MODE } from '../_models/preferences/reader-mode';
|
import { ReaderMode } from '../_models/preferences/reader-mode';
|
||||||
import { MangaFormat } from '../_models/manga-format';
|
import { MangaFormat } from '../_models/manga-format';
|
||||||
import { LibraryService } from '../_services/library.service';
|
import { LibraryService } from '../_services/library.service';
|
||||||
import { LibraryType } from '../_models/library';
|
import { LibraryType } from '../_models/library';
|
||||||
|
import { ShorcutsModalComponent } from '../reader-shared/_modals/shorcuts-modal/shorcuts-modal.component';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { LayoutMode } from './_models/layout-mode';
|
||||||
|
|
||||||
const PREFETCH_PAGES = 5;
|
const PREFETCH_PAGES = 8;
|
||||||
|
|
||||||
const CHAPTER_ID_NOT_FETCHED = -2;
|
const CHAPTER_ID_NOT_FETCHED = -2;
|
||||||
const CHAPTER_ID_DOESNT_EXIST = -1;
|
const CHAPTER_ID_DOESNT_EXIST = -1;
|
||||||
@ -101,7 +104,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD;
|
pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD;
|
||||||
isFullscreen: boolean = false;
|
isFullscreen: boolean = false;
|
||||||
autoCloseMenu: boolean = true;
|
autoCloseMenu: boolean = true;
|
||||||
readerMode: READER_MODE = READER_MODE.MANGA_LR;
|
readerMode: ReaderMode = ReaderMode.LeftRight;
|
||||||
|
|
||||||
pageSplitOptions = pageSplitOptions;
|
pageSplitOptions = pageSplitOptions;
|
||||||
|
|
||||||
@ -110,7 +113,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
@ViewChild('reader') reader!: ElementRef;
|
@ViewChild('reader') reader!: ElementRef;
|
||||||
@ViewChild('content') canvas: ElementRef | undefined;
|
@ViewChild('content') canvas: ElementRef | undefined;
|
||||||
private ctx!: CanvasRenderingContext2D;
|
private ctx!: CanvasRenderingContext2D;
|
||||||
canvasImage = new Image(); // private
|
/**
|
||||||
|
* Used to render a page on the canvas or in the image tag. This Image element is prefetched by the cachedImages buffer
|
||||||
|
*/
|
||||||
|
canvasImage = new Image();
|
||||||
|
/**
|
||||||
|
* Used soley for LayoutMode.Double rendering. Will always hold the next image in buffer.
|
||||||
|
*/
|
||||||
|
canvasImage2 = new Image();
|
||||||
|
renderWithCanvas: boolean = false; // Dictates if we use render with canvas or with image
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A circular array of size PREFETCH_PAGES + 2. Maintains prefetched Images around the current page to load from to avoid loading animation.
|
* A circular array of size PREFETCH_PAGES + 2. Maintains prefetched Images around the current page to load from to avoid loading animation.
|
||||||
@ -140,6 +151,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
* If the menu is open/visible.
|
* If the menu is open/visible.
|
||||||
*/
|
*/
|
||||||
menuOpen = false;
|
menuOpen = false;
|
||||||
|
/**
|
||||||
|
* Image Viewer collapsed
|
||||||
|
*/
|
||||||
|
imageViewerCollapsed = true;
|
||||||
/**
|
/**
|
||||||
* If the prev page allows a page change to occur.
|
* If the prev page allows a page change to occur.
|
||||||
*/
|
*/
|
||||||
@ -226,6 +241,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
* Used for webtoon reader. When loading pages or data, this will disable the reader
|
* Used for webtoon reader. When loading pages or data, this will disable the reader
|
||||||
*/
|
*/
|
||||||
inSetup: boolean = true;
|
inSetup: boolean = true;
|
||||||
|
/**
|
||||||
|
* If we render 2 pages at once or 1
|
||||||
|
*/
|
||||||
|
layoutMode: LayoutMode = LayoutMode.Single;
|
||||||
|
/**
|
||||||
|
* Background color for canvas/reader. User configured.
|
||||||
|
*/
|
||||||
|
backgroundColor: string = '#FFFFFF';
|
||||||
|
|
||||||
|
|
||||||
private readonly onDestroy = new Subject<void>();
|
private readonly onDestroy = new Subject<void>();
|
||||||
|
|
||||||
@ -233,7 +257,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
getPageUrl = (pageNum: number) => this.readerService.getPageUrl(this.chapterId, pageNum);
|
getPageUrl = (pageNum: number) => this.readerService.getPageUrl(this.chapterId, pageNum);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
get pageBookmarked() {
|
get pageBookmarked() {
|
||||||
return this.bookmarks.hasOwnProperty(this.pageNum);
|
return this.bookmarks.hasOwnProperty(this.pageNum);
|
||||||
}
|
}
|
||||||
@ -250,33 +273,46 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
get readerModeIcon() {
|
get readerModeIcon() {
|
||||||
switch(this.readerMode) {
|
switch(this.readerMode) {
|
||||||
case READER_MODE.MANGA_LR:
|
case ReaderMode.LeftRight:
|
||||||
return 'fa-exchange-alt';
|
return 'fa-exchange-alt';
|
||||||
case READER_MODE.MANGA_UD:
|
case ReaderMode.UpDown:
|
||||||
return 'fa-exchange-alt fa-rotate-90';
|
return 'fa-exchange-alt fa-rotate-90';
|
||||||
case READER_MODE.WEBTOON:
|
case ReaderMode.Webtoon:
|
||||||
return 'fa-arrows-alt-v';
|
return 'fa-arrows-alt-v';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get READER_MODE(): typeof READER_MODE {
|
get ReaderMode() {
|
||||||
return READER_MODE;
|
return ReaderMode;
|
||||||
|
}
|
||||||
|
get LayoutMode() {
|
||||||
|
return LayoutMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
get ReadingDirection(): typeof ReadingDirection {
|
get ReadingDirection() {
|
||||||
return ReadingDirection;
|
return ReadingDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
get PageSplitOption(): typeof PageSplitOption {
|
get PageSplitOption() {
|
||||||
return PageSplitOption;
|
return PageSplitOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get Breakpoint() {
|
||||||
|
return Breakpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
get FITTING_OPTION() {
|
||||||
|
return FITTING_OPTION;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
||||||
public readerService: ReaderService, private location: Location,
|
public readerService: ReaderService, private location: Location,
|
||||||
private formBuilder: FormBuilder, private navService: NavService,
|
private formBuilder: FormBuilder, private navService: NavService,
|
||||||
private toastr: ToastrService, private memberService: MemberService,
|
private toastr: ToastrService, private memberService: MemberService,
|
||||||
private libraryService: LibraryService, private utilityService: UtilityService,
|
private libraryService: LibraryService, public utilityService: UtilityService,
|
||||||
private renderer: Renderer2, @Inject(DOCUMENT) private document: Document) {
|
private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, private modalService: NgbModal) {
|
||||||
this.navService.hideNavBar();
|
this.navService.hideNavBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,8 +340,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.continuousChaptersStack.push(this.chapterId);
|
this.continuousChaptersStack.push(this.chapterId);
|
||||||
|
|
||||||
this.readerService.setOverrideStyles();
|
|
||||||
|
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
if (user) {
|
if (user) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
@ -314,16 +348,28 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.pageSplitOption = this.user.preferences.pageSplitOption;
|
this.pageSplitOption = this.user.preferences.pageSplitOption;
|
||||||
this.autoCloseMenu = this.user.preferences.autoCloseMenu;
|
this.autoCloseMenu = this.user.preferences.autoCloseMenu;
|
||||||
this.readerMode = this.user.preferences.readerMode;
|
this.readerMode = this.user.preferences.readerMode;
|
||||||
|
this.layoutMode = this.user.preferences.layoutMode || LayoutMode.Single;
|
||||||
|
this.backgroundColor = this.user.preferences.backgroundColor || '#000000';
|
||||||
|
this.readerService.setOverrideStyles(this.backgroundColor);
|
||||||
|
|
||||||
|
|
||||||
this.generalSettingsForm = this.formBuilder.group({
|
this.generalSettingsForm = this.formBuilder.group({
|
||||||
autoCloseMenu: this.autoCloseMenu,
|
autoCloseMenu: this.autoCloseMenu,
|
||||||
pageSplitOption: this.pageSplitOption,
|
pageSplitOption: this.pageSplitOption,
|
||||||
fittingOption: this.translateScalingOption(this.scalingOption)
|
fittingOption: this.translateScalingOption(this.scalingOption),
|
||||||
|
layoutMode: this.layoutMode
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateForm();
|
this.updateForm();
|
||||||
|
|
||||||
|
this.generalSettingsForm.get('layoutMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||||
|
this.layoutMode = parseInt(val, 10);
|
||||||
|
if (this.layoutMode === LayoutMode.Double) {
|
||||||
|
// Update canvasImage2
|
||||||
|
this.canvasImage2 = this.cachedImages.next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => {
|
this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => {
|
||||||
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
|
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
|
||||||
@ -370,17 +416,16 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
@HostListener('window:keyup', ['$event'])
|
@HostListener('window:keyup', ['$event'])
|
||||||
handleKeyPress(event: KeyboardEvent) {
|
handleKeyPress(event: KeyboardEvent) {
|
||||||
|
|
||||||
switch (this.readerMode) {
|
switch (this.readerMode) {
|
||||||
case READER_MODE.MANGA_LR:
|
case ReaderMode.LeftRight:
|
||||||
if (event.key === KEY_CODES.RIGHT_ARROW) {
|
if (event.key === KEY_CODES.RIGHT_ARROW) {
|
||||||
this.readingDirection === ReadingDirection.LeftToRight ? this.nextPage() : this.prevPage();
|
this.readingDirection === ReadingDirection.LeftToRight ? this.nextPage() : this.prevPage();
|
||||||
} else if (event.key === KEY_CODES.LEFT_ARROW) {
|
} else if (event.key === KEY_CODES.LEFT_ARROW) {
|
||||||
this.readingDirection === ReadingDirection.LeftToRight ? this.prevPage() : this.nextPage();
|
this.readingDirection === ReadingDirection.LeftToRight ? this.prevPage() : this.nextPage();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case READER_MODE.MANGA_UD:
|
case ReaderMode.UpDown:
|
||||||
case READER_MODE.WEBTOON:
|
case ReaderMode.Webtoon:
|
||||||
if (event.key === KEY_CODES.DOWN_ARROW) {
|
if (event.key === KEY_CODES.DOWN_ARROW) {
|
||||||
this.nextPage()
|
this.nextPage()
|
||||||
} else if (event.key === KEY_CODES.UP_ARROW) {
|
} else if (event.key === KEY_CODES.UP_ARROW) {
|
||||||
@ -514,7 +559,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.readerMode === READER_MODE.WEBTOON) {
|
if (this.readerMode === ReaderMode.Webtoon) {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
} else {
|
} else {
|
||||||
this.loadPage();
|
this.loadPage();
|
||||||
@ -535,6 +580,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.title += ' - ' + chapterInfo.chapterTitle;
|
this.title += ' - ' + chapterInfo.chapterTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Move this to the backend
|
||||||
this.subtitle = '';
|
this.subtitle = '';
|
||||||
if (chapterInfo.isSpecial && chapterInfo.volumeNumber === '0') {
|
if (chapterInfo.isSpecial && chapterInfo.volumeNumber === '0') {
|
||||||
this.subtitle = chapterInfo.fileName;
|
this.subtitle = chapterInfo.fileName;
|
||||||
@ -581,10 +627,21 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
getFittingOptionClass() {
|
getFittingOptionClass() {
|
||||||
const formControl = this.generalSettingsForm.get('fittingOption');
|
const formControl = this.generalSettingsForm.get('fittingOption');
|
||||||
|
let val = FITTING_OPTION.HEIGHT;
|
||||||
if (formControl === undefined) {
|
if (formControl === undefined) {
|
||||||
return FITTING_OPTION.HEIGHT;
|
val = FITTING_OPTION.HEIGHT;
|
||||||
}
|
}
|
||||||
return formControl?.value;
|
val = formControl?.value;
|
||||||
|
|
||||||
|
if (this.isCoverImage() && this.shouldRenderAsFitSplit()) {
|
||||||
|
// Rewriting to fit to width for this cover image
|
||||||
|
val = FITTING_OPTION.WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isCoverImage() && this.layoutMode === LayoutMode.Double) {
|
||||||
|
return val + ' double';
|
||||||
|
}
|
||||||
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFittingIcon() {
|
getFittingIcon() {
|
||||||
@ -703,7 +760,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePageChange(event: any, direction: string) {
|
handlePageChange(event: any, direction: string) {
|
||||||
if (this.readerMode === READER_MODE.WEBTOON) {
|
if (this.readerMode === ReaderMode.Webtoon) {
|
||||||
if (direction === 'right') {
|
if (direction === 'right') {
|
||||||
this.nextPage(event);
|
this.nextPage(event);
|
||||||
} else {
|
} else {
|
||||||
@ -738,12 +795,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.pagingDirection = PAGING_DIRECTION.FORWARD;
|
this.pagingDirection = PAGING_DIRECTION.FORWARD;
|
||||||
if (this.isNoSplit() || notInSplit) {
|
if (this.isNoSplit() || notInSplit) {
|
||||||
this.setPageNum(this.pageNum + 1);
|
this.setPageNum(this.pageNum + 1);
|
||||||
if (this.readerMode !== READER_MODE.WEBTOON) {
|
if (this.readerMode !== ReaderMode.Webtoon) {
|
||||||
this.canvasImage = this.cachedImages.next();
|
this.canvasImage = this.cachedImages.next();
|
||||||
|
this.canvasImage2 = this.cachedImages.peek(2);
|
||||||
|
console.log('[nextPage] canvasImage: ', this.canvasImage);
|
||||||
|
console.log('[nextPage] canvasImage2: ', this.canvasImage2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.readerMode !== READER_MODE.WEBTOON) {
|
if (this.readerMode !== ReaderMode.Webtoon) {
|
||||||
this.loadPage();
|
this.loadPage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -769,9 +829,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (this.isNoSplit() || notInSplit) {
|
if (this.isNoSplit() || notInSplit) {
|
||||||
this.setPageNum(this.pageNum - 1);
|
this.setPageNum(this.pageNum - 1);
|
||||||
this.canvasImage = this.cachedImages.prev();
|
this.canvasImage = this.cachedImages.prev();
|
||||||
|
this.canvasImage2 = this.cachedImages.peek(-2);
|
||||||
|
console.log('[prevPage] canvasImage: ', this.canvasImage);
|
||||||
|
console.log('[prevPage] canvasImage2: ', this.canvasImage2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.readerMode !== READER_MODE.WEBTOON) {
|
if (this.readerMode !== ReaderMode.Webtoon) {
|
||||||
this.loadPage();
|
this.loadPage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -877,69 +940,19 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
const needsSplitting = this.isCoverImage();
|
const needsSplitting = this.isCoverImage();
|
||||||
this.updateSplitPage();
|
this.updateSplitPage();
|
||||||
|
|
||||||
|
|
||||||
if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.LEFT_PART) {
|
if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.LEFT_PART) {
|
||||||
this.canvas.nativeElement.width = this.canvasImage.width / 2;
|
this.canvas.nativeElement.width = this.canvasImage.width / 2;
|
||||||
this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, 0, 0, this.canvasImage.width, this.canvasImage.height);
|
this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, 0, 0, this.canvasImage.width, this.canvasImage.height);
|
||||||
|
this.renderWithCanvas = true;
|
||||||
|
console.log('[Render] Canvas')
|
||||||
} else if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.RIGHT_PART) {
|
} else if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.RIGHT_PART) {
|
||||||
this.canvas.nativeElement.width = this.canvasImage.width / 2;
|
this.canvas.nativeElement.width = this.canvasImage.width / 2;
|
||||||
this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, -this.canvasImage.width / 2, 0, this.canvasImage.width, this.canvasImage.height);
|
this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, -this.canvasImage.width / 2, 0, this.canvasImage.width, this.canvasImage.height);
|
||||||
|
this.renderWithCanvas = true;
|
||||||
|
console.log('[Render] Canvas')
|
||||||
} else {
|
} else {
|
||||||
if (!this.firstPageRendered && this.scalingOption === ScalingOption.Automatic) {
|
this.renderWithCanvas = false;
|
||||||
this.updateScalingForFirstPageRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fit Split on a page that needs splitting
|
|
||||||
if (!this.shouldRenderAsFitSplit()) {
|
|
||||||
this.setCanvasSize();
|
|
||||||
this.ctx.drawImage(this.canvasImage, 0, 0);
|
|
||||||
|
|
||||||
// Reset scroll on non HEIGHT Fits
|
|
||||||
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isLoading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const windowWidth = window.innerWidth
|
|
||||||
|| document.documentElement.clientWidth
|
|
||||||
|| document.body.clientWidth;
|
|
||||||
const windowHeight = window.innerHeight
|
|
||||||
|| document.documentElement.clientHeight
|
|
||||||
|| document.body.clientHeight;
|
|
||||||
// If the user's screen is wider than the image, just pretend this is no split, as it will render nicer
|
|
||||||
this.canvas.nativeElement.width = windowWidth;
|
|
||||||
this.canvas.nativeElement.height = windowHeight;
|
|
||||||
const ratio = this.canvasImage.width / this.canvasImage.height;
|
|
||||||
let newWidth = windowWidth;
|
|
||||||
let newHeight = newWidth / ratio;
|
|
||||||
if (newHeight > windowHeight) {
|
|
||||||
newHeight = windowHeight;
|
|
||||||
newWidth = newHeight * ratio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optimization: When the screen is larger than newWidth, allow no split rendering to occur for a better fit
|
|
||||||
// if (windowWidth > newWidth) {
|
|
||||||
// this.setCanvasSize();
|
|
||||||
// this.ctx.drawImage(this.canvasImage, 0, 0);
|
|
||||||
// } else {
|
|
||||||
// this.setCanvasSize();
|
|
||||||
// //this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
|
||||||
// this.ctx.drawImage(this.canvasImage, 0, 0, newWidth, newHeight);
|
|
||||||
// }
|
|
||||||
|
|
||||||
this.setCanvasSize();
|
|
||||||
// var offScreenCanvas = document.createElement('canvas')
|
|
||||||
// offScreenCanvas.width = newWidth;
|
|
||||||
// offScreenCanvas.height = newHeight;
|
|
||||||
// const resizedImage = new Image();
|
|
||||||
// pica.resize(this.canvasImage, offScreenCanvas);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//this.document.querySelector('.reading-area')?.appendChild(this.canvasImage);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset scroll on non HEIGHT Fits
|
// Reset scroll on non HEIGHT Fits
|
||||||
@ -984,7 +997,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
shouldRenderAsFitSplit() {
|
shouldRenderAsFitSplit() {
|
||||||
// Some pages aren't cover images but might need fit split renderings
|
// Some pages aren't cover images but might need fit split renderings
|
||||||
if (parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10) !== PageSplitOption.FitSplit) return false;
|
if (parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10) !== PageSplitOption.FitSplit) return false;
|
||||||
//if (!this.isCoverImage() || parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10) !== PageSplitOption.FitSplit) return false;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1011,9 +1023,16 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.canvasImage = this.cachedImages.current();
|
this.canvasImage = this.cachedImages.current();
|
||||||
|
this.canvasImage2 = this.cachedImages.next(); // TODO: Do I need this here?
|
||||||
|
console.log('[loadPage] canvasImage: ', this.canvasImage);
|
||||||
|
console.log('[loadPage] canvasImage2: ', this.canvasImage2);
|
||||||
if (this.readerService.imageUrlToPageNum(this.canvasImage.src) !== this.pageNum || this.canvasImage.src === '' || !this.canvasImage.complete) {
|
if (this.readerService.imageUrlToPageNum(this.canvasImage.src) !== this.pageNum || this.canvasImage.src === '' || !this.canvasImage.complete) {
|
||||||
this.canvasImage.src = this.readerService.getPageUrl(this.chapterId, this.pageNum);
|
this.canvasImage.src = this.readerService.getPageUrl(this.chapterId, this.pageNum);
|
||||||
|
this.canvasImage2.src = this.readerService.getPageUrl(this.chapterId, this.pageNum + 1); // TODO: I need to handle last page correctly
|
||||||
this.canvasImage.onload = () => this.renderPage();
|
this.canvasImage.onload = () => this.renderPage();
|
||||||
|
|
||||||
|
console.log('[loadPage] (after setting) canvasImage: ', this.canvasImage);
|
||||||
|
console.log('[loadPage] (after setting) canvasImage2: ', this.canvasImage2);
|
||||||
} else {
|
} else {
|
||||||
this.renderPage();
|
this.renderPage();
|
||||||
}
|
}
|
||||||
@ -1027,7 +1046,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.readingDirection = ReadingDirection.LeftToRight;
|
this.readingDirection = ReadingDirection.LeftToRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.menuOpen) {
|
if (this.menuOpen && this.user.preferences.showScreenHints) {
|
||||||
this.showClickOverlay = true;
|
this.showClickOverlay = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.showClickOverlay = false;
|
this.showClickOverlay = false;
|
||||||
@ -1039,7 +1058,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
sliderDragUpdate(context: ChangeContext) {
|
sliderDragUpdate(context: ChangeContext) {
|
||||||
// This will update the value for value except when in webtoon due to how the webtoon reader
|
// This will update the value for value except when in webtoon due to how the webtoon reader
|
||||||
// responds to page changes
|
// responds to page changes
|
||||||
if (this.readerMode !== READER_MODE.WEBTOON) {
|
if (this.readerMode !== ReaderMode.Webtoon) {
|
||||||
this.setPageNum(context.value);
|
this.setPageNum(context.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1142,15 +1161,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
toggleReaderMode() {
|
toggleReaderMode() {
|
||||||
switch(this.readerMode) {
|
switch(this.readerMode) {
|
||||||
case READER_MODE.MANGA_LR:
|
case ReaderMode.LeftRight:
|
||||||
this.readerMode = READER_MODE.MANGA_UD;
|
this.readerMode = ReaderMode.UpDown;
|
||||||
this.pagingDirection = PAGING_DIRECTION.FORWARD;
|
this.pagingDirection = PAGING_DIRECTION.FORWARD;
|
||||||
break;
|
break;
|
||||||
case READER_MODE.MANGA_UD:
|
case ReaderMode.UpDown:
|
||||||
this.readerMode = READER_MODE.WEBTOON;
|
this.readerMode = ReaderMode.Webtoon;
|
||||||
break;
|
break;
|
||||||
case READER_MODE.WEBTOON:
|
case ReaderMode.Webtoon:
|
||||||
this.readerMode = READER_MODE.MANGA_LR;
|
this.readerMode = ReaderMode.LeftRight;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1160,7 +1179,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateForm() {
|
updateForm() {
|
||||||
if ( this.readerMode === READER_MODE.WEBTOON) {
|
if ( this.readerMode === ReaderMode.Webtoon) {
|
||||||
this.generalSettingsForm.get('fittingOption')?.disable()
|
this.generalSettingsForm.get('fittingOption')?.disable()
|
||||||
this.generalSettingsForm.get('pageSplitOption')?.disable();
|
this.generalSettingsForm.get('pageSplitOption')?.disable();
|
||||||
} else {
|
} else {
|
||||||
@ -1178,23 +1197,45 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
bookmarkPage() {
|
bookmarkPage() {
|
||||||
const pageNum = this.pageNum;
|
const pageNum = this.pageNum;
|
||||||
|
|
||||||
|
// TODO: Handle LayoutMode.Double
|
||||||
|
|
||||||
if (this.pageBookmarked) {
|
if (this.pageBookmarked) {
|
||||||
this.readerService.unbookmark(this.seriesId, this.volumeId, this.chapterId, pageNum).pipe(take(1)).subscribe(() => {
|
let apis = [this.readerService.unbookmark(this.seriesId, this.volumeId, this.chapterId, pageNum)];
|
||||||
|
if (this.layoutMode === LayoutMode.Double) apis.push(this.readerService.unbookmark(this.seriesId, this.volumeId, this.chapterId, pageNum + 1));
|
||||||
|
forkJoin(apis).pipe(take(1)).subscribe(() => {
|
||||||
delete this.bookmarks[pageNum];
|
delete this.bookmarks[pageNum];
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.readerService.bookmark(this.seriesId, this.volumeId, this.chapterId, pageNum).pipe(take(1)).subscribe(() => {
|
let apis = [this.readerService.bookmark(this.seriesId, this.volumeId, this.chapterId, pageNum)];
|
||||||
|
if (this.layoutMode === LayoutMode.Double) apis.push(this.readerService.bookmark(this.seriesId, this.volumeId, this.chapterId, pageNum + 1));
|
||||||
|
forkJoin(apis).pipe(take(1)).subscribe(() => {
|
||||||
this.bookmarks[pageNum] = 1;
|
this.bookmarks[pageNum] = 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show an effect on the image to show that it was bookmarked
|
// Show an effect on the image to show that it was bookmarked
|
||||||
this.showBookmarkEffectEvent.next(pageNum);
|
this.showBookmarkEffectEvent.next(pageNum);
|
||||||
if (this.readerMode != READER_MODE.WEBTOON) {
|
if (this.readerMode != ReaderMode.Webtoon) {
|
||||||
if (this.canvas) {
|
|
||||||
this.renderer.addClass(this.canvas?.nativeElement, 'bookmark-effect');
|
let elements:Array<Element | ElementRef> = [];
|
||||||
|
if (this.renderWithCanvas && this.canvas) {
|
||||||
|
elements.push(this.canvas?.nativeElement);
|
||||||
|
} else {
|
||||||
|
const image1 = this.document.querySelector('#image-1');
|
||||||
|
if (image1 != null) elements.push(image1);
|
||||||
|
|
||||||
|
if (this.layoutMode === LayoutMode.Double) {
|
||||||
|
const image2 = this.document.querySelector('#image-2');
|
||||||
|
if (image2 != null) elements.push(image2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (elements.length > 0) {
|
||||||
|
elements.forEach(elem => this.renderer.addClass(elem, 'bookmark-effect'));
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.renderer.removeClass(this.canvas?.nativeElement, 'bookmark-effect');
|
elements.forEach(elem => this.renderer.removeClass(elem, 'bookmark-effect'));
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1220,4 +1261,18 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|| document.body.clientHeight;
|
|| document.body.clientHeight;
|
||||||
return [windowWidth, windowHeight];
|
return [windowWidth, windowHeight];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openShortcutModal() {
|
||||||
|
let ref = this.modalService.open(ShorcutsModalComponent, { scrollable: true, size: 'md' });
|
||||||
|
ref.componentInstance.shortcuts = [
|
||||||
|
{key: '⇽', description: 'Move to previous page'},
|
||||||
|
{key: '⇾', description: 'Move to next page'},
|
||||||
|
{key: '↑', description: 'Move to previous page'},
|
||||||
|
{key: '↓', description: 'Move to previous page'},
|
||||||
|
{key: 'G', description: 'Open Go to Page dialog'},
|
||||||
|
{key: 'B', description: 'Bookmark current page'},
|
||||||
|
{key: 'ESC', description: 'Close reader'},
|
||||||
|
{key: 'SPACE', description: 'Toggle Menu'},
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { MangaReaderRoutingModule } from './manga-reader.router.module';
|
|||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { NgxSliderModule } from '@angular-slider/ngx-slider';
|
import { NgxSliderModule } from '@angular-slider/ngx-slider';
|
||||||
import { InfiniteScrollerComponent } from './infinite-scroller/infinite-scroller.component';
|
import { InfiniteScrollerComponent } from './infinite-scroller/infinite-scroller.component';
|
||||||
|
import { ReaderSharedModule } from '../reader-shared/reader-shared.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -22,6 +23,7 @@ import { InfiniteScrollerComponent } from './infinite-scroller/infinite-scroller
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgxSliderModule,
|
NgxSliderModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
ReaderSharedModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
MangaReaderComponent
|
MangaReaderComponent
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FilterPipe } from './filter.pipe';
|
import { FilterPipe } from './filter.pipe';
|
||||||
import { PersonRolePipe } from './person-role.pipe';
|
|
||||||
import { PublicationStatusPipe } from './publication-status.pipe';
|
import { PublicationStatusPipe } from './publication-status.pipe';
|
||||||
|
import { SentenceCasePipe } from './sentence-case.pipe';
|
||||||
|
import { PersonRolePipe } from './person-role.pipe';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -10,7 +11,8 @@ import { PublicationStatusPipe } from './publication-status.pipe';
|
|||||||
declarations: [
|
declarations: [
|
||||||
FilterPipe,
|
FilterPipe,
|
||||||
PersonRolePipe,
|
PersonRolePipe,
|
||||||
PublicationStatusPipe
|
PublicationStatusPipe,
|
||||||
|
SentenceCasePipe
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -18,7 +20,8 @@ import { PublicationStatusPipe } from './publication-status.pipe';
|
|||||||
exports: [
|
exports: [
|
||||||
FilterPipe,
|
FilterPipe,
|
||||||
PersonRolePipe,
|
PersonRolePipe,
|
||||||
PublicationStatusPipe
|
PublicationStatusPipe,
|
||||||
|
SentenceCasePipe
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class PipeModule { }
|
export class PipeModule { }
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">Keyboard Shortcuts</h4>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-6 mb-2" *ngFor="let shortcut of shortcuts">
|
||||||
|
<span><code>{{shortcut.key}}</code> {{shortcut.description}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" (click)="close()">Close</button>
|
||||||
|
</div>
|
@ -0,0 +1,34 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
export interface KeyboardShortcut {
|
||||||
|
/**
|
||||||
|
* String representing key or key combo. Should use + for combos. Will render as upper case
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
/**
|
||||||
|
* Description of how it works
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-shorcuts-modal',
|
||||||
|
templateUrl: './shorcuts-modal.component.html',
|
||||||
|
styleUrls: ['./shorcuts-modal.component.scss']
|
||||||
|
})
|
||||||
|
export class ShorcutsModalComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() shortcuts: Array<KeyboardShortcut> = [];
|
||||||
|
|
||||||
|
constructor(public modal: NgbActiveModal) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.modal.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
UI/Web/src/app/reader-shared/reader-shared.module.ts
Normal file
20
UI/Web/src/app/reader-shared/reader-shared.module.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ShorcutsModalComponent } from './_modals/shorcuts-modal/shorcuts-modal.component';
|
||||||
|
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
ShorcutsModalComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
NgbModalModule
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
ShorcutsModalComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ReaderSharedModule { }
|
@ -522,7 +522,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openEditSeriesModal() {
|
openEditSeriesModal() {
|
||||||
const modalRef = this.modalService.open(EditSeriesModalComponent, { size: 'lg' }); // scrollable: true, size: 'lg', windowClass: 'scrollable-modal' (these don't work well on mobile)
|
const modalRef = this.modalService.open(EditSeriesModalComponent, { size: 'xl' });
|
||||||
modalRef.componentInstance.series = this.series;
|
modalRef.componentInstance.series = this.series;
|
||||||
modalRef.closed.subscribe((closeResult: {success: boolean, series: Series, coverImageUpdate: boolean}) => {
|
modalRef.closed.subscribe((closeResult: {success: boolean, series: Series, coverImageUpdate: boolean}) => {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
|
@ -14,7 +14,6 @@ import { SeriesFormatComponent } from './series-format/series-format.component';
|
|||||||
import { UpdateNotificationModalComponent } from './update-notification/update-notification-modal.component';
|
import { UpdateNotificationModalComponent } from './update-notification/update-notification-modal.component';
|
||||||
import { CircularLoaderComponent } from './circular-loader/circular-loader.component';
|
import { CircularLoaderComponent } from './circular-loader/circular-loader.component';
|
||||||
import { NgCircleProgressModule } from 'ng-circle-progress';
|
import { NgCircleProgressModule } from 'ng-circle-progress';
|
||||||
import { SentenceCasePipe } from './sentence-case.pipe';
|
|
||||||
import { PersonBadgeComponent } from './person-badge/person-badge.component';
|
import { PersonBadgeComponent } from './person-badge/person-badge.component';
|
||||||
import { BadgeExpanderComponent } from './badge-expander/badge-expander.component';
|
import { BadgeExpanderComponent } from './badge-expander/badge-expander.component';
|
||||||
import { ImageComponent } from './image/image.component';
|
import { ImageComponent } from './image/image.component';
|
||||||
@ -31,7 +30,6 @@ import { ImageComponent } from './image/image.component';
|
|||||||
SeriesFormatComponent,
|
SeriesFormatComponent,
|
||||||
UpdateNotificationModalComponent,
|
UpdateNotificationModalComponent,
|
||||||
CircularLoaderComponent,
|
CircularLoaderComponent,
|
||||||
SentenceCasePipe,
|
|
||||||
PersonBadgeComponent,
|
PersonBadgeComponent,
|
||||||
BadgeExpanderComponent,
|
BadgeExpanderComponent,
|
||||||
ImageComponent
|
ImageComponent
|
||||||
@ -46,7 +44,6 @@ import { ImageComponent } from './image/image.component';
|
|||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
SafeHtmlPipe, // Used globally
|
SafeHtmlPipe, // Used globally
|
||||||
SentenceCasePipe, // Used globablly
|
|
||||||
ReadMoreComponent, // Used globably
|
ReadMoreComponent, // Used globably
|
||||||
DrawerComponent, // Can be replaced with boostrap offscreen canvas (v5)
|
DrawerComponent, // Can be replaced with boostrap offscreen canvas (v5)
|
||||||
ShowIfScrollbarDirective, // Used book reader only?
|
ShowIfScrollbarDirective, // Used book reader only?
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<span class="visually-hidden">Field is locked</span>
|
<span class="visually-hidden">Field is locked</span>
|
||||||
</span>
|
</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="typeahead-input" [ngStyle]="{'width': (settings.showLocked ? '93%' : '100%')}" (click)="onInputFocus($event)">
|
<div class="typeahead-input" (click)="onInputFocus($event)">
|
||||||
<app-tag-badge *ngFor="let option of optionSelection.selected(); let i = index">
|
<app-tag-badge *ngFor="let option of optionSelection.selected(); let i = index">
|
||||||
<ng-container [ngTemplateOutlet]="badgeTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: i }"></ng-container>
|
<ng-container [ngTemplateOutlet]="badgeTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: i }"></ng-container>
|
||||||
<i class="fa fa-times" (click)="toggleSelection(option)" tabindex="0" aria-label="close"></i>
|
<i class="fa fa-times" (click)="toggleSelection(option)" tabindex="0" aria-label="close"></i>
|
||||||
|
@ -94,14 +94,49 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
|
<label for="settings-layoutmode-option" class="form-label">Layout Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #layoutModeTooltip>Render a single image to the screen to two side-by-side images</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-layoutmode-option-help"><ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container></span>
|
||||||
|
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode" id="settings-layoutmode-option">
|
||||||
|
<option *ngFor="let opt of layoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
|
<label for="settings-backgroundcolor-option" class="form-label">Background Color</label>
|
||||||
|
<input [value]="user.preferences.backgroundColor"
|
||||||
|
class="form-control"
|
||||||
|
(colorPickerChange)="settingsForm.markAsTouched()"
|
||||||
|
[style.background]="user.preferences.backgroundColor"
|
||||||
|
[cpAlphaChannel]="'disabled'"
|
||||||
|
[(colorPicker)]="user.preferences.backgroundColor"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="row g-0">
|
||||||
<label id="auto-close-label" class="form-label"></label>
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="form-check form-switch">
|
<label id="auto-close-label" class="form-label"></label>
|
||||||
<input type="checkbox" id="auto-close" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
<div class="mb-3">
|
||||||
<label class="form-check-label" for="auto-close">Auto Close Menu</label>
|
<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">
|
||||||
|
<label class="form-check-label" for="auto-close">Auto Close Menu</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label id="show-screen-hints-label" class="form-label"></label>
|
||||||
|
<div class="mb-3">
|
||||||
|
<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">
|
||||||
|
<label class="form-check-label" for="show-screen-hints">Show Screen Hints</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,12 +5,13 @@ import { take } from 'rxjs/operators';
|
|||||||
import { Options } from '@angular-slider/ngx-slider';
|
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 } from 'src/app/_models/preferences/preferences';
|
import { readingDirections, scalingOptions, pageSplitOptions, readingModes, Preferences, 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 { NavService } from 'src/app/_services/nav.service';
|
||||||
import { ActivatedRoute } 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 { forkJoin } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-preferences',
|
selector: 'app-user-preferences',
|
||||||
@ -23,6 +24,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
scalingOptions = scalingOptions;
|
scalingOptions = scalingOptions;
|
||||||
pageSplitOptions = pageSplitOptions;
|
pageSplitOptions = pageSplitOptions;
|
||||||
readingModes = readingModes;
|
readingModes = readingModes;
|
||||||
|
layoutModes = layoutModes;
|
||||||
|
|
||||||
settingsForm: FormGroup = new FormGroup({});
|
settingsForm: FormGroup = new FormGroup({});
|
||||||
passwordChangeForm: FormGroup = new FormGroup({});
|
passwordChangeForm: FormGroup = new FormGroup({});
|
||||||
@ -63,8 +65,11 @@ 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
|
||||||
|
|
||||||
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 navService: NavService, private titleService: Title, private route: ActivatedRoute, private settingsService: SettingsService,
|
||||||
|
private router: Router) {
|
||||||
this.fontFamilies = this.bookService.getFontFamilies();
|
this.fontFamilies = this.bookService.getFontFamilies();
|
||||||
|
|
||||||
this.route.fragment.subscribe(frag => {
|
this.route.fragment.subscribe(frag => {
|
||||||
@ -83,31 +88,41 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.titleService.setTitle('Kavita - User Preferences');
|
this.titleService.setTitle('Kavita - User Preferences');
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe((user) => {
|
|
||||||
if (user) {
|
|
||||||
this.user = user;
|
|
||||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
|
||||||
this.hasChangePasswordRole = this.accountService.hasChangePasswordRole(user);
|
|
||||||
|
|
||||||
if (this.fontFamilies.indexOf(this.user.preferences.bookReaderFontFamily) < 0) {
|
forkJoin({
|
||||||
this.user.preferences.bookReaderFontFamily = 'default';
|
user: this.accountService.currentUser$.pipe(take(1)),
|
||||||
}
|
pref: this.accountService.getPreferences()
|
||||||
|
}).subscribe(results => {
|
||||||
this.settingsForm.addControl('readingDirection', new FormControl(user.preferences.readingDirection, []));
|
if (results.user === undefined) {
|
||||||
this.settingsForm.addControl('scalingOption', new FormControl(user.preferences.scalingOption, []));
|
this.router.navigateByUrl('/login');
|
||||||
this.settingsForm.addControl('pageSplitOption', new FormControl(user.preferences.pageSplitOption, []));
|
return;
|
||||||
this.settingsForm.addControl('autoCloseMenu', new FormControl(user.preferences.autoCloseMenu, []));
|
|
||||||
this.settingsForm.addControl('readerMode', new FormControl(user.preferences.readerMode, []));
|
|
||||||
this.settingsForm.addControl('bookReaderDarkMode', new FormControl(user.preferences.bookReaderDarkMode, []));
|
|
||||||
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(user.preferences.bookReaderFontFamily, []));
|
|
||||||
this.settingsForm.addControl('bookReaderFontSize', new FormControl(user.preferences.bookReaderFontSize, []));
|
|
||||||
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(user.preferences.bookReaderLineSpacing, []));
|
|
||||||
this.settingsForm.addControl('bookReaderMargin', new FormControl(user.preferences.bookReaderMargin, []));
|
|
||||||
this.settingsForm.addControl('bookReaderReadingDirection', new FormControl(user.preferences.bookReaderReadingDirection, []));
|
|
||||||
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(!!user.preferences.bookReaderTapToPaginate, []));
|
|
||||||
|
|
||||||
this.settingsForm.addControl('theme', new FormControl(user.preferences.theme, []));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.user = results.user;
|
||||||
|
this.user.preferences = results.pref;
|
||||||
|
this.isAdmin = this.accountService.hasAdminRole(results.user);
|
||||||
|
this.hasChangePasswordRole = this.accountService.hasChangePasswordRole(results.user);
|
||||||
|
|
||||||
|
if (this.fontFamilies.indexOf(this.user.preferences.bookReaderFontFamily) < 0) {
|
||||||
|
this.user.preferences.bookReaderFontFamily = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.settingsForm.addControl('readingDirection', new FormControl(this.user.preferences.readingDirection, []));
|
||||||
|
this.settingsForm.addControl('scalingOption', new FormControl(this.user.preferences.scalingOption, []));
|
||||||
|
this.settingsForm.addControl('pageSplitOption', new FormControl(this.user.preferences.pageSplitOption, []));
|
||||||
|
this.settingsForm.addControl('autoCloseMenu', new FormControl(this.user.preferences.autoCloseMenu, []));
|
||||||
|
this.settingsForm.addControl('showScreenHints', new FormControl(this.user.preferences.showScreenHints, []));
|
||||||
|
this.settingsForm.addControl('readerMode', new FormControl(this.user.preferences.readerMode, []));
|
||||||
|
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('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, []));
|
||||||
|
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, []));
|
||||||
|
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, []));
|
||||||
|
this.settingsForm.addControl('bookReaderReadingDirection', new FormControl(this.user.preferences.bookReaderReadingDirection, []));
|
||||||
|
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(!!this.user.preferences.bookReaderTapToPaginate, []));
|
||||||
|
|
||||||
|
this.settingsForm.addControl('theme', new FormControl(this.user.preferences.theme, []));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.passwordChangeForm.addControl('password', new FormControl('', [Validators.required]));
|
this.passwordChangeForm.addControl('password', new FormControl('', [Validators.required]));
|
||||||
@ -131,7 +146,9 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
this.settingsForm.get('readingDirection')?.setValue(this.user.preferences.readingDirection);
|
this.settingsForm.get('readingDirection')?.setValue(this.user.preferences.readingDirection);
|
||||||
this.settingsForm.get('scalingOption')?.setValue(this.user.preferences.scalingOption);
|
this.settingsForm.get('scalingOption')?.setValue(this.user.preferences.scalingOption);
|
||||||
this.settingsForm.get('autoCloseMenu')?.setValue(this.user.preferences.autoCloseMenu);
|
this.settingsForm.get('autoCloseMenu')?.setValue(this.user.preferences.autoCloseMenu);
|
||||||
|
this.settingsForm.get('showScreenHints')?.setValue(this.user.preferences.showScreenHints);
|
||||||
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('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('bookReaderDarkMode')?.setValue(this.user.preferences.bookReaderDarkMode);
|
||||||
this.settingsForm.get('bookReaderFontFamily')?.setValue(this.user.preferences.bookReaderFontFamily);
|
this.settingsForm.get('bookReaderFontFamily')?.setValue(this.user.preferences.bookReaderFontFamily);
|
||||||
@ -157,7 +174,10 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
scalingOption: parseInt(modelSettings.scalingOption, 10),
|
scalingOption: parseInt(modelSettings.scalingOption, 10),
|
||||||
pageSplitOption: parseInt(modelSettings.pageSplitOption, 10),
|
pageSplitOption: parseInt(modelSettings.pageSplitOption, 10),
|
||||||
autoCloseMenu: modelSettings.autoCloseMenu,
|
autoCloseMenu: modelSettings.autoCloseMenu,
|
||||||
readerMode: parseInt(modelSettings.readerMode),
|
readerMode: parseInt(modelSettings.readerMode, 10),
|
||||||
|
layoutMode: parseInt(modelSettings.layoutMode, 10),
|
||||||
|
showScreenHints: modelSettings.showScreenHints,
|
||||||
|
backgroundColor: this.user.preferences.backgroundColor,
|
||||||
bookReaderDarkMode: modelSettings.bookReaderDarkMode,
|
bookReaderDarkMode: modelSettings.bookReaderDarkMode,
|
||||||
bookReaderFontFamily: modelSettings.bookReaderFontFamily,
|
bookReaderFontFamily: modelSettings.bookReaderFontFamily,
|
||||||
bookReaderLineSpacing: modelSettings.bookReaderLineSpacing,
|
bookReaderLineSpacing: modelSettings.bookReaderLineSpacing,
|
||||||
|
@ -10,7 +10,8 @@ import { ApiKeyComponent } from './api-key/api-key.component';
|
|||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { ThemeManagerComponent } from './theme-manager/theme-manager.component';
|
import { ThemeManagerComponent } from './theme-manager/theme-manager.component';
|
||||||
import { SiteThemeProviderPipe } from './_pipes/site-theme-provider.pipe';
|
import { SiteThemeProviderPipe } from './_pipes/site-theme-provider.pipe';
|
||||||
|
import { PipeModule } from '../pipe/pipe.module';
|
||||||
|
import { ColorPickerModule } from 'ngx-color-picker';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -30,7 +31,9 @@ import { SiteThemeProviderPipe } from './_pipes/site-theme-provider.pipe';
|
|||||||
NgbTooltipModule,
|
NgbTooltipModule,
|
||||||
NgxSliderModule,
|
NgxSliderModule,
|
||||||
UserSettingsRoutingModule,
|
UserSettingsRoutingModule,
|
||||||
SharedModule // SentenceCase pipe
|
//SharedModule, // SentenceCase pipe
|
||||||
|
PipeModule,
|
||||||
|
ColorPickerModule, // User prefernces background color
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
SiteThemeProviderPipe
|
SiteThemeProviderPipe
|
||||||
|
Loading…
x
Reference in New Issue
Block a user