mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -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")]
|
||||
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.DTOs;
|
||||
using API.Extensions;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -14,10 +15,12 @@ namespace API.Controllers
|
||||
public class UsersController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public UsersController(IUnitOfWork unitOfWork)
|
||||
public UsersController(IUnitOfWork unitOfWork, IMapper mapper)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
@ -71,7 +74,10 @@ namespace API.Controllers
|
||||
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
|
||||
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
|
||||
existingPreferences.AutoCloseMenu = preferencesDto.AutoCloseMenu;
|
||||
existingPreferences.ShowScreenHints = preferencesDto.ShowScreenHints;
|
||||
existingPreferences.ReaderMode = preferencesDto.ReaderMode;
|
||||
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
||||
existingPreferences.BackgroundColor = string.IsNullOrEmpty(preferencesDto.BackgroundColor) ? "#000000" : preferencesDto.BackgroundColor;
|
||||
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
|
||||
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
|
||||
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
|
||||
@ -90,5 +96,13 @@ namespace API.Controllers
|
||||
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Manga Reader Option: What direction should the next/prev page buttons go
|
||||
/// </summary>
|
||||
public ReadingDirection ReadingDirection { get; set; }
|
||||
/// <summary>
|
||||
/// Manga Reader Option: How should the image be scaled to screen
|
||||
/// </summary>
|
||||
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; }
|
||||
/// <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; }
|
||||
/// <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; }
|
||||
/// <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;
|
||||
/// <summary>
|
||||
/// Book Reader Option: Override extra Margin
|
||||
/// </summary>
|
||||
public int BookReaderMargin { get; set; }
|
||||
/// <summary>
|
||||
/// Book Reader Option: Override line-height
|
||||
/// </summary>
|
||||
public int BookReaderLineSpacing { get; set; }
|
||||
/// <summary>
|
||||
/// Book Reader Option: Override font size
|
||||
/// </summary>
|
||||
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; }
|
||||
/// <summary>
|
||||
/// Book Reader Option: Allows tapping on side of screens to paginate
|
||||
/// </summary>
|
||||
public bool BookReaderTapToPaginate { get; set; }
|
||||
/// <summary>
|
||||
/// Book Reader Option: What direction should the next/prev page buttons go
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
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")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BackgroundColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("BookReaderDarkMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@ -186,6 +189,9 @@ namespace API.Data.Migrations
|
||||
b.Property<bool>("BookReaderTapToPaginate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LayoutMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PageSplitOption")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@ -198,6 +204,9 @@ namespace API.Data.Migrations
|
||||
b.Property<int>("ScalingOption")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ShowScreenHints")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ThemeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -25,12 +25,23 @@ namespace API.Entities
|
||||
/// </example>
|
||||
/// </summary>
|
||||
public ReaderMode ReaderMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
||||
/// </summary>
|
||||
public bool AutoCloseMenu { get; set; } = true;
|
||||
/// <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
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
[Description("Left and Right")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
MANGA_LR = 0,
|
||||
LeftRight = 0,
|
||||
[Description("Up and Down")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
MANGA_UP = 1,
|
||||
UpDown = 1,
|
||||
[Description("Webtoon")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
WEBTOON = 2
|
||||
Webtoon = 2
|
||||
}
|
||||
}
|
||||
|
@ -19,12 +19,13 @@ namespace API.Extensions
|
||||
/// <returns>Sorted Enumerable</returns>
|
||||
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))
|
||||
.Select(digitChunk => (int?)digitChunk.Value.Length))
|
||||
.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);
|
||||
bool ArchiveNeedsFlattening(ZipArchive archive);
|
||||
Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder);
|
||||
string FindCoverImageFilename(string archivePath, IList<string> entryNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -127,8 +126,8 @@ namespace API.Services
|
||||
public static string FindFolderEntry(IEnumerable<string> entryFullNames)
|
||||
{
|
||||
var result = entryFullNames
|
||||
.OrderByNatural(Path.GetFileNameWithoutExtension)
|
||||
.Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)))
|
||||
.OrderByNatural(Path.GetFileNameWithoutExtension)
|
||||
.FirstOrDefault(Parser.Parser.IsCoverImage);
|
||||
|
||||
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
|
||||
// because NaturalSortComparer does not work with paths and doesn't seem 001.jpg as before chapter 1/001.jpg.
|
||||
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))
|
||||
.OrderByNatural(c => c.GetFullPathWithoutExtension())
|
||||
.ToList();
|
||||
if (fullNames.Count == 0) return null;
|
||||
|
||||
@ -201,9 +200,8 @@ namespace API.Services
|
||||
case ArchiveLibrary.Default:
|
||||
{
|
||||
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);
|
||||
|
||||
using var stream = entry.Open();
|
||||
@ -242,7 +240,7 @@ namespace API.Services
|
||||
/// <param name="archivePath"></param>
|
||||
/// <param name="entryNames"></param>
|
||||
/// <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));
|
||||
return entryName;
|
||||
|
@ -79,6 +79,12 @@ public class SeriesService : ISeriesService
|
||||
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>();
|
||||
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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-13.0.0.tgz",
|
||||
|
@ -38,6 +38,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"lazysizes": "^5.3.2",
|
||||
"ng-circle-progress": "^1.6.0",
|
||||
"ngx-color-picker": "^12.0.0",
|
||||
"ngx-file-drop": "^13.0.0",
|
||||
"ngx-toastr": "^14.2.1",
|
||||
"rxjs": "~7.5.4",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { MemberService } from '../_services/member.service';
|
||||
|
||||
@Injectable({
|
||||
@ -12,6 +12,7 @@ export class LibraryAccessGuard implements CanActivate {
|
||||
|
||||
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||
const libraryId = parseInt(state.url.split('library/')[1], 10);
|
||||
if (isNaN(libraryId)) return of(false);
|
||||
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 { READER_MODE } from './reader-mode';
|
||||
import { ReaderMode } from './reader-mode';
|
||||
import { ReadingDirection } from './reading-direction';
|
||||
import { ScalingOption } from './scaling-option';
|
||||
import { SiteTheme } from './site-theme';
|
||||
@ -10,8 +11,11 @@ export interface Preferences {
|
||||
readingDirection: ReadingDirection;
|
||||
scalingOption: ScalingOption;
|
||||
pageSplitOption: PageSplitOption;
|
||||
readerMode: READER_MODE;
|
||||
readerMode: ReaderMode;
|
||||
autoCloseMenu: boolean;
|
||||
layoutMode: LayoutMode;
|
||||
backgroundColor: string;
|
||||
showScreenHints: boolean;
|
||||
|
||||
// Book Reader
|
||||
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 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 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_LR = 0,
|
||||
LeftRight = 0,
|
||||
/**
|
||||
* Manga up and down to page
|
||||
*/
|
||||
MANGA_UD = 1,
|
||||
UpDown = 1,
|
||||
/**
|
||||
* 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 {
|
||||
LeftToRight = 0,
|
||||
RightToLeft = 1
|
||||
|
@ -1,6 +1,21 @@
|
||||
/**
|
||||
* How the image should scale to the screen size
|
||||
*/
|
||||
export enum ScalingOption {
|
||||
/**
|
||||
* Fit the image into the height of screen
|
||||
*/
|
||||
FitToHeight = 0,
|
||||
/**
|
||||
* Fit the image into the width of screen
|
||||
*/
|
||||
FitToWidth = 1,
|
||||
/**
|
||||
* Apply no logic and render the image as is
|
||||
*/
|
||||
Original = 2,
|
||||
/**
|
||||
* Ask the reader to attempt to choose the best ScalingOption for the user
|
||||
*/
|
||||
Automatic = 3
|
||||
}
|
||||
|
@ -162,6 +162,20 @@ export class AccountService implements OnDestroy {
|
||||
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) {
|
||||
return this.httpClient.post<Preferences>(this.baseUrl + 'users/update-preferences', userPreferences).pipe(map(settings => {
|
||||
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
|
||||
*/
|
||||
setOverrideStyles() {
|
||||
setOverrideStyles(backgroundColor: string = 'black') {
|
||||
const bodyNode = document.querySelector('body');
|
||||
if (bodyNode !== undefined && bodyNode !== null) {
|
||||
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
|
||||
|
||||
const routes: Routes = [
|
||||
{path: '', component: UserLoginComponent},
|
||||
{
|
||||
path: 'admin',
|
||||
canActivate: [AdminGuard],
|
||||
@ -69,8 +70,7 @@ const routes: Routes = [
|
||||
]
|
||||
},
|
||||
{path: 'theme', component: ThemeTestComponent},
|
||||
|
||||
{path: '', component: UserLoginComponent},
|
||||
|
||||
{path: 'login', component: UserLoginComponent}, // TODO: move this to registration module
|
||||
{path: '**', component: UserLoginComponent, pathMatch: 'full'}
|
||||
];
|
||||
|
@ -8,7 +8,6 @@ import { NavService } from './_services/nav.service';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { ThemeService } from './theme.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@ -59,7 +58,7 @@ export class AppComponent implements OnInit {
|
||||
if (user) {
|
||||
this.messageHub.createHubConnection(user, this.accountService.hasAdminRole(user));
|
||||
this.libraryService.getLibraryNames().pipe(take(1)).subscribe(() => {/* No Operation */});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDocHeight() {
|
||||
|
@ -35,6 +35,7 @@ import { RegistrationModule } from './registration/registration.module';
|
||||
import { GroupedTypeaheadComponent } from './grouped-typeahead/grouped-typeahead.component';
|
||||
import { ThemeTestComponent } from './theme-test/theme-test.component';
|
||||
import { PipeModule } from './pipe/pipe.module';
|
||||
import { ColorPickerModule } from 'ngx-color-picker';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@ -79,6 +80,8 @@ import { PipeModule } from './pipe/pipe.module';
|
||||
ReadingListModule,
|
||||
RegistrationModule,
|
||||
|
||||
ColorPickerModule, // User preferences
|
||||
|
||||
NgbAccordionModule, // ThemeTest Component only
|
||||
PipeModule,
|
||||
|
||||
|
@ -7,7 +7,6 @@ import { PageBookmark } from 'src/app/_models/page-bookmark';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bookmarks-modal',
|
||||
@ -28,7 +27,7 @@ export class BookmarksModalComponent implements OnInit {
|
||||
|
||||
constructor(public imageService: ImageService, private readerService: ReaderService,
|
||||
public modal: NgbActiveModal, private downloadService: DownloadService,
|
||||
private toastr: ToastrService, private seriesService: SeriesService) { }
|
||||
private toastr: ToastrService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.init();
|
||||
|
@ -112,7 +112,7 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<label for="language" class="form-label">Language</label>
|
||||
<app-typeahead (selectedData)="updateLanguage($event)" [settings]="languageSettings"
|
||||
@ -127,7 +127,7 @@
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 pe-2">
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="age-rating" class="form-label">Age Rating</label>
|
||||
<div class="input-group {{metadata.ageRatingLocked ? 'lock-active' : ''}}">
|
||||
@ -138,7 +138,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<div class="mb-3">
|
||||
<label for="publication-status" class="form-label">Publication Status</label>
|
||||
<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.metadata.ageRating = parseInt(val + '', 10);
|
||||
if (!this.editSeriesForm.get('ageRating')?.touched) return;
|
||||
this.metadata.ageRatingLocked = true;
|
||||
});
|
||||
|
||||
this.editSeriesForm.get('publicationStatus')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
this.metadata.publicationStatus = parseInt(val + '', 10);
|
||||
if (!this.editSeriesForm.get('publicationStatus')?.touched) return;
|
||||
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% {
|
||||
filter: opacity(0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,13 @@
|
||||
{{subtitle}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -23,37 +29,49 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div (click)="toggleMenu()" class="reading-area">
|
||||
<!-- TODO: Change this logic to only render for split pages and use image for all else-->
|
||||
<canvas style="display: none;" #content class="{{getFittingOptionClass()}} {{readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}"
|
||||
ondragstart="return false;" onselectstart="return false;">
|
||||
</canvas>
|
||||
<div (click)="toggleMenu()" class="reading-area" [ngStyle]="{'background-color': backgroundColor}">
|
||||
<ng-container *ngIf="readerMode !== ReaderMode.Webtoon">
|
||||
<div [ngClass]="{'d-none': !renderWithCanvas }">
|
||||
<canvas #content class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}}"
|
||||
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' : ''}}">
|
||||
</div>
|
||||
<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')">
|
||||
|
||||
<ng-container *ngIf="readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown">
|
||||
<div class="pagination-area {{readerMode === ReaderMode.LeftRight ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')">
|
||||
<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>
|
||||
</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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -82,7 +100,7 @@
|
||||
</div>
|
||||
<div class="row pt-4 ms-2 me-2">
|
||||
<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>
|
||||
<span id="reading-direction" class="visually-hidden">{{readingDirection === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}</span>
|
||||
</button>
|
||||
@ -142,11 +160,29 @@
|
||||
<label for="autoCloseMenu" class="form-check-label">Auto Close Menu</label>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-check">
|
||||
<input id="autoCloseMenu" type="checkbox" aria-label="Admin" class="form-check-input" formControlName="autoCloseMenu">
|
||||
<div class="mb-3">
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -14,10 +14,6 @@ $pointer-offset: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
// .btn-icon {
|
||||
// color: white;
|
||||
// }
|
||||
|
||||
canvas {
|
||||
position: absolute;
|
||||
}
|
||||
@ -32,9 +28,6 @@ canvas {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
left: 48%;
|
||||
@ -62,14 +55,21 @@ canvas {
|
||||
}
|
||||
|
||||
// Fitting Options
|
||||
// .full-height {
|
||||
// position: absolute;
|
||||
// margin: auto;
|
||||
// top: 0px;
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
// bottom: 0px;
|
||||
// height: 100%;
|
||||
// }
|
||||
|
||||
.full-height {
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
top: 0px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0px;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
margin: 0 auto;
|
||||
height: 100vh;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.original {
|
||||
@ -84,8 +84,29 @@ canvas {
|
||||
|
||||
.full-width {
|
||||
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 {
|
||||
position: fixed;
|
||||
@ -255,4 +276,4 @@ canvas {
|
||||
50% {
|
||||
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 { BehaviorSubject, forkJoin, ReplaySubject, Subject } from 'rxjs';
|
||||
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 { MemberService } from '../_services/member.service';
|
||||
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 { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums';
|
||||
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 { LibraryService } from '../_services/library.service';
|
||||
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_DOESNT_EXIST = -1;
|
||||
@ -101,7 +104,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD;
|
||||
isFullscreen: boolean = false;
|
||||
autoCloseMenu: boolean = true;
|
||||
readerMode: READER_MODE = READER_MODE.MANGA_LR;
|
||||
readerMode: ReaderMode = ReaderMode.LeftRight;
|
||||
|
||||
pageSplitOptions = pageSplitOptions;
|
||||
|
||||
@ -110,7 +113,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChild('reader') reader!: ElementRef;
|
||||
@ViewChild('content') canvas: ElementRef | undefined;
|
||||
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.
|
||||
@ -140,6 +151,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
* If the menu is open/visible.
|
||||
*/
|
||||
menuOpen = false;
|
||||
/**
|
||||
* Image Viewer collapsed
|
||||
*/
|
||||
imageViewerCollapsed = true;
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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>();
|
||||
|
||||
@ -233,7 +257,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
getPageUrl = (pageNum: number) => this.readerService.getPageUrl(this.chapterId, pageNum);
|
||||
|
||||
|
||||
|
||||
get pageBookmarked() {
|
||||
return this.bookmarks.hasOwnProperty(this.pageNum);
|
||||
}
|
||||
@ -250,33 +273,46 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
get readerModeIcon() {
|
||||
switch(this.readerMode) {
|
||||
case READER_MODE.MANGA_LR:
|
||||
case ReaderMode.LeftRight:
|
||||
return 'fa-exchange-alt';
|
||||
case READER_MODE.MANGA_UD:
|
||||
case ReaderMode.UpDown:
|
||||
return 'fa-exchange-alt fa-rotate-90';
|
||||
case READER_MODE.WEBTOON:
|
||||
case ReaderMode.Webtoon:
|
||||
return 'fa-arrows-alt-v';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
get READER_MODE(): typeof READER_MODE {
|
||||
return READER_MODE;
|
||||
get ReaderMode() {
|
||||
return ReaderMode;
|
||||
}
|
||||
get LayoutMode() {
|
||||
return LayoutMode;
|
||||
}
|
||||
|
||||
get ReadingDirection(): typeof ReadingDirection {
|
||||
get ReadingDirection() {
|
||||
return ReadingDirection;
|
||||
}
|
||||
|
||||
get PageSplitOption(): typeof PageSplitOption {
|
||||
get PageSplitOption() {
|
||||
return PageSplitOption;
|
||||
}
|
||||
|
||||
get Breakpoint() {
|
||||
return Breakpoint;
|
||||
}
|
||||
|
||||
get FITTING_OPTION() {
|
||||
return FITTING_OPTION;
|
||||
}
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
||||
public readerService: ReaderService, private location: Location,
|
||||
private formBuilder: FormBuilder, private navService: NavService,
|
||||
private toastr: ToastrService, private memberService: MemberService,
|
||||
private libraryService: LibraryService, private utilityService: UtilityService,
|
||||
private renderer: Renderer2, @Inject(DOCUMENT) private document: Document) {
|
||||
private libraryService: LibraryService, public utilityService: UtilityService,
|
||||
private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, private modalService: NgbModal) {
|
||||
this.navService.hideNavBar();
|
||||
}
|
||||
|
||||
@ -304,8 +340,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.continuousChaptersStack.push(this.chapterId);
|
||||
|
||||
this.readerService.setOverrideStyles();
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.user = user;
|
||||
@ -314,16 +348,28 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.pageSplitOption = this.user.preferences.pageSplitOption;
|
||||
this.autoCloseMenu = this.user.preferences.autoCloseMenu;
|
||||
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({
|
||||
autoCloseMenu: this.autoCloseMenu,
|
||||
pageSplitOption: this.pageSplitOption,
|
||||
fittingOption: this.translateScalingOption(this.scalingOption)
|
||||
fittingOption: this.translateScalingOption(this.scalingOption),
|
||||
layoutMode: this.layoutMode
|
||||
});
|
||||
|
||||
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.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
|
||||
@ -370,17 +416,16 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
@HostListener('window:keyup', ['$event'])
|
||||
handleKeyPress(event: KeyboardEvent) {
|
||||
|
||||
switch (this.readerMode) {
|
||||
case READER_MODE.MANGA_LR:
|
||||
case ReaderMode.LeftRight:
|
||||
if (event.key === KEY_CODES.RIGHT_ARROW) {
|
||||
this.readingDirection === ReadingDirection.LeftToRight ? this.nextPage() : this.prevPage();
|
||||
} else if (event.key === KEY_CODES.LEFT_ARROW) {
|
||||
this.readingDirection === ReadingDirection.LeftToRight ? this.prevPage() : this.nextPage();
|
||||
}
|
||||
break;
|
||||
case READER_MODE.MANGA_UD:
|
||||
case READER_MODE.WEBTOON:
|
||||
case ReaderMode.UpDown:
|
||||
case ReaderMode.Webtoon:
|
||||
if (event.key === KEY_CODES.DOWN_ARROW) {
|
||||
this.nextPage()
|
||||
} else if (event.key === KEY_CODES.UP_ARROW) {
|
||||
@ -514,7 +559,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.readerMode === READER_MODE.WEBTOON) {
|
||||
if (this.readerMode === ReaderMode.Webtoon) {
|
||||
this.isLoading = false;
|
||||
} else {
|
||||
this.loadPage();
|
||||
@ -535,6 +580,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.title += ' - ' + chapterInfo.chapterTitle;
|
||||
}
|
||||
|
||||
// TODO: Move this to the backend
|
||||
this.subtitle = '';
|
||||
if (chapterInfo.isSpecial && chapterInfo.volumeNumber === '0') {
|
||||
this.subtitle = chapterInfo.fileName;
|
||||
@ -581,10 +627,21 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
getFittingOptionClass() {
|
||||
const formControl = this.generalSettingsForm.get('fittingOption');
|
||||
let val = FITTING_OPTION.HEIGHT;
|
||||
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() {
|
||||
@ -703,7 +760,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
handlePageChange(event: any, direction: string) {
|
||||
if (this.readerMode === READER_MODE.WEBTOON) {
|
||||
if (this.readerMode === ReaderMode.Webtoon) {
|
||||
if (direction === 'right') {
|
||||
this.nextPage(event);
|
||||
} else {
|
||||
@ -738,12 +795,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.pagingDirection = PAGING_DIRECTION.FORWARD;
|
||||
if (this.isNoSplit() || notInSplit) {
|
||||
this.setPageNum(this.pageNum + 1);
|
||||
if (this.readerMode !== READER_MODE.WEBTOON) {
|
||||
if (this.readerMode !== ReaderMode.Webtoon) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -769,9 +829,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.isNoSplit() || notInSplit) {
|
||||
this.setPageNum(this.pageNum - 1);
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -877,69 +940,19 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const needsSplitting = this.isCoverImage();
|
||||
this.updateSplitPage();
|
||||
|
||||
|
||||
if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.LEFT_PART) {
|
||||
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.renderWithCanvas = true;
|
||||
console.log('[Render] Canvas')
|
||||
} else if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.RIGHT_PART) {
|
||||
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.renderWithCanvas = true;
|
||||
console.log('[Render] Canvas')
|
||||
} else {
|
||||
if (!this.firstPageRendered && this.scalingOption === ScalingOption.Automatic) {
|
||||
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);
|
||||
|
||||
this.renderWithCanvas = false;
|
||||
}
|
||||
|
||||
// Reset scroll on non HEIGHT Fits
|
||||
@ -984,7 +997,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
shouldRenderAsFitSplit() {
|
||||
// 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 (!this.isCoverImage() || parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10) !== PageSplitOption.FitSplit) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1011,9 +1023,16 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.isLoading = true;
|
||||
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) {
|
||||
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();
|
||||
|
||||
console.log('[loadPage] (after setting) canvasImage: ', this.canvasImage);
|
||||
console.log('[loadPage] (after setting) canvasImage2: ', this.canvasImage2);
|
||||
} else {
|
||||
this.renderPage();
|
||||
}
|
||||
@ -1027,7 +1046,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.readingDirection = ReadingDirection.LeftToRight;
|
||||
}
|
||||
|
||||
if (this.menuOpen) {
|
||||
if (this.menuOpen && this.user.preferences.showScreenHints) {
|
||||
this.showClickOverlay = true;
|
||||
setTimeout(() => {
|
||||
this.showClickOverlay = false;
|
||||
@ -1039,7 +1058,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
sliderDragUpdate(context: ChangeContext) {
|
||||
// This will update the value for value except when in webtoon due to how the webtoon reader
|
||||
// responds to page changes
|
||||
if (this.readerMode !== READER_MODE.WEBTOON) {
|
||||
if (this.readerMode !== ReaderMode.Webtoon) {
|
||||
this.setPageNum(context.value);
|
||||
}
|
||||
}
|
||||
@ -1142,15 +1161,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
toggleReaderMode() {
|
||||
switch(this.readerMode) {
|
||||
case READER_MODE.MANGA_LR:
|
||||
this.readerMode = READER_MODE.MANGA_UD;
|
||||
case ReaderMode.LeftRight:
|
||||
this.readerMode = ReaderMode.UpDown;
|
||||
this.pagingDirection = PAGING_DIRECTION.FORWARD;
|
||||
break;
|
||||
case READER_MODE.MANGA_UD:
|
||||
this.readerMode = READER_MODE.WEBTOON;
|
||||
case ReaderMode.UpDown:
|
||||
this.readerMode = ReaderMode.Webtoon;
|
||||
break;
|
||||
case READER_MODE.WEBTOON:
|
||||
this.readerMode = READER_MODE.MANGA_LR;
|
||||
case ReaderMode.Webtoon:
|
||||
this.readerMode = ReaderMode.LeftRight;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1160,7 +1179,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
updateForm() {
|
||||
if ( this.readerMode === READER_MODE.WEBTOON) {
|
||||
if ( this.readerMode === ReaderMode.Webtoon) {
|
||||
this.generalSettingsForm.get('fittingOption')?.disable()
|
||||
this.generalSettingsForm.get('pageSplitOption')?.disable();
|
||||
} else {
|
||||
@ -1178,23 +1197,45 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
*/
|
||||
bookmarkPage() {
|
||||
const pageNum = this.pageNum;
|
||||
|
||||
// TODO: Handle LayoutMode.Double
|
||||
|
||||
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];
|
||||
});
|
||||
} 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;
|
||||
});
|
||||
}
|
||||
|
||||
// Show an effect on the image to show that it was bookmarked
|
||||
this.showBookmarkEffectEvent.next(pageNum);
|
||||
if (this.readerMode != READER_MODE.WEBTOON) {
|
||||
if (this.canvas) {
|
||||
this.renderer.addClass(this.canvas?.nativeElement, 'bookmark-effect');
|
||||
if (this.readerMode != ReaderMode.Webtoon) {
|
||||
|
||||
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(() => {
|
||||
this.renderer.removeClass(this.canvas?.nativeElement, 'bookmark-effect');
|
||||
elements.forEach(elem => this.renderer.removeClass(elem, 'bookmark-effect'));
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
@ -1220,4 +1261,18 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|| document.body.clientHeight;
|
||||
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 { NgxSliderModule } from '@angular-slider/ngx-slider';
|
||||
import { InfiniteScrollerComponent } from './infinite-scroller/infinite-scroller.component';
|
||||
import { ReaderSharedModule } from '../reader-shared/reader-shared.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -22,6 +23,7 @@ import { InfiniteScrollerComponent } from './infinite-scroller/infinite-scroller
|
||||
NgbDropdownModule,
|
||||
NgxSliderModule,
|
||||
SharedModule,
|
||||
ReaderSharedModule,
|
||||
],
|
||||
exports: [
|
||||
MangaReaderComponent
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FilterPipe } from './filter.pipe';
|
||||
import { PersonRolePipe } from './person-role.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: [
|
||||
FilterPipe,
|
||||
PersonRolePipe,
|
||||
PublicationStatusPipe
|
||||
PublicationStatusPipe,
|
||||
SentenceCasePipe
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -18,7 +20,8 @@ import { PublicationStatusPipe } from './publication-status.pipe';
|
||||
exports: [
|
||||
FilterPipe,
|
||||
PersonRolePipe,
|
||||
PublicationStatusPipe
|
||||
PublicationStatusPipe,
|
||||
SentenceCasePipe
|
||||
]
|
||||
})
|
||||
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() {
|
||||
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.closed.subscribe((closeResult: {success: boolean, series: Series, coverImageUpdate: boolean}) => {
|
||||
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 { CircularLoaderComponent } from './circular-loader/circular-loader.component';
|
||||
import { NgCircleProgressModule } from 'ng-circle-progress';
|
||||
import { SentenceCasePipe } from './sentence-case.pipe';
|
||||
import { PersonBadgeComponent } from './person-badge/person-badge.component';
|
||||
import { BadgeExpanderComponent } from './badge-expander/badge-expander.component';
|
||||
import { ImageComponent } from './image/image.component';
|
||||
@ -31,7 +30,6 @@ import { ImageComponent } from './image/image.component';
|
||||
SeriesFormatComponent,
|
||||
UpdateNotificationModalComponent,
|
||||
CircularLoaderComponent,
|
||||
SentenceCasePipe,
|
||||
PersonBadgeComponent,
|
||||
BadgeExpanderComponent,
|
||||
ImageComponent
|
||||
@ -46,7 +44,6 @@ import { ImageComponent } from './image/image.component';
|
||||
],
|
||||
exports: [
|
||||
SafeHtmlPipe, // Used globally
|
||||
SentenceCasePipe, // Used globablly
|
||||
ReadMoreComponent, // Used globably
|
||||
DrawerComponent, // Can be replaced with boostrap offscreen canvas (v5)
|
||||
ShowIfScrollbarDirective, // Used book reader only?
|
||||
|
@ -5,7 +5,7 @@
|
||||
<span class="visually-hidden">Field is locked</span>
|
||||
</span>
|
||||
</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">
|
||||
<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>
|
||||
|
@ -94,14 +94,49 @@
|
||||
</select>
|
||||
</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">
|
||||
<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 class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3">
|
||||
<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 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>
|
||||
|
@ -5,12 +5,13 @@ import { take } from 'rxjs/operators';
|
||||
import { Options } from '@angular-slider/ngx-slider';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
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 { AccountService } from 'src/app/_services/account.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 { forkJoin } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-preferences',
|
||||
@ -23,6 +24,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
scalingOptions = scalingOptions;
|
||||
pageSplitOptions = pageSplitOptions;
|
||||
readingModes = readingModes;
|
||||
layoutModes = layoutModes;
|
||||
|
||||
settingsForm: FormGroup = new FormGroup({});
|
||||
passwordChangeForm: FormGroup = new FormGroup({});
|
||||
@ -63,8 +65,11 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
opdsEnabled: boolean = false;
|
||||
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,
|
||||
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.route.fragment.subscribe(frag => {
|
||||
@ -83,31 +88,41 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
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) {
|
||||
this.user.preferences.bookReaderFontFamily = 'default';
|
||||
}
|
||||
|
||||
this.settingsForm.addControl('readingDirection', new FormControl(user.preferences.readingDirection, []));
|
||||
this.settingsForm.addControl('scalingOption', new FormControl(user.preferences.scalingOption, []));
|
||||
this.settingsForm.addControl('pageSplitOption', new FormControl(user.preferences.pageSplitOption, []));
|
||||
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, []));
|
||||
forkJoin({
|
||||
user: this.accountService.currentUser$.pipe(take(1)),
|
||||
pref: this.accountService.getPreferences()
|
||||
}).subscribe(results => {
|
||||
if (results.user === undefined) {
|
||||
this.router.navigateByUrl('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
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]));
|
||||
@ -131,7 +146,9 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
this.settingsForm.get('readingDirection')?.setValue(this.user.preferences.readingDirection);
|
||||
this.settingsForm.get('scalingOption')?.setValue(this.user.preferences.scalingOption);
|
||||
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('layoutMode')?.setValue(this.user.preferences.layoutMode);
|
||||
this.settingsForm.get('pageSplitOption')?.setValue(this.user.preferences.pageSplitOption);
|
||||
this.settingsForm.get('bookReaderDarkMode')?.setValue(this.user.preferences.bookReaderDarkMode);
|
||||
this.settingsForm.get('bookReaderFontFamily')?.setValue(this.user.preferences.bookReaderFontFamily);
|
||||
@ -157,7 +174,10 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
scalingOption: parseInt(modelSettings.scalingOption, 10),
|
||||
pageSplitOption: parseInt(modelSettings.pageSplitOption, 10),
|
||||
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,
|
||||
bookReaderFontFamily: modelSettings.bookReaderFontFamily,
|
||||
bookReaderLineSpacing: modelSettings.bookReaderLineSpacing,
|
||||
|
@ -10,7 +10,8 @@ import { ApiKeyComponent } from './api-key/api-key.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { ThemeManagerComponent } from './theme-manager/theme-manager.component';
|
||||
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,
|
||||
NgxSliderModule,
|
||||
UserSettingsRoutingModule,
|
||||
SharedModule // SentenceCase pipe
|
||||
//SharedModule, // SentenceCase pipe
|
||||
PipeModule,
|
||||
ColorPickerModule, // User prefernces background color
|
||||
],
|
||||
exports: [
|
||||
SiteThemeProviderPipe
|
||||
|
Loading…
x
Reference in New Issue
Block a user