mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Change Detection: On Push aka UI Smoothness (#1369)
* Updated Series Info Cards to use OnPush and hooked in progress events when we do a mark as read/unread on entities. These events update progress bars but also will now trigger a re-calculation on Read Time Left. * Removed Library Card Component * Refactored manga reader title and subtitle calculation to the backend. * Coverted card actionables to onPush * Series Card on push cleanup * Updated edit collection tags for on push * Update cover image chooser for on push * Cleaned up carsouel reel * Updated cover image to allow for uploading gif and webp files * Bulk add to collection on push * Updated bulk operation to use on push. Updated bulk operation to have mark as unread and read buttons explicitly. Updated so add to collection is visible and delete. Fixed a bug where manage library component wasn't invoking the trackBy function * Updating entity title for on push * Removed file info component * Updated Mange Library for on push * Entity info cards on push * List item on push * Updated icon and title for on push and fixed some missing change detection on series detail * Restricted the typeahead interface to simplify the design * Edit Series Relation now shows a value in the dropdown for Parent relationships and disables the field. * Updated edit series relation to focus on new typeahead when adding a new relationship * Added some documentation and when Scanning a library, don't allow the user to enqueue the same job multiple times. * Applied the No-enqueue if already enqueued logic to other tasks * Library detail on push * Updated events widget to onpush * Card detail drawer on push. Card detail cover chooser now will show all chapter's covers for selection in cover chooser. * Chapter metadata detail on push * Removed Card Detail modal * All collections on push * Removed some comments * Updated bulk selection to use an observable rather than function calls so new on push strategy works * collection detail now uses on push and scroller is placed on correct element * Updated library recommended to on push. Ensure that when mark as read occurs, the appropriate streams are refreshed. * Updated library detail to on push * Update metadata fiter to onpush. Bugs found and reported to Project * person badge on push * Read more on push * Updated tag badge to on push * User login on push * When initing side nav, don't call an authenticated api until we are sure a user is logged in * Updated splash container to on push * Dashboard on push * Side nav slight refactor around some api calls * Cleaned up series card on push to use same cdRef naming convention * Updated Static Files to use caching * Added width and height to logo image * shortcuts modal on push * reading lists on push * Reading list detail on push * draggable ordered list on push * Refactored reading-list-detail to use a new item which drastically reduces renders on operations * series format on push * circular loader on push * Badge Expander on push * update notification modal on push * drawer on push * Edit Series Modal on push * reset password on push * review series modal on push * series metadata detail on push * theme manager on push * confirm reset password on push * register on push * confirm migration email on push * confirm email on push * add email to account migration on push * user preferences on push. Made global settings default open * edit series relation on push * Fixed an edge case bug for next chapter where if the current volume had a single chapter of 1 and the next volume had a chapter number of 0, it would say there are no more chapters. * Updated infinite scroller with on push support * Moved some animations over to typeahead, not integrated yet. * Manga reader is now on push * Reader settings on push * refactored how we close the book * Updated table of contents for on push * Updated book reader for on push. Fixed a bug where table of contents wasn't showing current page anchor due to a scroll calulation bug * Small code tweak * Icon and title on push * nav header on push * grouped typeahead on push * typeahead on push and added a new trackby identity function to allow even faster rendering of big lists * pdf reader on push * code cleanup
This commit is contained in:
parent
f5be0fac58
commit
4e49aa47ce
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
@ -16,7 +15,6 @@ using API.Tests.Helpers;
|
||||
using AutoMapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
@ -28,7 +26,6 @@ public class ReaderServiceTests
|
||||
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
private readonly DbConnection _connection;
|
||||
private readonly DataContext _context;
|
||||
|
||||
private const string CacheDirectory = "C:/kavita/config/cache/";
|
||||
@ -39,7 +36,6 @@ public class ReaderServiceTests
|
||||
public ReaderServiceTests()
|
||||
{
|
||||
var contextOptions = new DbContextOptionsBuilder().UseSqlite(CreateInMemoryDatabase()).Options;
|
||||
_connection = RelationalOptionsExtension.Extract(contextOptions).Connection;
|
||||
|
||||
_context = new DataContext(contextOptions);
|
||||
Task.Run(SeedDb).GetAwaiter().GetResult();
|
||||
@ -83,7 +79,7 @@ public class ReaderServiceTests
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
private async Task ResetDB()
|
||||
private async Task ResetDb()
|
||||
{
|
||||
_context.Series.RemoveRange(_context.Series.ToList());
|
||||
|
||||
@ -122,7 +118,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task CapPageToChapterTest()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -161,7 +157,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task SaveReadingProgress_ShouldCreateNewEntity()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -210,7 +206,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task SaveReadingProgress_ShouldUpdateExisting()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -276,7 +272,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task MarkChaptersAsReadTest()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -314,7 +310,7 @@ public class ReaderServiceTests
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumes(1);
|
||||
readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
await readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
|
||||
@ -326,7 +322,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task MarkChapterAsUnreadTest()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -364,12 +360,12 @@ public class ReaderServiceTests
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList();
|
||||
readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
await readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
|
||||
|
||||
readerService.MarkChaptersAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
await readerService.MarkChaptersAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var progresses = (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses;
|
||||
@ -385,7 +381,7 @@ public class ReaderServiceTests
|
||||
public async Task GetNextChapterIdAsync_ShouldGetNextVolume()
|
||||
{
|
||||
// V1 -> V2
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -431,7 +427,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldRollIntoNextVolume()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -478,7 +474,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldRollIntoChaptersFromVolume()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -518,10 +514,55 @@ public class ReaderServiceTests
|
||||
Assert.Equal("1", actualChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldRollIntoNextChapterWhenVolumesAreOnlyOneChapterAndNextChapterIs0()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("66", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("67", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
}
|
||||
});
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
UserName = "majora2007"
|
||||
});
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 3, 1);
|
||||
Assert.NotEqual(-1, nextChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("0", actualChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromSpecial()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -562,7 +603,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromVolume()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -598,7 +639,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_NoSpecials()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -639,7 +680,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldMoveFromVolumeToSpecial_NoLooseLeafChapters()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -682,7 +723,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldMoveFromLooseLeafChapterToSpecial()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -721,7 +762,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromSpecial_WithVolumeAndLooseLeafChapters()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -763,7 +804,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldMoveFromSpecialToSpecial()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -811,7 +852,7 @@ public class ReaderServiceTests
|
||||
public async Task GetPrevChapterIdAsync_ShouldGetPrevVolume()
|
||||
{
|
||||
// V1 -> V2
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -857,7 +898,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetPrevChapterIdAsync_ShouldRollIntoPrevVolume()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -904,7 +945,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetPrevChapterIdAsync_ShouldMoveFromSpecialToVolume()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -947,7 +988,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetPrevChapterIdAsync_ShouldFindNoPrevChapterFromVolume()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -983,7 +1024,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetPrevChapterIdAsync_ShouldFindNoPrevChapterFromVolumeWithZeroChapter()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -1018,7 +1059,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetPrevChapterIdAsync_ShouldFindNoPrevChapterFromVolumeWithZeroChapterAndHasNormalChapters()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -1058,7 +1099,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetPrevChapterIdAsync_ShouldFindNoPrevChapterFromVolumeWithZeroChapterAndHasNormalChapters2()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -1112,7 +1153,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetPrevChapterIdAsync_ShouldFindNoPrevChapterFromChapter()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -1148,7 +1189,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetPrevChapterIdAsync_ShouldMoveFromSpecialToSpecial()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -1191,7 +1232,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task GetPrevChapterIdAsync_ShouldMoveFromChapterToVolume()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -1902,7 +1943,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task MarkChaptersUntilAsRead_ShouldMarkAsReadAnythingUntil()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
@ -1979,7 +2020,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task MarkSeriesAsReadTest()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -2044,7 +2085,7 @@ public class ReaderServiceTests
|
||||
[Fact]
|
||||
public async Task MarkSeriesAsUnreadTest()
|
||||
{
|
||||
await ResetDB();
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
@ -2082,7 +2123,7 @@ public class ReaderServiceTests
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList();
|
||||
readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
await readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
|
||||
@ -2096,4 +2137,40 @@ public class ReaderServiceTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FormatChapterName
|
||||
|
||||
[Fact]
|
||||
public void FormatChapterName_Manga_Chapter()
|
||||
{
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var actual = readerService.FormatChapterName(LibraryType.Manga, false, false);
|
||||
Assert.Equal("Chapter", actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatChapterName_Book_Chapter_WithTitle()
|
||||
{
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var actual = readerService.FormatChapterName(LibraryType.Book, false, false);
|
||||
Assert.Equal("Book", actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatChapterName_Comic()
|
||||
{
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var actual = readerService.FormatChapterName(LibraryType.Comic, false, false);
|
||||
Assert.Equal("Issue", actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatChapterName_Comic_WithHash()
|
||||
{
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var actual = readerService.FormatChapterName(LibraryType.Comic, true, true);
|
||||
Assert.Equal("Issue #", actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -28,32 +28,27 @@ namespace API.Controllers
|
||||
private readonly ILogger<ReaderController> _logger;
|
||||
private readonly IReaderService _readerService;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IEventHub _eventHub;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReaderController(ICacheService cacheService,
|
||||
IUnitOfWork unitOfWork, ILogger<ReaderController> logger,
|
||||
IReaderService readerService, IBookmarkService bookmarkService,
|
||||
IEventHub eventHub)
|
||||
IReaderService readerService, IBookmarkService bookmarkService)
|
||||
{
|
||||
_cacheService = cacheService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
_readerService = readerService;
|
||||
_bookmarkService = bookmarkService;
|
||||
_eventHub = eventHub;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the PDF for the chapterId.
|
||||
/// </summary>
|
||||
/// <param name="apiKey">API Key for user to validate they have access</param>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("pdf")]
|
||||
public async Task<ActionResult> GetPdf(int chapterId)
|
||||
{
|
||||
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
if (chapter == null) return BadRequest("There was an issue finding pdf file for reading");
|
||||
|
||||
@ -152,9 +147,9 @@ namespace API.Controllers
|
||||
if (dto == null) return BadRequest("Please perform a scan on this series or library and try again");
|
||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
|
||||
return Ok(new ChapterInfoDto()
|
||||
var info = new ChapterInfoDto()
|
||||
{
|
||||
ChapterNumber = dto.ChapterNumber,
|
||||
ChapterNumber = dto.ChapterNumber,
|
||||
VolumeNumber = dto.VolumeNumber,
|
||||
VolumeId = dto.VolumeId,
|
||||
FileName = Path.GetFileName(mangaFile.FilePath),
|
||||
@ -164,8 +159,33 @@ namespace API.Controllers
|
||||
LibraryId = dto.LibraryId,
|
||||
IsSpecial = dto.IsSpecial,
|
||||
Pages = dto.Pages,
|
||||
ChapterTitle = dto.ChapterTitle ?? string.Empty
|
||||
});
|
||||
ChapterTitle = dto.ChapterTitle ?? string.Empty,
|
||||
Subtitle = string.Empty,
|
||||
Title = dto.SeriesName
|
||||
};
|
||||
|
||||
if (info.ChapterTitle is {Length: > 0}) {
|
||||
info.Title += " - " + info.ChapterTitle;
|
||||
}
|
||||
|
||||
if (info.IsSpecial && dto.VolumeNumber.Equals(Parser.Parser.DefaultVolume))
|
||||
{
|
||||
info.Subtitle = info.FileName;
|
||||
} else if (!info.IsSpecial && info.VolumeNumber.Equals(Parser.Parser.DefaultVolume))
|
||||
{
|
||||
info.Subtitle = _readerService.FormatChapterName(info.LibraryType, true, true) + info.ChapterNumber;
|
||||
}
|
||||
else
|
||||
{
|
||||
info.Subtitle = "Volume " + info.VolumeNumber;
|
||||
if (!info.ChapterNumber.Equals(Parser.Parser.DefaultChapter))
|
||||
{
|
||||
info.Subtitle += " " + _readerService.FormatChapterName(info.LibraryType, true, true) +
|
||||
info.ChapterNumber;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -235,7 +255,7 @@ namespace API.Controllers
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
|
||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||
_readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters);
|
||||
await _readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters);
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
@ -258,7 +278,7 @@ namespace API.Controllers
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
|
||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||
_readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
|
||||
await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
@ -288,7 +308,7 @@ namespace API.Controllers
|
||||
chapterIds.Add(chapterId);
|
||||
}
|
||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
||||
_readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters);
|
||||
await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters);
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
@ -317,7 +337,7 @@ namespace API.Controllers
|
||||
chapterIds.Add(chapterId);
|
||||
}
|
||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
||||
_readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters);
|
||||
await _readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters);
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
@ -343,7 +363,7 @@ namespace API.Controllers
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
_readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
||||
await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
@ -370,7 +390,7 @@ namespace API.Controllers
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
_readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters);
|
||||
await _readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
@ -73,8 +73,6 @@ namespace API.Controllers
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
|
||||
return Ok(items);
|
||||
|
||||
//return Ok(await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,23 +1,68 @@
|
||||
using System;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.Reader
|
||||
namespace API.DTOs.Reader;
|
||||
|
||||
/// <summary>
|
||||
/// Information about the Chapter for the Reader to render
|
||||
/// </summary>
|
||||
public class ChapterInfoDto : IChapterInfoDto
|
||||
{
|
||||
public class ChapterInfoDto : IChapterInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// The Chapter Number
|
||||
/// </summary>
|
||||
public string ChapterNumber { get; set; }
|
||||
/// <summary>
|
||||
/// The Volume Number
|
||||
/// </summary>
|
||||
public string VolumeNumber { get; set; }
|
||||
/// <summary>
|
||||
/// Volume entity Id
|
||||
/// </summary>
|
||||
public int VolumeId { get; set; }
|
||||
/// <summary>
|
||||
/// Series Name
|
||||
/// </summary>
|
||||
public string SeriesName { get; set; }
|
||||
/// <summary>
|
||||
/// Series Format
|
||||
/// </summary>
|
||||
public MangaFormat SeriesFormat { get; set; }
|
||||
/// <summary>
|
||||
/// Series entity Id
|
||||
/// </summary>
|
||||
public int SeriesId { get; set; }
|
||||
/// <summary>
|
||||
/// Library entity Id
|
||||
/// </summary>
|
||||
public int LibraryId { get; set; }
|
||||
/// <summary>
|
||||
/// Library type
|
||||
/// </summary>
|
||||
public LibraryType LibraryType { get; set; }
|
||||
/// <summary>
|
||||
/// Chapter's title if set via ComicInfo.xml (Title field)
|
||||
/// </summary>
|
||||
public string ChapterTitle { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Total Number of pages in this Chapter
|
||||
/// </summary>
|
||||
public int Pages { get; set; }
|
||||
/// <summary>
|
||||
/// File name of the chapter
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
/// <summary>
|
||||
/// If this is marked as a special in Kavita
|
||||
/// </summary>
|
||||
public bool IsSpecial { get; set; }
|
||||
/// <summary>
|
||||
/// The subtitle to render on the reader
|
||||
/// </summary>
|
||||
public string Subtitle { get; set; }
|
||||
/// <summary>
|
||||
/// Series Title
|
||||
/// </summary>
|
||||
/// <remarks>Usually just series name, but can include chapter title</remarks>
|
||||
public string Title { get; set; }
|
||||
|
||||
public string ChapterNumber { get; set; }
|
||||
public string VolumeNumber { get; set; }
|
||||
public int VolumeId { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public MangaFormat SeriesFormat { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
public LibraryType LibraryType { get; set; }
|
||||
public string ChapterTitle { get; set; } = string.Empty;
|
||||
public int Pages { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public bool IsSpecial { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
namespace API.Entities
|
||||
{
|
||||
//[Index(nameof(SeriesId), nameof(VolumeId), nameof(ChapterId), IsUnique = true)]
|
||||
public class ReadingListItem
|
||||
{
|
||||
public int Id { get; init; }
|
||||
@ -16,7 +15,7 @@
|
||||
public ReadingList ReadingList { get; set; }
|
||||
public int ReadingListId { get; set; }
|
||||
|
||||
// Idea, keep these for easy join statements
|
||||
// Keep these for easy join statements
|
||||
public Series Series { get; set; }
|
||||
public Volume Volume { get; set; }
|
||||
public Chapter Chapter { get; set; }
|
||||
|
@ -66,9 +66,9 @@ public class CacheHelper : ICacheHelper
|
||||
/// <summary>
|
||||
/// Has the file been modified since last scan or is user forcing an update
|
||||
/// </summary>
|
||||
/// <param name="lastScan"></param>
|
||||
/// <param name="forceUpdate"></param>
|
||||
/// <param name="firstFile"></param>
|
||||
/// <param name="lastScan">Last time the scan was performed on this file</param>
|
||||
/// <param name="forceUpdate">Should we ignore any logic and force this to return true</param>
|
||||
/// <param name="firstFile">The file in question</param>
|
||||
/// <returns></returns>
|
||||
public bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile firstFile)
|
||||
{
|
||||
@ -76,10 +76,6 @@ public class CacheHelper : ICacheHelper
|
||||
if (forceUpdate) return true;
|
||||
return _fileService.HasFileBeenModifiedSince(firstFile.FilePath, lastScan)
|
||||
|| _fileService.HasFileBeenModifiedSince(firstFile.FilePath, firstFile.LastModified);
|
||||
// return firstFile != null &&
|
||||
// (!forceUpdate &&
|
||||
// !(_fileService.HasFileBeenModifiedSince(firstFile.FilePath, lastScan)
|
||||
// || _fileService.HasFileBeenModifiedSince(firstFile.FilePath, firstFile.LastModified)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -189,6 +189,7 @@ namespace API.Services
|
||||
/// <remarks>This always creates a thumbnail</remarks>
|
||||
/// <param name="archivePath"></param>
|
||||
/// <param name="fileName">File name to use based on context of entity.</param>
|
||||
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
|
||||
/// <returns></returns>
|
||||
public string GetCoverImage(string archivePath, string fileName, string outputDirectory)
|
||||
{
|
||||
@ -261,16 +262,16 @@ namespace API.Services
|
||||
archive.Entries.Any(e => e.FullName.Contains(Path.AltDirectorySeparatorChar) && !Parser.Parser.HasBlacklistedFolderInPath(e.FullName));
|
||||
}
|
||||
|
||||
// TODO: Refactor CreateZipForDownload to return the temp file so we can stream it from temp
|
||||
/// <summary>
|
||||
///
|
||||
/// Creates a zip file form the listed files and outputs to the temp folder.
|
||||
/// </summary>
|
||||
/// <param name="files"></param>
|
||||
/// <param name="files">List of files to be zipped up. Should be full file paths.</param>
|
||||
/// <param name="tempFolder">Temp folder name to use for preparing the files. Will be created and deleted</param>
|
||||
/// <returns></returns>
|
||||
/// <returns>All bytes for the given file in a Tuple</returns>
|
||||
/// <exception cref="KavitaException"></exception>
|
||||
public async Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder)
|
||||
{
|
||||
// TODO: Refactor CreateZipForDownload to return the temp file so we can stream it from temp
|
||||
var dateString = DateTime.Now.ToShortDateString().Replace("/", "_");
|
||||
|
||||
var tempLocation = Path.Join(_directoryService.TempDirectory, $"{tempFolder}_{dateString}");
|
||||
|
@ -686,6 +686,7 @@ namespace API.Services
|
||||
/// </summary>
|
||||
/// <param name="fileFilePath"></param>
|
||||
/// <param name="fileName">Name of the new file.</param>
|
||||
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
|
||||
/// <returns></returns>
|
||||
public string GetCoverImage(string fileFilePath, string fileName, string outputDirectory)
|
||||
{
|
||||
|
@ -25,6 +25,7 @@ public interface IImageService
|
||||
/// Converts the passed image to webP and outputs it in the same directory
|
||||
/// </summary>
|
||||
/// <param name="filePath">Full path to the image to convert</param>
|
||||
/// <param name="outputPath">Where to output the file</param>
|
||||
/// <returns>File of written webp image</returns>
|
||||
Task<string> ConvertToWebP(string filePath, string outputPath);
|
||||
}
|
||||
@ -89,6 +90,7 @@ public class ImageService : IImageService
|
||||
/// </summary>
|
||||
/// <param name="stream">Stream to write to disk. Ensure this is rewinded.</param>
|
||||
/// <param name="fileName">filename to save as without extension</param>
|
||||
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
|
||||
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
|
||||
public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory)
|
||||
{
|
||||
|
@ -35,6 +35,7 @@ public interface IMetadataService
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="forceUpdate">Overrides any cache logic and forces execution</param>
|
||||
Task RefreshMetadataForSeries(int libraryId, int seriesId, bool forceUpdate = true);
|
||||
}
|
||||
|
||||
@ -278,6 +279,7 @@ public class MetadataService : IMetadataService
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="forceUpdate">Overrides any cache logic and forces execution</param>
|
||||
public async Task RefreshMetadataForSeries(int libraryId, int seriesId, bool forceUpdate = true)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
@ -9,6 +9,7 @@ using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.SignalR;
|
||||
using Kavita.Common;
|
||||
@ -20,8 +21,8 @@ public interface IReaderService
|
||||
{
|
||||
Task MarkSeriesAsRead(AppUser user, int seriesId);
|
||||
Task MarkSeriesAsUnread(AppUser user, int seriesId);
|
||||
void MarkChaptersAsRead(AppUser user, int seriesId, IEnumerable<Chapter> chapters);
|
||||
void MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable<Chapter> chapters);
|
||||
Task MarkChaptersAsRead(AppUser user, int seriesId, IEnumerable<Chapter> chapters);
|
||||
Task MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable<Chapter> chapters);
|
||||
Task<bool> SaveReadingProgress(ProgressDto progressDto, int userId);
|
||||
Task<int> CapPageToChapter(int chapterId, int page);
|
||||
Task<int> GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId);
|
||||
@ -30,6 +31,7 @@ public interface IReaderService
|
||||
Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber);
|
||||
Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber);
|
||||
HourEstimateRangeDto GetTimeEstimate(long wordCount, int pageCount, bool isEpub);
|
||||
string FormatChapterName(LibraryType libraryType, bool includeHash = false, bool includeSpace = false);
|
||||
}
|
||||
|
||||
public class ReaderService : IReaderService
|
||||
@ -71,7 +73,7 @@ public class ReaderService : IReaderService
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
MarkChaptersAsRead(user, seriesId, volume.Chapters);
|
||||
await MarkChaptersAsRead(user, seriesId, volume.Chapters);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
@ -88,7 +90,7 @@ public class ReaderService : IReaderService
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
MarkChaptersAsUnread(user, seriesId, volume.Chapters);
|
||||
await MarkChaptersAsUnread(user, seriesId, volume.Chapters);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
@ -100,7 +102,7 @@ public class ReaderService : IReaderService
|
||||
/// <param name="user"></param>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="chapters"></param>
|
||||
public void MarkChaptersAsRead(AppUser user, int seriesId, IEnumerable<Chapter> chapters)
|
||||
public async Task MarkChaptersAsRead(AppUser user, int seriesId, IEnumerable<Chapter> chapters)
|
||||
{
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
@ -115,12 +117,17 @@ public class ReaderService : IReaderService
|
||||
SeriesId = seriesId,
|
||||
ChapterId = chapter.Id
|
||||
});
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, seriesId, chapter.VolumeId, chapter.Id, chapter.Pages));
|
||||
}
|
||||
else
|
||||
{
|
||||
userProgress.PagesRead = chapter.Pages;
|
||||
userProgress.SeriesId = seriesId;
|
||||
userProgress.VolumeId = chapter.VolumeId;
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, userProgress.SeriesId, userProgress.VolumeId, userProgress.ChapterId, chapter.Pages));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -131,7 +138,7 @@ public class ReaderService : IReaderService
|
||||
/// <param name="user"></param>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="chapters"></param>
|
||||
public void MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable<Chapter> chapters)
|
||||
public async Task MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable<Chapter> chapters)
|
||||
{
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
@ -142,6 +149,9 @@ public class ReaderService : IReaderService
|
||||
userProgress.PagesRead = 0;
|
||||
userProgress.SeriesId = seriesId;
|
||||
userProgress.VolumeId = chapter.VolumeId;
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, userProgress.SeriesId, userProgress.VolumeId, userProgress.ChapterId, 0));
|
||||
}
|
||||
}
|
||||
|
||||
@ -321,6 +331,8 @@ public class ReaderService : IReaderService
|
||||
currentChapter.Range, dto => dto.Range);
|
||||
if (chapterId > 0) return chapterId;
|
||||
} else if (double.Parse(firstChapter.Number) > double.Parse(currentChapter.Number)) return firstChapter.Id;
|
||||
// If we are the last chapter and next volume is there, we should try to use it (unless it's volume 0)
|
||||
else if (double.Parse(firstChapter.Number) == 0) return firstChapter.Id;
|
||||
}
|
||||
|
||||
// If we are the last volume and we didn't find any next volume, loop back to volume 0 and give the first chapter
|
||||
@ -485,7 +497,7 @@ public class ReaderService : IReaderService
|
||||
var chapters = volume.Chapters
|
||||
.OrderBy(c => float.Parse(c.Number))
|
||||
.Where(c => !c.IsSpecial && Parser.Parser.MaxNumberFromRange(c.Range) <= chapterNumber);
|
||||
MarkChaptersAsRead(user, volume.SeriesId, chapters);
|
||||
await MarkChaptersAsRead(user, volume.SeriesId, chapters);
|
||||
}
|
||||
}
|
||||
|
||||
@ -494,7 +506,7 @@ public class ReaderService : IReaderService
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List<int> { seriesId }, true);
|
||||
foreach (var volume in volumes.OrderBy(v => v.Number).Where(v => v.Number <= volumeNumber && v.Number > 0))
|
||||
{
|
||||
MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
||||
await MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
||||
}
|
||||
}
|
||||
|
||||
@ -540,4 +552,29 @@ public class ReaderService : IReaderService
|
||||
AvgHours = (int) Math.Round((pageCount / AvgPagesPerMinute / 60F))
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a Chapter name based on the library it's in
|
||||
/// </summary>
|
||||
/// <param name="libraryType"></param>
|
||||
/// <param name="includeHash">For comics only, includes a # which is used for numbering on cards</param>
|
||||
/// <param name="includeSpace">Add a space at the end of the string. if includeHash and includeSpace are true, only hash will be at the end.</param>
|
||||
/// <returns></returns>
|
||||
public string FormatChapterName(LibraryType libraryType, bool includeHash = false, bool includeSpace = false)
|
||||
{
|
||||
switch(libraryType)
|
||||
{
|
||||
case LibraryType.Manga:
|
||||
return "Chapter" + (includeSpace ? " " : string.Empty);
|
||||
case LibraryType.Comic:
|
||||
if (includeHash) {
|
||||
return "Issue #";
|
||||
}
|
||||
return "Issue" + (includeSpace ? " " : string.Empty);
|
||||
case LibraryType.Book:
|
||||
return "Book" + (includeSpace ? " " : string.Empty);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +105,6 @@ public class SeriesService : ISeriesService
|
||||
series.Metadata.LanguageLocked = true;
|
||||
}
|
||||
|
||||
|
||||
series.Metadata.CollectionTags ??= new List<CollectionTag>();
|
||||
UpdateRelatedList(updateSeriesMetadataDto.CollectionTags, series, allCollectionTags, (tag) =>
|
||||
{
|
||||
@ -200,10 +199,11 @@ public class SeriesService : ISeriesService
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Move this to a helper so we can easily test
|
||||
|
||||
private static void UpdateRelatedList(ICollection<CollectionTagDto> tags, Series series, IReadOnlyCollection<CollectionTag> allTags,
|
||||
Action<CollectionTag> handleAdd)
|
||||
{
|
||||
// TODO: Move UpdateRelatedList to a helper so we can easily test
|
||||
if (tags == null) return;
|
||||
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
||||
var existingTags = series.Metadata.CollectionTags.ToList();
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
@ -162,8 +163,12 @@ public class TaskScheduler : ITaskScheduler
|
||||
|
||||
public void ScanLibrary(int libraryId)
|
||||
{
|
||||
if (HasAlreadyEnqueuedTask("ScannerService","ScanLibrary", new object[] {libraryId}))
|
||||
{
|
||||
_logger.LogInformation("A duplicate request to scan library for library occured. Skipping");
|
||||
return;
|
||||
}
|
||||
_logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId);
|
||||
// TODO: If a library scan is already queued up for libraryId, don't do anything
|
||||
BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId));
|
||||
// When we do a scan, force cache to re-unpack in case page numbers change
|
||||
BackgroundJob.Enqueue(() => _cleanupService.CleanupCacheDirectory());
|
||||
@ -176,24 +181,48 @@ public class TaskScheduler : ITaskScheduler
|
||||
|
||||
public void RefreshMetadata(int libraryId, bool forceUpdate = true)
|
||||
{
|
||||
if (HasAlreadyEnqueuedTask("MetadataService","RefreshMetadata", new object[] {libraryId, forceUpdate}))
|
||||
{
|
||||
_logger.LogInformation("A duplicate request to refresh metadata for library occured. Skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Enqueuing library metadata refresh for: {LibraryId}", libraryId);
|
||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, forceUpdate));
|
||||
}
|
||||
|
||||
public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false)
|
||||
{
|
||||
if (HasAlreadyEnqueuedTask("MetadataService","RefreshMetadataForSeries", new object[] {libraryId, seriesId, forceUpdate}))
|
||||
{
|
||||
_logger.LogInformation("A duplicate request to refresh metadata for library occured. Skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Enqueuing series metadata refresh for: {SeriesId}", seriesId);
|
||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, forceUpdate));
|
||||
}
|
||||
|
||||
public void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false)
|
||||
{
|
||||
if (HasAlreadyEnqueuedTask("ScannerService", "ScanSeries", new object[] {libraryId, seriesId, forceUpdate}))
|
||||
{
|
||||
_logger.LogInformation("A duplicate request to scan series occured. Skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Enqueuing series scan for: {SeriesId}", seriesId);
|
||||
BackgroundJob.Enqueue(() => _scannerService.ScanSeries(libraryId, seriesId, CancellationToken.None));
|
||||
}
|
||||
|
||||
public void AnalyzeFilesForSeries(int libraryId, int seriesId, bool forceUpdate = false)
|
||||
{
|
||||
if (HasAlreadyEnqueuedTask("WordCountAnalyzerService", "ScanSeries", new object[] {libraryId, seriesId, forceUpdate}))
|
||||
{
|
||||
_logger.LogInformation("A duplicate request to scan series occured. Skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Enqueuing analyze files scan for: {SeriesId}", seriesId);
|
||||
BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(libraryId, seriesId, forceUpdate));
|
||||
}
|
||||
@ -212,4 +241,21 @@ public class TaskScheduler : ITaskScheduler
|
||||
var update = await _versionUpdaterService.CheckForUpdate();
|
||||
await _versionUpdaterService.PushUpdate(update);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this same invocation is already enqueued
|
||||
/// </summary>
|
||||
/// <param name="methodName">Method name that was enqueued</param>
|
||||
/// <param name="className">Class name the method resides on</param>
|
||||
/// <param name="args">object[] of arguments in the order they are passed to enqueued job</param>
|
||||
/// <param name="queue">Queue to check against. Defaults to "default"</param>
|
||||
/// <returns></returns>
|
||||
private static bool HasAlreadyEnqueuedTask(string className, string methodName, object[] args, string queue = "default")
|
||||
{
|
||||
var enqueuedJobs = JobStorage.Current.GetMonitoringApi().EnqueuedJobs(queue, 0, int.MaxValue);
|
||||
return enqueuedJobs.Any(j => j.Value.InEnqueuedState &&
|
||||
j.Value.Job.Method.DeclaringType != null && j.Value.Job.Args.SequenceEqual(args) &&
|
||||
j.Value.Job.Method.Name.Equals(methodName) &&
|
||||
j.Value.Job.Method.DeclaringType.Name.Equals(className));
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ namespace API.Services.Tasks.Scanner
|
||||
/// <param name="logger">Logger of the parent class that invokes this</param>
|
||||
/// <param name="directoryService">Directory Service</param>
|
||||
/// <param name="readingItemService">ReadingItemService Service for extracting information on a number of formats</param>
|
||||
/// <param name="eventHub">For firing off SignalR events</param>
|
||||
public ParseScannedFiles(ILogger logger, IDirectoryService directoryService,
|
||||
IReadingItemService readingItemService, IEventHub eventHub)
|
||||
{
|
||||
@ -251,6 +252,7 @@ namespace API.Services.Tasks.Scanner
|
||||
/// </summary>
|
||||
/// <param name="libraryType">Type of library. Used for selecting the correct file extensions to search for and parsing files</param>
|
||||
/// <param name="folders">The folders to scan. By default, this should be library.Folders, however it can be overwritten to restrict folders</param>
|
||||
/// <param name="libraryName">Name of the Library</param>
|
||||
/// <returns></returns>
|
||||
public async Task<Dictionary<ParsedSeries, List<ParserInfo>>> ScanLibrariesForSeries(LibraryType libraryType, IEnumerable<string> folders, string libraryName)
|
||||
{
|
||||
|
@ -22,6 +22,7 @@ using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.ResponseCompression;
|
||||
@ -30,6 +31,7 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using TaskScheduler = API.Services.TaskScheduler;
|
||||
|
||||
@ -230,7 +232,13 @@ namespace API
|
||||
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
ContentTypeProvider = new FileExtensionContentTypeProvider()
|
||||
ContentTypeProvider = new FileExtensionContentTypeProvider(),
|
||||
HttpsCompression = HttpsCompressionMode.Compress,
|
||||
OnPrepareResponse = ctx =>
|
||||
{
|
||||
const int durationInSeconds = 60 * 60 * 24;
|
||||
ctx.Context.Response.Headers[HeaderNames.CacheControl] = "public,max-age=" + durationInSeconds;
|
||||
}
|
||||
});
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
|
@ -1,56 +1,33 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Kavita.Common.EnvironmentInfo
|
||||
namespace Kavita.Common.EnvironmentInfo;
|
||||
|
||||
public static class BuildInfo
|
||||
{
|
||||
public static class BuildInfo
|
||||
static BuildInfo()
|
||||
{
|
||||
static BuildInfo()
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
Version = assembly.GetName().Version;
|
||||
|
||||
var attributes = assembly.GetCustomAttributes(true);
|
||||
|
||||
Branch = "unknown";
|
||||
|
||||
var config = attributes.OfType<AssemblyConfigurationAttribute>().FirstOrDefault();
|
||||
if (config != null)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
Version = assembly.GetName().Version;
|
||||
|
||||
var attributes = assembly.GetCustomAttributes(true);
|
||||
|
||||
Branch = "unknown";
|
||||
|
||||
var config = attributes.OfType<AssemblyConfigurationAttribute>().FirstOrDefault();
|
||||
if (config != null)
|
||||
{
|
||||
Branch = config.Configuration; // NOTE: This is not helpful, better to have main/develop branch
|
||||
}
|
||||
|
||||
Release = $"{Version}-{Branch}";
|
||||
Branch = config.Configuration; // NOTE: This is not helpful, better to have main/develop branch
|
||||
}
|
||||
|
||||
public static string AppName { get; } = "Kavita";
|
||||
|
||||
public static Version Version { get; }
|
||||
public static string Branch { get; }
|
||||
public static string Release { get; }
|
||||
|
||||
public static DateTime BuildDateTime
|
||||
{
|
||||
get
|
||||
{
|
||||
var fileLocation = Assembly.GetCallingAssembly().Location;
|
||||
return new FileInfo(fileLocation).LastWriteTimeUtc;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsDebug
|
||||
{
|
||||
get
|
||||
{
|
||||
#if DEBUG
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Release = $"{Version}-{Branch}";
|
||||
}
|
||||
|
||||
public static string AppName { get; } = "Kavita";
|
||||
|
||||
public static Version Version { get; }
|
||||
public static string Branch { get; }
|
||||
public static string Release { get; }
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Subject } from 'rxjs';
|
||||
import { finalize, take, takeWhile } from 'rxjs/operators';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { BulkAddToCollectionComponent } from '../cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component';
|
||||
import { AddToListModalComponent, ADD_FLOW } from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component';
|
||||
import { EditReadingListModalComponent } from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component';
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { ChapterInfo } from '../manga-reader/_models/chapter-info';
|
||||
import { UtilityService } from '../shared/_services/utility.service';
|
||||
import { Chapter } from '../_models/chapter';
|
||||
import { HourEstimateRange } from '../_models/hour-estimate-range';
|
||||
import { MangaFormat } from '../_models/manga-format';
|
||||
@ -23,7 +24,7 @@ export class ReaderService {
|
||||
// Override background color for reader and restore it onDestroy
|
||||
private originalBodyColor!: string;
|
||||
|
||||
constructor(private httpClient: HttpClient, private utilityService: UtilityService) { }
|
||||
constructor(private httpClient: HttpClient, private router: Router, private location: Location) { }
|
||||
|
||||
getNavigationArray(libraryId: number, seriesId: number, chapterId: number, format: MangaFormat) {
|
||||
if (format === undefined) format = MangaFormat.ARCHIVE;
|
||||
@ -144,7 +145,6 @@ export class ReaderService {
|
||||
return this.httpClient.get<Chapter>(this.baseUrl + 'reader/continue-point?seriesId=' + seriesId);
|
||||
}
|
||||
|
||||
// TODO: Cache this information
|
||||
getTimeLeft(seriesId: number) {
|
||||
return this.httpClient.get<HourEstimateRange>(this.baseUrl + 'reader/time-left?seriesId=' + seriesId);
|
||||
}
|
||||
@ -240,4 +240,15 @@ export class ReaderService {
|
||||
checkFullscreenMode() {
|
||||
return document.fullscreenElement != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the reader and causes a redirection
|
||||
*/
|
||||
closeReader(readingListMode: boolean = false, readingListId: number = 0) {
|
||||
if (readingListMode) {
|
||||
this.router.navigateByUrl('lists/' + readingListId);
|
||||
} else {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,10 @@
|
||||
<div class="col-4"><button class="btn btn-primary float-end" (click)="addLibrary()" title="Add Library"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> Add Library</span></button></div>
|
||||
</div>
|
||||
<ul class="list-group" *ngIf="!createLibraryToggle; else createLibrary">
|
||||
<li *ngFor="let library of libraries; let idx = index; trackby: trackbyLibrary" class="list-group-item no-hover">
|
||||
<li *ngFor="let library of libraries; let idx = index; trackBy: libraryTrackBy" class="list-group-item no-hover">
|
||||
<div>
|
||||
<h4>
|
||||
<span id="library-name--{{idx}}"><a [routerLink]="'/library/' + library.id">{{library.name}}</a></span>
|
||||
<!-- <div class="spinner-border text-primary" style="width: 1.5rem; height: 1.5rem;" role="status" *ngIf="scanInProgress.hasOwnProperty(library.id) && scanInProgress[library.id].progress" title="Scan in progress. Started at {{scanInProgress[library.id].timestamp | date: 'short'}}">
|
||||
<span class="visually-hidden">Scan for {{library.name}} in progress</span>
|
||||
</div> -->
|
||||
<div class="float-end">
|
||||
<button class="btn btn-secondary me-2 btn-sm" (click)="scanLibrary(library)" placement="top" ngbTooltip="Scan Library" attr.aria-label="Scan Library"><i class="fa fa-sync-alt" title="Scan"></i></button>
|
||||
<button class="btn btn-danger me-2 btn-sm" [disabled]="deletionInProgress" (click)="deleteLibrary(library)"><i class="fa fa-trash" placement="top" ngbTooltip="Delete Library" attr.aria-label="Delete {{library.name | sentenceCase}}"></i></button>
|
||||
@ -18,7 +15,7 @@
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div>Type: {{libraryType(library.type)}}</div>
|
||||
<div>Type: {{library.type | libraryType}}</div>
|
||||
<div>Shared Folders: {{library.folders.length + ' folders'}}</div>
|
||||
<div>
|
||||
Last Scanned:
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Subject } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, take, takeLast, takeUntil } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, filter, take, takeUntil } from 'rxjs/operators';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { NotificationProgressEvent } from 'src/app/_models/events/notification-progress-event';
|
||||
import { ScanSeriesEvent } from 'src/app/_models/events/scan-series-event';
|
||||
@ -14,7 +14,8 @@ import { LibraryEditorModalComponent } from '../_modals/library-editor-modal/lib
|
||||
@Component({
|
||||
selector: 'app-manage-library',
|
||||
templateUrl: './manage-library.component.html',
|
||||
styleUrls: ['./manage-library.component.scss']
|
||||
styleUrls: ['./manage-library.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -31,7 +32,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(private modalService: NgbModal, private libraryService: LibraryService,
|
||||
private toastr: ToastrService, private confirmService: ConfirmService,
|
||||
private hubService: MessageHubService) { }
|
||||
private hubService: MessageHubService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getLibraries();
|
||||
@ -56,6 +57,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
const existingLibrary = this.libraries.find(lib => lib.id === libId);
|
||||
if (existingLibrary !== undefined) {
|
||||
existingLibrary.lastScanned = newLibrary?.lastScanned || existingLibrary.lastScanned;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -79,9 +81,11 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
|
||||
getLibraries() {
|
||||
this.loading = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.libraryService.getLibraries().pipe(take(1)).subscribe(libraries => {
|
||||
this.libraries = libraries;
|
||||
this.loading = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -109,6 +113,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
this.deletionInProgress = true;
|
||||
this.libraryService.delete(library.id).pipe(take(1)).subscribe(() => {
|
||||
this.deletionInProgress = false;
|
||||
this.cdRef.markForCheck();
|
||||
this.getLibraries();
|
||||
this.toastr.success('Library has been removed');
|
||||
});
|
||||
@ -120,16 +125,4 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
this.toastr.info('A scan has been queued for ' + library.name);
|
||||
});
|
||||
}
|
||||
|
||||
libraryType(libraryType: LibraryType) {
|
||||
switch(libraryType) {
|
||||
case LibraryType.Book:
|
||||
return 'Book';
|
||||
case LibraryType.Comic:
|
||||
return 'Comic';
|
||||
case LibraryType.Manga:
|
||||
return 'Manga';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -84,7 +84,7 @@
|
||||
tabindex="-1" [ngStyle]="{height: PageHeightForPagination}"></div>
|
||||
</ng-container>
|
||||
|
||||
<div class="book-container" [ngClass]="{'immersive' : immersiveMode}"> <!-- {{ColumnLayout}}-->
|
||||
<div class="book-container" [ngClass]="{'immersive' : immersiveMode}">
|
||||
|
||||
<div #readingHtml class="book-content {{ColumnLayout}}" [ngStyle]="{'max-height': ColumnHeight, 'column-width': ColumnWidth}"
|
||||
[ngClass]="{'immersive': immersiveMode && actionBarVisible}"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AfterViewInit, Component, ElementRef, HostListener, Inject, OnDestroy, OnInit, Renderer2, RendererStyleFlags2, ViewChild } from '@angular/core';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, Inject, OnDestroy, OnInit, Renderer2, RendererStyleFlags2, ViewChild } from '@angular/core';
|
||||
import {DOCUMENT, Location} from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
@ -54,6 +54,7 @@ const elementLevelStyles = ['line-height', 'font-family'];
|
||||
selector: 'app-book-reader',
|
||||
templateUrl: './book-reader.component.html',
|
||||
styleUrls: ['./book-reader.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [
|
||||
trigger('isLoading', [
|
||||
state('false', style({opacity: 1})),
|
||||
@ -110,8 +111,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
/**
|
||||
* A stack of the chapter ids we come across during continuous reading mode. When we traverse a boundary, we use this to avoid extra API calls.
|
||||
* @see Stack
|
||||
* TODO: See if continuousChaptersStack can be moved into reader service so we can reduce code duplication between readers (and also use ChapterInfo with it instead)
|
||||
*/
|
||||
continuousChaptersStack: Stack<number> = new Stack(); // TODO: See if continuousChaptersStack can be moved into reader service so we can reduce code duplication between readers (and also use ChapterInfo with it instead)
|
||||
continuousChaptersStack: Stack<number> = new Stack();
|
||||
|
||||
/**
|
||||
* Belongs to the drawer component
|
||||
@ -383,10 +385,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private renderer: Renderer2, private navService: NavService, private toastr: ToastrService,
|
||||
private domSanitizer: DomSanitizer, private bookService: BookService, private memberService: MemberService,
|
||||
private scrollService: ScrollService, private utilityService: UtilityService, private libraryService: LibraryService,
|
||||
@Inject(DOCUMENT) private document: Document, private themeService: ThemeService) {
|
||||
@Inject(DOCUMENT) private document: Document, private themeService: ThemeService, private readonly cdRef: ChangeDetectorRef) {
|
||||
this.navService.hideNavBar();
|
||||
this.themeService.clearThemes();
|
||||
this.navService.hideSideNav();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -411,7 +414,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
// Highlight the current chapter we are on
|
||||
if (Object.keys(this.pageAnchors).length !== 0) {
|
||||
// get the height of the document so we can capture markers that are halfway on the document viewport
|
||||
const verticalOffset = this.scrollService.scrollPosition + (this.document.body.offsetHeight / 2);
|
||||
const verticalOffset = this.reader.nativeElement?.scrollTop || (this.scrollService.scrollPosition + (this.document.body.offsetHeight / 2));
|
||||
|
||||
const alreadyReached = Object.values(this.pageAnchors).filter((i: number) => i <= verticalOffset);
|
||||
if (alreadyReached.length > 0) {
|
||||
@ -419,6 +422,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
} else {
|
||||
this.currentPageAnchor = '';
|
||||
}
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
// Find the element that is on screen to bookmark against
|
||||
@ -439,7 +444,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (!this.incognitoMode) {
|
||||
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, tempPageNum, this.lastSeenScrollPartPath).pipe(take(1)).subscribe(() => {/* No operation */});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@ -481,6 +485,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.readingListMode = true;
|
||||
this.readingListId = parseInt(readingListId, 10);
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
||||
this.memberService.hasReadingProgress(this.libraryId).pipe(take(1)).subscribe(hasProgress => {
|
||||
@ -504,18 +509,20 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.nextChapterDisabled = false;
|
||||
this.prevChapterDisabled = false;
|
||||
this.nextChapterPrefetched = false;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
||||
|
||||
this.bookService.getBookInfo(this.chapterId).subscribe(info => {
|
||||
this.bookTitle = info.bookTitle;
|
||||
|
||||
if (this.readingListMode && info.seriesFormat !== MangaFormat.EPUB) {
|
||||
// Redirect to the manga reader.
|
||||
const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId);
|
||||
this.router.navigate(this.readerService.getNavigationArray(info.libraryId, info.seriesId, this.chapterId, info.seriesFormat), {queryParams: params});
|
||||
return;
|
||||
}
|
||||
|
||||
this.bookTitle = info.bookTitle;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
forkJoin({
|
||||
chapter: this.seriesService.getChapter(this.chapterId),
|
||||
@ -527,23 +534,21 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.maxPages = results.chapter.pages;
|
||||
this.chapters = results.chapters;
|
||||
this.pageNum = results.progress.pageNum;
|
||||
this.cdRef.markForCheck();
|
||||
if (results.progress.bookScrollId) this.lastSeenScrollPartPath = results.progress.bookScrollId;
|
||||
|
||||
|
||||
|
||||
this.continuousChaptersStack.push(this.chapterId);
|
||||
|
||||
this.libraryService.getLibraryType(this.libraryId).pipe(take(1)).subscribe(type => {
|
||||
this.libraryType = type;
|
||||
});
|
||||
|
||||
// We need to think about if the user modified this and this function call is a continuous reader one
|
||||
//this.updateLayoutMode(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default);
|
||||
this.updateImagesWithHeight();
|
||||
|
||||
|
||||
if (this.pageNum >= this.maxPages) {
|
||||
this.pageNum = this.maxPages - 1;
|
||||
this.cdRef.markForCheck();
|
||||
this.saveProgress();
|
||||
}
|
||||
|
||||
@ -552,6 +557,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {
|
||||
this.nextChapterDisabled = true;
|
||||
this.nextChapterPrefetched = true;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
this.setPageNum(this.pageNum);
|
||||
@ -561,6 +567,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {
|
||||
this.prevChapterDisabled = true;
|
||||
this.prevChapterPrefetched = true; // If there is no prev chapter, then mark it as prefetched
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
this.setPageNum(this.pageNum);
|
||||
@ -577,20 +584,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event: any){
|
||||
// Update the window Height
|
||||
this.updateWidthAndHeightCalcs();
|
||||
|
||||
const resumeElement = this.getFirstVisibleElementXPath();
|
||||
if (this.layoutMode !== BookPageLayoutMode.Default && resumeElement !== null && resumeElement !== undefined) {
|
||||
this.scrollTo(resumeElement); // This works pretty well, but not perfect
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:orientationchange', ['$event'])
|
||||
onOrientationChange() {
|
||||
onResize(){
|
||||
// Update the window Height
|
||||
this.updateWidthAndHeightCalcs();
|
||||
|
||||
const resumeElement = this.getFirstVisibleElementXPath();
|
||||
if (this.layoutMode !== BookPageLayoutMode.Default && resumeElement !== null && resumeElement !== undefined) {
|
||||
this.scrollTo(resumeElement); // This works pretty well, but not perfect
|
||||
@ -616,6 +614,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
closeReader() {
|
||||
this.readerService.closeReader(this.readingListMode, this.readingListId);
|
||||
}
|
||||
|
||||
sortElements(a: Element, b: Element) {
|
||||
const aTop = a.getBoundingClientRect().top;
|
||||
const bTop = b.getBoundingClientRect().top;
|
||||
@ -646,6 +648,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.prevPageDisabled) { return; }
|
||||
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.continuousChaptersStack.pop();
|
||||
const prevChapter = this.continuousChaptersStack.peek();
|
||||
if (prevChapter != this.chapterId) {
|
||||
@ -657,6 +660,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (this.prevChapterPrefetched && this.prevChapterId === CHAPTER_ID_DOESNT_EXIST) {
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -677,8 +682,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
// Load chapter Id onto route but don't reload
|
||||
const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId);
|
||||
window.history.replaceState({}, '', newRoute);
|
||||
this.init();
|
||||
this.toastr.info(direction + ' ' + this.utilityService.formatChapterName(this.libraryType).toLowerCase() + ' loaded', '', {timeOut: 3000});
|
||||
this.cdRef.markForCheck();
|
||||
this.init();
|
||||
} else {
|
||||
// This will only happen if no actual chapter can be found
|
||||
this.toastr.warning('Could not find ' + direction.toLowerCase() + ' ' + this.utilityService.formatChapterName(this.libraryType).toLowerCase());
|
||||
@ -688,6 +694,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
} else {
|
||||
this.nextPageDisabled = true;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@ -696,15 +703,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.loadPage('id("' + event.part + '")');
|
||||
}
|
||||
|
||||
closeReader() {
|
||||
if (this.readingListMode) {
|
||||
this.router.navigateByUrl('lists/' + this.readingListId);
|
||||
} else {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds a click handler for any anchors that have 'kavita-page'. If 'kavita-page' present, changes page to kavita-page and optionally passes a part value
|
||||
* from 'kavita-part', which will cause the reader to scroll to the marker.
|
||||
@ -775,14 +773,15 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
loadPage(part?: string | undefined, scrollTop?: number | undefined) {
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.bookService.getBookPage(this.chapterId, this.pageNum).pipe(take(1)).subscribe(content => {
|
||||
this.page = this.domSanitizer.bypassSecurityTrustHtml(content); // PERF: Potential optimization to prefetch next/prev page and store in localStorage
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
setTimeout(() => {
|
||||
this.addLinkClickHandlers();
|
||||
this.updateReaderStyles(this.pageStyles);
|
||||
this.updateReaderStyles(this.pageStyles);
|
||||
|
||||
const imgs = this.readingSectionElemRef.nativeElement.querySelectorAll('img');
|
||||
if (imgs === null || imgs.length === 0) {
|
||||
@ -805,7 +804,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
* Applies a max-height inline css property on each image in the page if the layout mode is column-based, else it removes the property
|
||||
*/
|
||||
updateImagesWithHeight() {
|
||||
|
||||
const images = this.readingSectionElemRef?.nativeElement.querySelectorAll('img') || [];
|
||||
|
||||
if (this.layoutMode !== BookPageLayoutMode.Default) {
|
||||
@ -822,6 +820,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
setupPage(part?: string | undefined, scrollTop?: number | undefined) {
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
// Virtual Paging stuff
|
||||
this.updateWidthAndHeightCalcs();
|
||||
@ -853,6 +852,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
// we need to click the document before arrow keys will scroll down.
|
||||
this.reader.nativeElement.focus();
|
||||
this.saveProgress();
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
@ -868,21 +869,24 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
setPageNum(pageNum: number) {
|
||||
this.pageNum = Math.max(Math.min(pageNum, this.maxPages), 0);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
if (this.pageNum >= this.maxPages - 10) {
|
||||
// Tell server to cache the next chapter
|
||||
if (!this.nextChapterPrefetched && this.nextChapterId !== CHAPTER_ID_DOESNT_EXIST) { // && !this.nextChapterDisabled
|
||||
if (!this.nextChapterPrefetched && this.nextChapterId !== CHAPTER_ID_DOESNT_EXIST) {
|
||||
this.readerService.getChapterInfo(this.nextChapterId).pipe(take(1), catchError(err => {
|
||||
this.nextChapterDisabled = true;
|
||||
this.cdRef.markForCheck();
|
||||
return of(null);
|
||||
})).subscribe(res => {
|
||||
this.nextChapterPrefetched = true;
|
||||
});
|
||||
}
|
||||
} else if (this.pageNum <= 10) {
|
||||
if (!this.prevChapterPrefetched && this.prevChapterId !== CHAPTER_ID_DOESNT_EXIST) { // && !this.prevChapterDisabled
|
||||
if (!this.prevChapterPrefetched && this.prevChapterId !== CHAPTER_ID_DOESNT_EXIST) {
|
||||
this.readerService.getChapterInfo(this.prevChapterId).pipe(take(1), catchError(err => {
|
||||
this.prevChapterDisabled = true;
|
||||
this.cdRef.markForCheck();
|
||||
return of(null);
|
||||
})).subscribe(res => {
|
||||
this.prevChapterPrefetched = true;
|
||||
@ -1105,6 +1109,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
// Recalculate if bottom action bar is needed
|
||||
this.scrollbarNeeded = this.readingHtml.nativeElement.clientHeight > this.reader.nativeElement.clientHeight;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
toggleDrawer() {
|
||||
@ -1113,6 +1118,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.immersiveMode) {
|
||||
this.actionBarVisible = false;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
scrollTo(partSelector: string) {
|
||||
@ -1133,7 +1139,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.layoutMode === BookPageLayoutMode.Default) {
|
||||
const fromTopOffset = element.getBoundingClientRect().top + window.pageYOffset + TOP_OFFSET;
|
||||
// We need to use a delay as webkit browsers (aka apple devices) don't always have the document rendered by this point
|
||||
setTimeout(() => this.scrollService.scrollTo(fromTopOffset, this.reader.nativeElement), 10);
|
||||
setTimeout(() => this.scrollService.scrollTo(fromTopOffset, this.reader.nativeElement), 10); // BUG: This is broken
|
||||
} else {
|
||||
setTimeout(() => (element as Element).scrollIntoView({'block': 'start', 'inline': 'start'}));
|
||||
}
|
||||
@ -1184,11 +1190,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.isFullscreen) {
|
||||
this.readerService.exitFullscreen(() => {
|
||||
this.isFullscreen = false;
|
||||
this.cdRef.markForCheck();
|
||||
this.renderer.removeStyle(this.reader.nativeElement, 'background');
|
||||
});
|
||||
} else {
|
||||
this.readerService.enterFullscreen(this.reader.nativeElement, () => {
|
||||
this.isFullscreen = true;
|
||||
this.cdRef.markForCheck();
|
||||
// HACK: This is a bug with how browsers change the background color for fullscreen mode
|
||||
this.renderer.setStyle(this.reader.nativeElement, 'background', this.themeService.getCssVariable('--bs-body-color'));
|
||||
if (!this.darkMode) {
|
||||
@ -1200,6 +1208,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
updateLayoutMode(mode: BookPageLayoutMode) {
|
||||
this.layoutMode = mode;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
// Remove any max-heights from column layout
|
||||
this.updateImagesWithHeight();
|
||||
@ -1209,7 +1218,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
setTimeout(() => this.updateLayoutMode(this.layoutMode), 10);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {this.scrollbarNeeded = this.readingHtml.nativeElement.clientHeight > this.reader.nativeElement.clientHeight;});
|
||||
setTimeout(() => {
|
||||
this.scrollbarNeeded = this.readingHtml.nativeElement.clientHeight > this.reader.nativeElement.clientHeight;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
// When I switch layout, I might need to resume the progress point.
|
||||
if (mode === BookPageLayoutMode.Default) {
|
||||
@ -1220,6 +1232,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
updateReadingDirection(readingDirection: ReadingDirection) {
|
||||
this.readingDirection = readingDirection;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateImmersiveMode(immersiveMode: boolean) {
|
||||
@ -1227,13 +1240,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.immersiveMode && !this.drawerOpen) {
|
||||
this.actionBarVisible = false;
|
||||
}
|
||||
|
||||
this.updateReadingSectionHeight();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateReadingSectionHeight() {
|
||||
setTimeout(() => {
|
||||
//console.log('setting height on ', this.readingSectionElemRef)
|
||||
if (this.immersiveMode) {
|
||||
this.renderer.setStyle(this.readingSectionElemRef, 'height', 'calc(var(--vh, 1vh) * 100)', RendererStyleFlags2.Important);
|
||||
} else {
|
||||
@ -1263,6 +1275,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
setupPageAnchors() {
|
||||
this.pageAnchors = {};
|
||||
this.currentPageAnchor = '';
|
||||
this.cdRef.markForCheck();
|
||||
const ids = this.chapters.map(item => item.children).flat().filter(item => item.page === this.pageNum).map(item => item.part).filter(item => item.length > 0);
|
||||
if (ids.length > 0) {
|
||||
const elems = this.getPageMarkers(ids);
|
||||
@ -1275,6 +1288,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
// Settings Handlers
|
||||
showPaginationOverlay(clickToPaginate: boolean) {
|
||||
this.clickToPaginate = clickToPaginate;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.clearTimeout(this.clickToPaginateVisualOverlayTimeout2);
|
||||
if (!clickToPaginate) { return; }
|
||||
@ -1293,6 +1307,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
showClickToPaginateVisualOverlay() {
|
||||
this.clickToPaginateVisualOverlay = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
if (this.clickToPaginateVisualOverlay && this.clickToPaginateVisualOverlayTimeout !== undefined) {
|
||||
clearTimeout(this.clickToPaginateVisualOverlayTimeout);
|
||||
@ -1300,6 +1315,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
this.clickToPaginateVisualOverlayTimeout = setTimeout(() => {
|
||||
this.clickToPaginateVisualOverlay = false;
|
||||
this.cdRef.markForCheck();
|
||||
}, 1000);
|
||||
|
||||
}
|
||||
@ -1338,8 +1354,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
Math.abs(this.mousePosition.y - event.screenY) <= mouseOffset
|
||||
) {
|
||||
this.actionBarVisible = !this.actionBarVisible;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mouseDown($event: MouseEvent) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { Subject, take, takeUntil } from 'rxjs';
|
||||
import { BookPageLayoutMode } from 'src/app/_models/book-page-layout-mode';
|
||||
@ -60,7 +60,8 @@ const mobileBreakpointMarginOverride = 700;
|
||||
@Component({
|
||||
selector: 'app-reader-settings',
|
||||
templateUrl: './reader-settings.component.html',
|
||||
styleUrls: ['./reader-settings.component.scss']
|
||||
styleUrls: ['./reader-settings.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ReaderSettingsComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
@ -131,12 +132,14 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
||||
constructor(private bookService: BookService, private accountService: AccountService,
|
||||
@Inject(DOCUMENT) private document: Document, private themeService: ThemeService) {}
|
||||
@Inject(DOCUMENT) private document: Document, private themeService: ThemeService,
|
||||
private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.fontFamilies = this.bookService.getFontFamilies();
|
||||
this.fontOptions = this.fontFamilies.map(f => f.title);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
@ -211,6 +214,7 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
||||
this.setTheme(this.user.preferences.bookReaderThemeName || this.themeService.defaultBookTheme);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
// Emit first time so book reader gets the setting
|
||||
this.readingDirection.emit(this.readingDirectionModel);
|
||||
@ -241,6 +245,7 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.settingsForm.get('bookReaderFontFamily')?.setValue(this.user.preferences.bookReaderFontFamily);
|
||||
this.cdRef.markForCheck();
|
||||
this.styleUpdate.emit(this.pageStyles);
|
||||
}
|
||||
|
||||
@ -264,12 +269,12 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
|
||||
'margin-right': margin || this.pageStyles['margin-right'] || defaultMargin,
|
||||
'line-height': lineHeight || this.pageStyles['line-height'] || '100%'
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
setTheme(themeName: string) {
|
||||
const theme = this.themes.find(t => t.name === themeName);
|
||||
this.activeTheme = theme;
|
||||
this.cdRef.markForCheck();
|
||||
this.colorThemeUpdate.emit(theme);
|
||||
}
|
||||
|
||||
@ -280,11 +285,13 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
|
||||
this.readingDirectionModel = ReadingDirection.LeftToRight;
|
||||
}
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
this.readingDirection.emit(this.readingDirectionModel);
|
||||
}
|
||||
|
||||
toggleFullscreen() {
|
||||
this.isFullscreen = !this.isFullscreen;
|
||||
this.cdRef.markForCheck();
|
||||
this.fullscreen.emit();
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
<div class="table-of-contents">
|
||||
<!-- <h3>Table of Contents</h3> -->
|
||||
<div *ngIf="chapters.length === 0">
|
||||
<em>This book does not have Table of Contents set in the metadata or a toc file</em>
|
||||
</div>
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { BookChapterItem } from '../_models/book-chapter-item';
|
||||
|
||||
@Component({
|
||||
selector: 'app-table-of-contents',
|
||||
templateUrl: './table-of-contents.component.html',
|
||||
styleUrls: ['./table-of-contents.component.scss']
|
||||
styleUrls: ['./table-of-contents.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.Default
|
||||
})
|
||||
export class TableOfContentsComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -16,11 +17,8 @@ export class TableOfContentsComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Output() loadChapter: EventEmitter<{pageNum: number, part: string}> = new EventEmitter();
|
||||
|
||||
|
||||
|
||||
private onDestroy: Subject<void> = new Subject();
|
||||
|
||||
|
||||
pageAnchors: {[n: string]: number } = {};
|
||||
|
||||
constructor() {}
|
||||
@ -44,5 +42,4 @@ export class TableOfContentsComponent implements OnInit, OnDestroy {
|
||||
loadChapterPage(pageNum: number, part: string) {
|
||||
this.loadChapter.emit({pageNum, part});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item clickable" tabindex="0" role="button" *ngFor="let collectionTag of lists | filter: filterList; let i = index" (click)="addToCollection(collectionTag)">
|
||||
<li class="list-group-item clickable" tabindex="0" role="button" *ngFor="let collectionTag of lists | filter: filterList; let i = index; trackBy: collectionTitleTrackby" (click)="addToCollection(collectionTag)">
|
||||
{{collectionTag.title}} <i class="fa fa-angle-double-up" *ngIf="collectionTag.promoted" title="Promoted"></i>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No collections created yet</li>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, ElementRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { FormGroup, FormControl } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
@ -10,7 +10,8 @@ import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
||||
selector: 'app-bulk-add-to-collection',
|
||||
templateUrl: './bulk-add-to-collection.component.html',
|
||||
encapsulation: ViewEncapsulation.None, // This is needed as per the bootstrap modal documentation to get styles to work.
|
||||
styleUrls: ['./bulk-add-to-collection.component.scss']
|
||||
styleUrls: ['./bulk-add-to-collection.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BulkAddToCollectionComponent implements OnInit {
|
||||
|
||||
@ -27,10 +28,13 @@ export class BulkAddToCollectionComponent implements OnInit {
|
||||
loading: boolean = false;
|
||||
listForm: FormGroup = new FormGroup({});
|
||||
|
||||
collectionTitleTrackby = (index: number, item: CollectionTag) => `${item.title}`;
|
||||
|
||||
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
|
||||
|
||||
|
||||
constructor(private modal: NgbActiveModal, private collectionService: CollectionTagService, private toastr: ToastrService) { }
|
||||
constructor(private modal: NgbActiveModal, private collectionService: CollectionTagService,
|
||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@ -38,9 +42,11 @@ export class BulkAddToCollectionComponent implements OnInit {
|
||||
this.listForm.addControl('filterQuery', new FormControl('', []));
|
||||
|
||||
this.loading = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.collectionService.allTags().subscribe(tags => {
|
||||
this.lists = tags;
|
||||
this.loading = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -48,6 +54,7 @@ export class BulkAddToCollectionComponent implements OnInit {
|
||||
// Shift focus to input
|
||||
if (this.inputElem) {
|
||||
this.inputElem.nativeElement.select();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,144 +0,0 @@
|
||||
<div *ngIf="data !== undefined">
|
||||
<div class="modal-header">
|
||||
<h4 *ngIf="libraryType !== LibraryType.Comic else comicHeader" class="modal-title" id="modal-basic-title">
|
||||
{{parentName}} - {{data.number != 0 ? (isChapter ? 'Chapter ' : 'Volume ') + data.number : 'Special'}} Details</h4>
|
||||
<ng-template #comicHeader><h4 class="modal-title" id="modal-basic-title">
|
||||
{{parentName}} - {{data.number != 0 ? (isChapter ? 'Issue #' : 'Volume ') + data.number : 'Special'}} Details</h4>
|
||||
</ng-template>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
<li [ngbNavItem]="tabs[0]" *ngIf="!tabs[0].disabled">
|
||||
<a ngbNavLink>{{tabs[0].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="container-fluid row g-0">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="chapter.coverImage"></app-image>
|
||||
ID: {{data.id}}
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6">
|
||||
<div class="row g-0">
|
||||
<h4>
|
||||
{{chapter?.titleName}}
|
||||
</h4>
|
||||
<span>
|
||||
<span *ngIf="chapterMetadata && chapterMetadata.releaseDate !== null">Release Date: {{chapterMetadata.releaseDate | date: 'shortDate' || '-'}}</span>
|
||||
</span>
|
||||
<span class="text-accent">{{data.pages}} pages</span>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="col-auto">
|
||||
Added: {{(chapter.created | date: 'short') || '-'}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="col-auto">
|
||||
Age Rating: {{ageRating}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<ng-container *ngIf="chapterMetadata !== undefined">
|
||||
<div class="row g-0" *ngIf="chapterMetadata.tags && chapterMetadata.tags.length > 0">
|
||||
<h6>Tags</h6>
|
||||
<app-badge-expander [items]="chapterMetadata.tags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge>{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
<div class="row g-0" *ngIf="chapterMetadata.genres && chapterMetadata.genres.length > 0">
|
||||
<h6>Genres</h6>
|
||||
<app-badge-expander [items]="chapterMetadata.genres">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge>{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[1]" *ngIf="!tabs[1].disabled">
|
||||
<a ngbNavLink>{{tabs[1].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-chapter-metadata-detail [chapter]="chapterMetadata"></app-chapter-metadata-detail>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[2]" *ngIf="!tabs[2].disabled">
|
||||
<a ngbNavLink>{{tabs[2].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)" [showReset]="chapter.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
<div class="row g-0">
|
||||
<button class="btn btn-primary flex-end mb-2" [disabled]="coverImageSaveLoading" (click)="saveCoverImage()">
|
||||
<ng-container *ngIf="coverImageSaveLoading; else notSaving">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</ng-container>
|
||||
<ng-template #notSaving>
|
||||
Save
|
||||
</ng-template>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[3]" *ngIf="!tabs[3].disabled">
|
||||
<a ngbNavLink>{{tabs[3].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<h4 *ngIf="!utilityService.isChapter(data)">{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
|
||||
<ul class="list-unstyled">
|
||||
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{utilityService.formatChapterName(libraryType, true, false)}} {{formatChapterNumber(chapter)}}">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="chapter.coverImage"></app-image>
|
||||
</a>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1">
|
||||
<span >
|
||||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
<ng-container *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
</ng-container>
|
||||
</span>
|
||||
<span class="badge bg-primary rounded-pill ms-1">
|
||||
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
||||
<span *ngIf="chapter.pagesRead === 0">UNREAD</span>
|
||||
<span *ngIf="chapter.pagesRead === chapter.pages">READ</span>
|
||||
</span>
|
||||
</span>
|
||||
<ng-template #specialHeader>Files</ng-template>
|
||||
</h5>
|
||||
<ul class="list-group">
|
||||
<li *ngFor="let file of chapter.files" class="list-group-item no-hover">
|
||||
<span>{{file.filePath}}</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
Pages: {{file.pages}}
|
||||
</div>
|
||||
<div class="col" *ngIf="data.hasOwnProperty('created')">
|
||||
Added: {{(data.created | date: 'short') || '-'}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary" (click)="close()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +0,0 @@
|
||||
.scrollable-modal {
|
||||
max-height: 90vh; // 600px
|
||||
overflow: auto;
|
||||
}
|
@ -1,227 +0,0 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { MangaFile } from 'src/app/_models/manga-file';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { ActionService } from 'src/app/_services/action.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
import { LibraryType } from '../../../_models/library';
|
||||
import { LibraryService } from '../../../_services/library.service';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { PersonRole } from 'src/app/_models/person';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
import { ChapterMetadata } from 'src/app/_models/chapter-metadata';
|
||||
import { PageBookmark } from 'src/app/_models/page-bookmark';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
import { MetadataService } from 'src/app/_services/metadata.service';
|
||||
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-details-modal',
|
||||
templateUrl: './card-details-modal.component.html',
|
||||
styleUrls: ['./card-details-modal.component.scss']
|
||||
})
|
||||
export class CardDetailsModalComponent implements OnInit {
|
||||
|
||||
@Input() parentName = '';
|
||||
@Input() seriesId: number = 0;
|
||||
@Input() libraryId: number = 0;
|
||||
@Input() data!: Volume | Chapter; // Volume | Chapter
|
||||
|
||||
/**
|
||||
* If this is a volume, this will be first chapter for said volume.
|
||||
*/
|
||||
chapter!: Chapter;
|
||||
isChapter = false;
|
||||
chapters: Chapter[] = [];
|
||||
|
||||
|
||||
/**
|
||||
* If a cover image update occured.
|
||||
*/
|
||||
coverImageUpdate: boolean = false;
|
||||
coverImageIndex: number = 0;
|
||||
/**
|
||||
* Url of the selected cover
|
||||
*/
|
||||
selectedCover: string = '';
|
||||
coverImageLocked: boolean = false;
|
||||
/**
|
||||
* When the API is doing work
|
||||
*/
|
||||
coverImageSaveLoading: boolean = false;
|
||||
imageUrls: Array<string> = [];
|
||||
|
||||
|
||||
actions: ActionItem<any>[] = [];
|
||||
chapterActions: ActionItem<Chapter>[] = [];
|
||||
libraryType: LibraryType = LibraryType.Manga;
|
||||
|
||||
|
||||
tabs = [{title: 'General', disabled: false}, {title: 'Metadata', disabled: false}, {title: 'Cover', disabled: false}, {title: 'Info', disabled: false}];
|
||||
active = this.tabs[0];
|
||||
|
||||
chapterMetadata!: ChapterMetadata;
|
||||
ageRating!: string;
|
||||
|
||||
|
||||
get Breakpoint(): typeof Breakpoint {
|
||||
return Breakpoint;
|
||||
}
|
||||
|
||||
get PersonRole() {
|
||||
return PersonRole;
|
||||
}
|
||||
|
||||
get LibraryType(): typeof LibraryType {
|
||||
return LibraryType;
|
||||
}
|
||||
|
||||
constructor(public modal: NgbActiveModal, public utilityService: UtilityService,
|
||||
public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService,
|
||||
private accountService: AccountService, private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService, private router: Router, private libraryService: LibraryService,
|
||||
private seriesService: SeriesService, private readerService: ReaderService, public metadataService: MetadataService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.utilityService.isChapter(this.data);
|
||||
|
||||
this.chapter = this.utilityService.isChapter(this.data) ? (this.data as Chapter) : (this.data as Volume).chapters[0];
|
||||
|
||||
this.imageUrls.push(this.imageService.getChapterCoverImage(this.chapter.id));
|
||||
|
||||
this.seriesService.getChapterMetadata(this.chapter.id).subscribe(metadata => {
|
||||
this.chapterMetadata = metadata;
|
||||
|
||||
this.metadataService.getAgeRating(this.chapterMetadata.ageRating).subscribe(ageRating => this.ageRating = ageRating);
|
||||
});
|
||||
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
if (!this.accountService.hasAdminRole(user)) {
|
||||
this.tabs.find(s => s.title === 'Cover')!.disabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.libraryService.getLibraryType(this.libraryId).subscribe(type => {
|
||||
this.libraryType = type;
|
||||
});
|
||||
|
||||
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)).filter(item => item.action !== Action.Edit);
|
||||
|
||||
if (this.isChapter) {
|
||||
this.chapters.push(this.data as Chapter);
|
||||
} else if (!this.isChapter) {
|
||||
this.chapters.push(...(this.data as Volume).chapters);
|
||||
}
|
||||
// TODO: Move this into the backend
|
||||
this.chapters.sort(this.utilityService.sortChapters);
|
||||
this.chapters.forEach(c => c.coverImage = this.imageService.getChapterCoverImage(c.id));
|
||||
// Try to show an approximation of the reading order for files
|
||||
var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||
this.chapters.forEach((c: Chapter) => {
|
||||
c.files.sort((a: MangaFile, b: MangaFile) => collator.compare(a.filePath, b.filePath));
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.close({coverImageUpdate: this.coverImageUpdate});
|
||||
}
|
||||
|
||||
formatChapterNumber(chapter: Chapter) {
|
||||
if (chapter.number === '0') {
|
||||
return '1';
|
||||
}
|
||||
return chapter.number;
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>, chapter: Chapter) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, chapter);
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedIndex(index: number) {
|
||||
this.coverImageIndex = index;
|
||||
}
|
||||
|
||||
updateSelectedImage(url: string) {
|
||||
this.selectedCover = url;
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
this.coverImageLocked = false;
|
||||
}
|
||||
|
||||
saveCoverImage() {
|
||||
this.coverImageSaveLoading = true;
|
||||
const selectedIndex = this.coverImageIndex || 0;
|
||||
if (selectedIndex > 0) {
|
||||
this.uploadService.updateChapterCoverImage(this.chapter.id, this.selectedCover).subscribe(() => {
|
||||
if (this.coverImageIndex > 0) {
|
||||
this.chapter.coverImageLocked = true;
|
||||
this.coverImageUpdate = true;
|
||||
}
|
||||
this.coverImageSaveLoading = false;
|
||||
}, err => this.coverImageSaveLoading = false);
|
||||
} else if (this.coverImageLocked === false) {
|
||||
this.uploadService.resetChapterCoverLock(this.chapter.id).subscribe(() => {
|
||||
this.toastr.info('Cover image reset');
|
||||
this.coverImageSaveLoading = false;
|
||||
this.coverImageUpdate = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
markChapterAsRead(chapter: Chapter) {
|
||||
if (this.seriesId === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsRead(this.seriesId, chapter, () => { /* No Action */ });
|
||||
}
|
||||
|
||||
markChapterAsUnread(chapter: Chapter) {
|
||||
if (this.seriesId === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsUnread(this.seriesId, chapter, () => { /* No Action */ });
|
||||
}
|
||||
|
||||
handleChapterActionCallback(action: Action, chapter: Chapter) {
|
||||
switch (action) {
|
||||
case(Action.MarkAsRead):
|
||||
this.markChapterAsRead(chapter);
|
||||
break;
|
||||
case(Action.MarkAsUnread):
|
||||
this.markChapterAsUnread(chapter);
|
||||
break;
|
||||
case(Action.AddToReadingList):
|
||||
this.actionService.addChapterToReadingList(chapter, this.seriesId);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
readChapter(chapter: Chapter) {
|
||||
if (chapter.pages === 0) {
|
||||
this.toastr.error('There are no pages. Kavita was not able to read this archive.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, this.chapter.id, chapter.files[0].format));
|
||||
}
|
||||
}
|
@ -1,16 +1,14 @@
|
||||
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Edit {{tag?.title}} Collection</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||
|
||||
</button>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
<li [ngbNavItem]="tabs[TabID.General].id">
|
||||
<a ngbNavLink>{{tabs[TabID.General].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p>
|
||||
<p class="alert alert-secondary" role="alert">
|
||||
This tag is currently {{tag?.promoted ? 'promoted' : 'not promoted'}} (<i class="fa fa-angle-double-up" aria-hidden="true"></i>).
|
||||
Promotion means that the tag can be seen server-wide, not just for admin users. All series that have this tag will still have user-access restrictions placed on them.
|
||||
</p>
|
||||
@ -52,7 +50,8 @@
|
||||
<li [ngbNavItem]="tabs[TabID.CoverImage].id">
|
||||
<a ngbNavLink>{{tabs[TabID.CoverImage].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p class="alert alert-primary" role="alert">
|
||||
<p class="alert alert-secondary" role="alert">
|
||||
<!-- TODO: I don't think we need this anymore, it's a bit intuitive -->
|
||||
Upload and choose a new cover image. Press Save to upload and override the cover.
|
||||
</p>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)" [showReset]="tag.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
@ -24,7 +24,8 @@ enum TabID {
|
||||
@Component({
|
||||
selector: 'app-edit-collection-tags',
|
||||
templateUrl: './edit-collection-tags.component.html',
|
||||
styleUrls: ['./edit-collection-tags.component.scss']
|
||||
styleUrls: ['./edit-collection-tags.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EditCollectionTagsComponent implements OnInit {
|
||||
|
||||
@ -58,7 +59,7 @@ export class EditCollectionTagsComponent implements OnInit {
|
||||
private collectionService: CollectionTagService, private toastr: ToastrService,
|
||||
private confirmSerivce: ConfirmService, private libraryService: LibraryService,
|
||||
private imageService: ImageService, private uploadService: UploadService,
|
||||
public utilityService: UtilityService) { }
|
||||
public utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.pagination == undefined) {
|
||||
@ -97,6 +98,7 @@ export class EditCollectionTagsComponent implements OnInit {
|
||||
this.isLoading = false;
|
||||
|
||||
this.libraryNames = results[1];
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -108,15 +110,18 @@ export class EditCollectionTagsComponent implements OnInit {
|
||||
} else if (numberOfSelected == this.series.length) {
|
||||
this.selectAll = true;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
togglePromotion() {
|
||||
const originalPromotion = this.tag.promoted;
|
||||
this.tag.promoted = !this.tag.promoted;
|
||||
this.cdRef.markForCheck();
|
||||
this.collectionService.updateTag(this.tag).subscribe(res => {
|
||||
this.toastr.success('Tag updated successfully');
|
||||
}, err => {
|
||||
this.tag.promoted = originalPromotion;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -144,7 +149,7 @@ export class EditCollectionTagsComponent implements OnInit {
|
||||
];
|
||||
|
||||
if (selectedIndex > 0) {
|
||||
apis.push(this.uploadService.updateCollectionCoverImage(this.tag.id, this.selectedCover))
|
||||
apis.push(this.uploadService.updateCollectionCoverImage(this.tag.id, this.selectedCover));
|
||||
}
|
||||
|
||||
forkJoin(apis).subscribe(results => {
|
||||
@ -161,12 +166,14 @@ export class EditCollectionTagsComponent implements OnInit {
|
||||
|
||||
updateSelectedImage(url: string) {
|
||||
this.selectedCover = url;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
this.collectionTagForm.patchValue({
|
||||
coverImageLocked: false
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { forkJoin, Observable, of, Subject } from 'rxjs';
|
||||
@ -34,7 +34,8 @@ enum TabID {
|
||||
@Component({
|
||||
selector: 'app-edit-series-modal',
|
||||
templateUrl: './edit-series-modal.component.html',
|
||||
styleUrls: ['./edit-series-modal.component.scss']
|
||||
styleUrls: ['./edit-series-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -109,7 +110,8 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
private libraryService: LibraryService,
|
||||
private collectionService: CollectionTagService,
|
||||
private uploadService: UploadService,
|
||||
private metadataService: MetadataService) { }
|
||||
private metadataService: MetadataService,
|
||||
private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.imageUrls.push(this.imageService.getSeriesCoverImage(this.series.id));
|
||||
@ -136,19 +138,23 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
publicationStatus: new FormControl('', []),
|
||||
language: new FormControl('', []),
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
||||
this.metadataService.getAllAgeRatings().subscribe(ratings => {
|
||||
this.ageRatings = ratings;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.metadataService.getAllPublicationStatus().subscribe(statuses => {
|
||||
this.publicationStatuses = statuses;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.metadataService.getAllValidLanguages().subscribe(validLanguages => {
|
||||
this.validLanguages = validLanguages;
|
||||
})
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.seriesService.getMetadata(this.series.id).subscribe(metadata => {
|
||||
if (metadata) {
|
||||
@ -159,38 +165,46 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
this.editSeriesForm.get('ageRating')?.patchValue(this.metadata.ageRating);
|
||||
this.editSeriesForm.get('publicationStatus')?.patchValue(this.metadata.publicationStatus);
|
||||
this.editSeriesForm.get('language')?.patchValue(this.metadata.language);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.editSeriesForm.get('name')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
this.series.nameLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editSeriesForm.get('sortName')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
this.series.sortNameLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editSeriesForm.get('localizedName')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
this.series.localizedNameLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editSeriesForm.get('summary')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
this.metadata.summaryLocked = true;
|
||||
this.metadata.summary = val;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
|
||||
this.editSeriesForm.get('ageRating')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
this.metadata.ageRating = parseInt(val + '', 10);
|
||||
this.metadata.ageRatingLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editSeriesForm.get('publicationStatus')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
this.metadata.publicationStatus = parseInt(val + '', 10);
|
||||
this.metadata.publicationStatusLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.isLoadingVolumes = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.seriesService.getVolumes(this.series.id).subscribe(volumes => {
|
||||
this.seriesVolumes = volumes;
|
||||
this.isLoadingVolumes = false;
|
||||
@ -204,6 +218,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
return f;
|
||||
})).flat();
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -221,6 +236,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
this.setupLanguageTypeahead()
|
||||
]).subscribe(results => {
|
||||
this.collectionTags = this.metadata.collectionTags;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -441,8 +457,6 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.saveNestedComponents.emit();
|
||||
|
||||
|
||||
|
||||
forkJoin(apis).subscribe(results => {
|
||||
this.modal.close({success: true, series: model, coverImageUpdate: selectedIndex > 0});
|
||||
});
|
||||
@ -450,16 +464,19 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
|
||||
updateCollections(tags: CollectionTag[]) {
|
||||
this.collectionTags = tags;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateTags(tags: Tag[]) {
|
||||
this.tags = tags;
|
||||
this.metadata.tags = tags;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateGenres(genres: Genre[]) {
|
||||
this.genres = genres;
|
||||
this.metadata.genres = genres;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateLanguage(language: Array<Language>) {
|
||||
@ -468,6 +485,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
this.metadata.language = language[0].isoCode;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updatePerson(persons: Person[], role: PersonRole) {
|
||||
@ -501,18 +519,20 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
break;
|
||||
case PersonRole.Translator:
|
||||
this.metadata.translators = persons;
|
||||
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateSelectedIndex(index: number) {
|
||||
this.editSeriesForm.patchValue({
|
||||
coverImageIndex: index
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateSelectedImage(url: string) {
|
||||
this.selectedCover = url;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
@ -520,12 +540,14 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
this.editSeriesForm.patchValue({
|
||||
coverImageLocked: false
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
unlock(b: any, field: string) {
|
||||
if (b) {
|
||||
b[field] = !b[field];
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,27 @@
|
||||
<div *ngIf="bulkSelectionService.hasSelections()" class="bulk-select mb-3 fixed-top" [ngStyle]="{'margin-top': topOffset + 'px'}">
|
||||
<div class="d-flex justify-content-around align-items-center">
|
||||
<span class="highlight"><i class="fa fa-check" aria-hidden="true"></i> {{bulkSelectionService.totalSelections()}} selected</span>
|
||||
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
<span id="bulk-actions-header" class="visually-hidden">Bulk Actions</span>
|
||||
<button class="btn btn-icon" (click)="bulkSelectionService.deselectAll()"><i class="fa fa-times" aria-hidden="true"></i> Deselect All</button>
|
||||
<ng-container *ngIf="bulkSelectionService.selections$ | async as selectionCount">
|
||||
<div *ngIf="selectionCount > 0" class="bulk-select mb-3 fixed-top" [ngStyle]="{'margin-top': topOffset + 'px'}">
|
||||
<div class="d-flex justify-content-around align-items-center">
|
||||
|
||||
<span class="highlight">
|
||||
<i class="fa fa-check me-1" aria-hidden="true"></i>
|
||||
{{selectionCount}} items selected
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<button *ngIf="hasMarkAsUnread" class="btn btn-icon" (click)="executeAction(Action.MarkAsUnread)" ngbTooltip="Mark as Unread" placement="bottom">
|
||||
<i class="fa-regular fa-circle-check" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Mark as Unread</span>
|
||||
</button>
|
||||
<button *ngIf="hasMarkAsRead" class="btn btn-icon" (click)="executeAction(Action.MarkAsRead)" ngbTooltip="Mark as Read" placement="bottom">
|
||||
<i class="fa-solid fa-circle-check" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Mark as Read</span>
|
||||
</button>
|
||||
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
</span>
|
||||
|
||||
<span id="bulk-actions-header" class="visually-hidden">Bulk Actions</span>
|
||||
|
||||
<button class="btn btn-icon" (click)="bulkSelectionService.deselectAll()"><i class="fa fa-times" aria-hidden="true"></i> Deselect All</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
@ -1,28 +1,44 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { BulkSelectionService } from '../bulk-selection.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bulk-operations',
|
||||
templateUrl: './bulk-operations.component.html',
|
||||
styleUrls: ['./bulk-operations.component.scss']
|
||||
styleUrls: ['./bulk-operations.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BulkOperationsComponent implements OnInit {
|
||||
export class BulkOperationsComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() actionCallback!: (action: Action, data: any) => void;
|
||||
topOffset: number = 0;
|
||||
|
||||
get actions() {
|
||||
return this.bulkSelectionService.getActions(this.actionCallback.bind(this));
|
||||
topOffset: number = 56;
|
||||
hasMarkAsRead: boolean = false;
|
||||
hasMarkAsUnread: boolean = false;
|
||||
actions: Array<ActionItem<any>> = [];
|
||||
|
||||
private onDestory: Subject<void> = new Subject();
|
||||
|
||||
get Action() {
|
||||
return Action;
|
||||
}
|
||||
|
||||
constructor(public bulkSelectionService: BulkSelectionService) { }
|
||||
constructor(public bulkSelectionService: BulkSelectionService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
const navBar = document.querySelector('.navbar');
|
||||
if (navBar) {
|
||||
this.topOffset = Math.ceil(navBar.getBoundingClientRect().height); // TODO: We can make this fixed 63px
|
||||
}
|
||||
this.bulkSelectionService.actions$.pipe(takeUntil(this.onDestory)).subscribe(actions => {
|
||||
actions.forEach(a => a.callback = this.actionCallback.bind(this));
|
||||
this.actions = actions;
|
||||
this.hasMarkAsRead = this.actions.filter(act => act.action === Action.MarkAsRead).length > 0;
|
||||
this.hasMarkAsUnread = this.actions.filter(act => act.action === Action.MarkAsUnread).length > 0;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestory.next();
|
||||
this.onDestory.complete();
|
||||
}
|
||||
|
||||
handleActionCallback(action: Action, data: any) {
|
||||
@ -35,5 +51,10 @@ export class BulkOperationsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
executeAction(action: Action) {
|
||||
const foundActions = this.actions.filter(act => act.action === action);
|
||||
if (foundActions.length > 0) {
|
||||
this.performAction(foundActions[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ChangeDetectorRef, Injectable } from '@angular/core';
|
||||
import { NavigationStart, Router } from '@angular/router';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { Action, ActionFactoryService } from '../_services/action-factory.service';
|
||||
import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service';
|
||||
|
||||
type DataSource = 'volume' | 'chapter' | 'special' | 'series' | 'bookmark';
|
||||
|
||||
@ -23,6 +24,15 @@ export class BulkSelectionService {
|
||||
private dataSourceMax: { [key: string]: number} = {};
|
||||
public isShiftDown: boolean = false;
|
||||
|
||||
private actionsSource = new ReplaySubject<ActionItem<any>[]>(1);
|
||||
public actions$ = this.actionsSource.asObservable();
|
||||
|
||||
private selectionsSource = new ReplaySubject<number>(1);
|
||||
/**
|
||||
* Number of active selections
|
||||
*/
|
||||
public selections$ = this.selectionsSource.asObservable();
|
||||
|
||||
constructor(private router: Router, private actionFactory: ActionFactoryService) {
|
||||
router.events
|
||||
.pipe(filter(event => event instanceof NavigationStart))
|
||||
@ -61,6 +71,7 @@ export class BulkSelectionService {
|
||||
this.prevIndex = index;
|
||||
this.prevDataSource = dataSource;
|
||||
this.dataSourceMax[dataSource] = maxIndex;
|
||||
this.actionsSource.next(this.getActions(() => {}));
|
||||
}
|
||||
|
||||
isCardSelected(dataSource: DataSource, index: number) {
|
||||
@ -77,6 +88,7 @@ export class BulkSelectionService {
|
||||
|
||||
if (from === to) {
|
||||
this.selectedCards[dataSource][to] = value;
|
||||
this.selectionsSource.next(this.totalSelections());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -89,10 +101,12 @@ export class BulkSelectionService {
|
||||
for (let i = from; i <= to; i++) {
|
||||
this.selectedCards[dataSource][i] = value;
|
||||
}
|
||||
this.selectionsSource.next(this.totalSelections());
|
||||
}
|
||||
|
||||
deselectAll() {
|
||||
this.selectedCards = {};
|
||||
this.selectionsSource.next(0);
|
||||
}
|
||||
|
||||
hasSelections() {
|
||||
|
@ -95,7 +95,7 @@
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Cover]">
|
||||
<li [ngbNavItem]="tabs[TabID.Cover]" [disabled]="!(isAdmin$ | async)">
|
||||
<a ngbNavLink>{{tabs[TabID.Cover].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls"
|
||||
@ -115,7 +115,7 @@
|
||||
<ul class="list-unstyled">
|
||||
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{utilityService.formatChapterName(libraryType, true, false)}} {{formatChapterNumber(chapter)}}">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="chapter.coverImage"></app-image>
|
||||
<app-image class="me-2" width="74px" [imageUrl]="imageService.getChapterCoverImage(chapter.id)"></app-image>
|
||||
</a>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1">
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbActiveOffcanvas } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { finalize, Observable, take, takeWhile } from 'rxjs';
|
||||
import { finalize, Observable, of, Subject, take, takeWhile, takeUntil, map, shareReplay } from 'rxjs';
|
||||
import { Download } from 'src/app/shared/_models/download';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
@ -33,9 +33,10 @@ enum TabID {
|
||||
@Component({
|
||||
selector: 'app-card-detail-drawer',
|
||||
templateUrl: './card-detail-drawer.component.html',
|
||||
styleUrls: ['./card-detail-drawer.component.scss']
|
||||
styleUrls: ['./card-detail-drawer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CardDetailDrawerComponent implements OnInit {
|
||||
export class CardDetailDrawerComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() parentName = '';
|
||||
@Input() seriesId: number = 0;
|
||||
@ -55,6 +56,8 @@ export class CardDetailDrawerComponent implements OnInit {
|
||||
*/
|
||||
coverImageUrl!: string;
|
||||
|
||||
isAdmin$: Observable<boolean> = of(false);
|
||||
|
||||
|
||||
actions: ActionItem<any>[] = [];
|
||||
chapterActions: ActionItem<Chapter>[] = [];
|
||||
@ -70,7 +73,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
||||
download$: Observable<Download> | null = null;
|
||||
downloadInProgress: boolean = false;
|
||||
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
get MangaFormat() {
|
||||
return MangaFormat;
|
||||
@ -97,59 +100,56 @@ export class CardDetailDrawerComponent implements OnInit {
|
||||
private accountService: AccountService, private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService, private router: Router, private libraryService: LibraryService,
|
||||
private seriesService: SeriesService, private readerService: ReaderService, public metadataService: MetadataService,
|
||||
public activeOffcanvas: NgbActiveOffcanvas, private downloadService: DownloadService) { }
|
||||
public activeOffcanvas: NgbActiveOffcanvas, private downloadService: DownloadService, private readonly cdRef: ChangeDetectorRef) {
|
||||
this.isAdmin$ = this.accountService.currentUser$.pipe(
|
||||
takeUntil(this.onDestroy),
|
||||
map(user => (user && this.accountService.hasAdminRole(user)) || false),
|
||||
shareReplay()
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.utilityService.isChapter(this.data);
|
||||
|
||||
this.chapter = this.utilityService.isChapter(this.data) ? (this.data as Chapter) : (this.data as Volume).chapters[0];
|
||||
if (this.isChapter) {
|
||||
this.coverImageUrl = this.imageService.getChapterCoverImage(this.data.id);
|
||||
} else {
|
||||
this.coverImageUrl = this.imageService.getVolumeCoverImage(this.data.id);
|
||||
}
|
||||
|
||||
this.imageUrls.push(this.imageService.getChapterCoverImage(this.chapter.id));
|
||||
|
||||
this.seriesService.getChapterMetadata(this.chapter.id).subscribe(metadata => {
|
||||
this.chapterMetadata = metadata;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
|
||||
if (this.isChapter) {
|
||||
this.coverImageUrl = this.imageService.getChapterCoverImage(this.data.id);
|
||||
this.summary = this.utilityService.asChapter(this.data).summary || '';
|
||||
this.chapters.push(this.data as Chapter);
|
||||
} else {
|
||||
this.coverImageUrl = this.imageService.getVolumeCoverImage(this.data.id);
|
||||
this.summary = this.utilityService.asVolume(this.data).chapters[0].summary || '';
|
||||
this.chapters.push(...(this.data as Volume).chapters);
|
||||
}
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
if (!this.accountService.hasAdminRole(user)) {
|
||||
this.tabs.find(s => s.title === 'Cover')!.disabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this))
|
||||
.filter(item => item.action !== Action.Edit);
|
||||
this.chapterActions.push({title: 'Read', action: Action.Read, callback: this.handleChapterActionCallback.bind(this), requiresAdmin: false});
|
||||
|
||||
this.libraryService.getLibraryType(this.libraryId).subscribe(type => {
|
||||
this.libraryType = type;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)).filter(item => item.action !== Action.Edit);
|
||||
this.chapterActions.push({title: 'Read', action: Action.Read, callback: this.handleChapterActionCallback.bind(this), requiresAdmin: false});
|
||||
|
||||
if (this.isChapter) {
|
||||
this.chapters.push(this.data as Chapter);
|
||||
} else if (!this.isChapter) {
|
||||
this.chapters.push(...(this.data as Volume).chapters);
|
||||
}
|
||||
// TODO: Move this into the backend
|
||||
this.chapters.sort(this.utilityService.sortChapters);
|
||||
this.chapters.forEach(c => c.coverImage = this.imageService.getChapterCoverImage(c.id));
|
||||
// Try to show an approximation of the reading order for files
|
||||
|
||||
var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||
this.chapters.forEach((c: Chapter) => {
|
||||
c.files.sort((a: MangaFile, b: MangaFile) => collator.compare(a.filePath, b.filePath));
|
||||
});
|
||||
|
||||
this.imageUrls = this.chapters.map(c => this.imageService.getChapterCoverImage(c.id));
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
close() {
|
||||
@ -184,7 +184,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsRead(this.seriesId, chapter, () => { /* No Action */ });
|
||||
this.actionService.markChapterAsRead(this.seriesId, chapter, () => { this.cdRef.markForCheck(); });
|
||||
}
|
||||
|
||||
markChapterAsUnread(chapter: Chapter) {
|
||||
@ -192,7 +192,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsUnread(this.seriesId, chapter, () => { /* No Action */ });
|
||||
this.actionService.markChapterAsUnread(this.seriesId, chapter, () => { this.cdRef.markForCheck(); });
|
||||
}
|
||||
|
||||
handleChapterActionCallback(action: Action, chapter: Chapter) {
|
||||
@ -249,8 +249,10 @@ export class CardDetailDrawerComponent implements OnInit {
|
||||
finalize(() => {
|
||||
this.download$ = null;
|
||||
this.downloadInProgress = false;
|
||||
this.cdRef.markForCheck();
|
||||
}));
|
||||
this.download$.subscribe(() => {});
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ActionItem } from 'src/app/_services/action-factory.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-actionables',
|
||||
templateUrl: './card-actionables.component.html',
|
||||
styleUrls: ['./card-actionables.component.scss']
|
||||
styleUrls: ['./card-actionables.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CardActionablesComponent implements OnInit {
|
||||
|
||||
@ -19,11 +20,12 @@ export class CardActionablesComponent implements OnInit {
|
||||
nonAdminActions: ActionItem<any>[] = [];
|
||||
|
||||
|
||||
constructor() { }
|
||||
constructor(private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.nonAdminActions = this.actions.filter(item => !item.requiresAdmin);
|
||||
this.adminActions = this.actions.filter(item => item.requiresAdmin);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
preventClick(event: any) {
|
||||
|
@ -132,7 +132,8 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
||||
constructor(public imageService: ImageService, private libraryService: LibraryService,
|
||||
public utilityService: UtilityService, private downloadService: DownloadService,
|
||||
private toastr: ToastrService, public bulkSelectionService: BulkSelectionService,
|
||||
private messageHub: MessageHubService, private accountService: AccountService, private scrollService: ScrollService, private changeDetectionRef: ChangeDetectorRef) {}
|
||||
private messageHub: MessageHubService, private accountService: AccountService,
|
||||
private scrollService: ScrollService, private readonly changeDetectionRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) {
|
||||
@ -143,6 +144,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
||||
if (this.suppressLibraryLink === false) {
|
||||
if (this.entity !== undefined && this.entity.hasOwnProperty('libraryId')) {
|
||||
this.libraryId = (this.entity as Series).libraryId;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}
|
||||
|
||||
if (this.libraryId !== undefined && this.libraryId > 0) {
|
||||
@ -175,7 +177,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
||||
if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return;
|
||||
|
||||
this.read = updateEvent.pagesRead;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
this.changeDetectionRef.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
@ -189,6 +191,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
||||
if (!this.allowSelection) return;
|
||||
|
||||
this.selectionInProgress = false;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}
|
||||
|
||||
@HostListener('touchstart', ['$event'])
|
||||
@ -304,5 +307,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
||||
event.stopPropagation();
|
||||
}
|
||||
this.selection.emit(this.selected);
|
||||
this.changeDetectionRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SeriesCardComponent } from './series-card/series-card.component';
|
||||
import { LibraryCardComponent } from './library-card/library-card.component';
|
||||
import { CoverImageChooserComponent } from './cover-image-chooser/cover-image-chooser.component';
|
||||
import { EditSeriesModalComponent } from './_modals/edit-series-modal/edit-series-modal.component';
|
||||
import { EditCollectionTagsComponent } from './_modals/edit-collection-tags/edit-collection-tags.component';
|
||||
@ -14,12 +13,10 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TypeaheadModule } from '../typeahead/typeahead.module';
|
||||
import { CardDetailLayoutComponent } from './card-detail-layout/card-detail-layout.component';
|
||||
import { CardDetailsModalComponent } from './_modals/card-details-modal/card-details-modal.component';
|
||||
import { BulkOperationsComponent } from './bulk-operations/bulk-operations.component';
|
||||
import { BulkAddToCollectionComponent } from './_modals/bulk-add-to-collection/bulk-add-to-collection.component';
|
||||
import { PipeModule } from '../pipe/pipe.module';
|
||||
import { ChapterMetadataDetailComponent } from './chapter-metadata-detail/chapter-metadata-detail.component';
|
||||
import { FileInfoComponent } from './file-info/file-info.component';
|
||||
import { MetadataFilterModule } from '../metadata-filter/metadata-filter.module';
|
||||
import { EditSeriesRelationComponent } from './edit-series-relation/edit-series-relation.component';
|
||||
import { CardDetailDrawerComponent } from './card-detail-drawer/card-detail-drawer.component';
|
||||
@ -36,17 +33,14 @@ import { SeriesInfoCardsComponent } from './series-info-cards/series-info-cards.
|
||||
declarations: [
|
||||
CardItemComponent,
|
||||
SeriesCardComponent,
|
||||
LibraryCardComponent,
|
||||
CoverImageChooserComponent,
|
||||
EditSeriesModalComponent,
|
||||
EditCollectionTagsComponent,
|
||||
CardActionablesComponent,
|
||||
CardDetailLayoutComponent,
|
||||
CardDetailsModalComponent,
|
||||
BulkOperationsComponent,
|
||||
BulkAddToCollectionComponent,
|
||||
ChapterMetadataDetailComponent,
|
||||
FileInfoComponent,
|
||||
EditSeriesRelationComponent,
|
||||
CardDetailDrawerComponent,
|
||||
EntityTitleComponent,
|
||||
@ -89,15 +83,12 @@ import { SeriesInfoCardsComponent } from './series-info-cards/series-info-cards.
|
||||
exports: [
|
||||
CardItemComponent,
|
||||
SeriesCardComponent,
|
||||
LibraryCardComponent,
|
||||
SeriesCardComponent,
|
||||
LibraryCardComponent,
|
||||
CoverImageChooserComponent,
|
||||
EditSeriesModalComponent,
|
||||
EditCollectionTagsComponent,
|
||||
CardActionablesComponent,
|
||||
CardDetailLayoutComponent,
|
||||
CardDetailsModalComponent,
|
||||
BulkOperationsComponent,
|
||||
ChapterMetadataDetailComponent,
|
||||
EditSeriesRelationComponent,
|
||||
|
@ -1,43 +1,16 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { ChapterMetadata } from 'src/app/_models/chapter-metadata';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { PersonRole } from 'src/app/_models/person';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chapter-metadata-detail',
|
||||
templateUrl: './chapter-metadata-detail.component.html',
|
||||
styleUrls: ['./chapter-metadata-detail.component.scss']
|
||||
styleUrls: ['./chapter-metadata-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ChapterMetadataDetailComponent implements OnInit {
|
||||
@Input() chapter: ChapterMetadata | undefined;
|
||||
|
||||
@Input() chapter!: ChapterMetadata;
|
||||
@Input() libraryType: LibraryType = LibraryType.Manga;
|
||||
constructor() { }
|
||||
|
||||
roles: string[] = [];
|
||||
|
||||
get LibraryType(): typeof LibraryType {
|
||||
return LibraryType;
|
||||
}
|
||||
|
||||
constructor(public utilityService: UtilityService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.roles = Object.keys(PersonRole).filter(role => /[0-9]/.test(role) === false);
|
||||
}
|
||||
|
||||
getPeople(role: string) {
|
||||
if (this.chapter) {
|
||||
return (this.chapter as any)[role.toLowerCase()];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<Chapter>, chapter: Chapter) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, chapter);
|
||||
}
|
||||
}
|
||||
ngOnInit(): void {}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container-fluid" style="padding-left: 0px; padding-right: 0px">
|
||||
<form [formGroup]="form">
|
||||
<ngx-file-drop (onFileDrop)="dropped($event)"
|
||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" accept=".png,.jpg,.jpeg" [directory]="false" dropZoneClassName="file-upload" contentClassName="file-upload-zone" [directory]="false">
|
||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" [accept]="acceptableExtensions" [directory]="false" dropZoneClassName="file-upload" contentClassName="file-upload-zone" [directory]="false">
|
||||
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
|
||||
<div class="row g-0 mt-3 pb-3" *ngIf="mode === 'all'">
|
||||
<div class="mx-auto">
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||
import { NgxFileDropEntry, FileSystemFileEntry } from 'ngx-file-drop';
|
||||
import { fromEvent, Subject } from 'rxjs';
|
||||
@ -14,7 +14,8 @@ export type SelectCoverFunction = (selectedCover: string) => void;
|
||||
@Component({
|
||||
selector: 'app-cover-image-chooser',
|
||||
templateUrl: './cover-image-chooser.component.html',
|
||||
styleUrls: ['./cover-image-chooser.component.scss']
|
||||
styleUrls: ['./cover-image-chooser.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -58,17 +59,19 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
||||
appliedIndex: number = 0;
|
||||
form!: FormGroup;
|
||||
files: NgxFileDropEntry[] = [];
|
||||
acceptableExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp'].join(',');
|
||||
|
||||
mode: 'file' | 'url' | 'all' = 'all';
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
constructor(public imageService: ImageService, private fb: FormBuilder, private toastr: ToastrService, private uploadService: UploadService,
|
||||
@Inject(DOCUMENT) private document: Document) { }
|
||||
@Inject(DOCUMENT) private document: Document, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form = this.fb.group({
|
||||
coverImageUrl: new FormControl('', [])
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@ -86,13 +89,14 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0);
|
||||
var dataURL = canvas.toDataURL("image/png");
|
||||
const dataURL = canvas.toDataURL("image/png");
|
||||
return dataURL;
|
||||
}
|
||||
|
||||
selectImage(index: number) {
|
||||
if (this.selectedIndex === index) { return; }
|
||||
this.selectedIndex = index;
|
||||
this.cdRef.markForCheck();
|
||||
this.imageSelected.emit(this.selectedIndex);
|
||||
this.selectedBase64Url.emit(this.imageUrls[this.selectedIndex]);
|
||||
}
|
||||
@ -101,6 +105,7 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
||||
if (this.showApplyButton) {
|
||||
this.applyCover.emit(this.imageUrls[index]);
|
||||
this.appliedIndex = index;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,25 +117,27 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
||||
|
||||
loadImage() {
|
||||
const url = this.form.get('coverImageUrl')?.value.trim();
|
||||
if (url && url != '') {
|
||||
if (!url && url === '') return;
|
||||
|
||||
this.uploadService.uploadByUrl(url).subscribe(filename => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = this.imageService.getCoverUploadImage(filename);
|
||||
img.onload = (e) => this.handleUrlImageAdd(img);
|
||||
img.onerror = (e) => {
|
||||
this.toastr.error('The image could not be fetched due to server refusing request. Please download and upload from file instead.');
|
||||
this.form.get('coverImageUrl')?.setValue('');
|
||||
};
|
||||
this.uploadService.uploadByUrl(url).subscribe(filename => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = this.imageService.getCoverUploadImage(filename);
|
||||
img.onload = (e) => this.handleUrlImageAdd(img);
|
||||
img.onerror = (e) => {
|
||||
this.toastr.error('The image could not be fetched due to server refusing request. Please download and upload from file instead.');
|
||||
this.form.get('coverImageUrl')?.setValue('');
|
||||
});
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
};
|
||||
this.form.get('coverImageUrl')?.setValue('');
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
changeMode(mode: 'url') {
|
||||
this.mode = mode;
|
||||
this.setupEnterHandler();
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
setTimeout(() => (this.document.querySelector('#load-image') as HTMLInputElement)?.focus(), 10);
|
||||
}
|
||||
@ -159,12 +166,14 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
||||
this.selectedIndex += 1;
|
||||
this.imageSelected.emit(this.selectedIndex); // Auto select newly uploaded image
|
||||
this.selectedBase64Url.emit(e.target.result);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
handleUrlImageAdd(img: HTMLImageElement) {
|
||||
const url = this.getBase64Image(img);
|
||||
this.imageUrls.push(url);
|
||||
this.imageUrlsChange.emit(this.imageUrls);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
setTimeout(() => {
|
||||
// Auto select newly uploaded image and tell parent of new base64 url
|
||||
|
@ -12,7 +12,7 @@
|
||||
<form>
|
||||
<div class="row g-0 mb-3" *ngFor="let relation of relations; let idx = index; let isLast = last;">
|
||||
<div class="col-sm-12 col-md-7">
|
||||
<app-typeahead (selectedData)="updateSeries($event, relation)" [settings]="relation.typeaheadSettings">
|
||||
<app-typeahead (selectedData)="updateSeries($event, relation)" [settings]="relation.typeaheadSettings" id="relation--{{idx}}">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}} ({{libraryNames[item.libraryId]}})
|
||||
</ng-template>
|
||||
@ -23,6 +23,7 @@
|
||||
</div>
|
||||
<div class="col-sm-auto col-md-3">
|
||||
<select class="form-select" [formControl]="relation.formControl">
|
||||
<option [value]="RelationKind.Parent" disabled>Parent</option>
|
||||
<option *ngFor="let opt of relationOptions" [value]="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { map, Subject, Observable, of, firstValueFrom, takeUntil, ReplaySubject } from 'rxjs';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
@ -19,7 +19,8 @@ interface RelationControl {
|
||||
@Component({
|
||||
selector: 'app-edit-series-relation',
|
||||
templateUrl: './edit-series-relation.component.html',
|
||||
styleUrls: ['./edit-series-relation.component.scss']
|
||||
styleUrls: ['./edit-series-relation.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EditSeriesRelationComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -30,18 +31,22 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
|
||||
@Input() save: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
@Output() saveApi = new ReplaySubject(1);
|
||||
relationOptions = RelationKinds;
|
||||
|
||||
relationOptions = RelationKinds;
|
||||
relations: Array<RelationControl> = [];
|
||||
seriesSettings: TypeaheadSettings<SearchResult> = new TypeaheadSettings();
|
||||
libraryNames: {[key:number]: string} = {};
|
||||
|
||||
get RelationKind() {
|
||||
return RelationKind;
|
||||
}
|
||||
|
||||
|
||||
private onDestroy: Subject<void> = new Subject<void>();
|
||||
|
||||
constructor(private seriesService: SeriesService, private utilityService: UtilityService,
|
||||
public imageService: ImageService, private libraryService: LibraryService) { }
|
||||
public imageService: ImageService, private libraryService: LibraryService,
|
||||
private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seriesService.getRelatedForSeries(this.series.id).subscribe(async relations => {
|
||||
@ -57,10 +62,12 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
|
||||
this.setupRelationRows(relations.doujinshis, RelationKind.Doujinshi);
|
||||
this.setupRelationRows(relations.contains, RelationKind.Contains);
|
||||
this.setupRelationRows(relations.parent, RelationKind.Parent);
|
||||
this.cdRef.detectChanges();
|
||||
});
|
||||
|
||||
this.libraryService.getLibraryNames().subscribe(names => {
|
||||
this.libraryNames = names;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.save.pipe(takeUntil(this.onDestroy)).subscribe(() => this.saveState());
|
||||
@ -74,27 +81,43 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
|
||||
setupRelationRows(relations: Array<Series>, kind: RelationKind) {
|
||||
relations.map(async item => {
|
||||
const settings = await firstValueFrom(this.createSeriesTypeahead(item, kind));
|
||||
return {series: item, typeaheadSettings: settings, formControl: new FormControl(kind, [])}
|
||||
const form = new FormControl(kind, []);
|
||||
if (kind === RelationKind.Parent) {
|
||||
form.disable();
|
||||
}
|
||||
return {series: item, typeaheadSettings: settings, formControl: form};
|
||||
}).forEach(async p => {
|
||||
this.relations.push(await p);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
async addNewRelation() {
|
||||
this.relations.push({series: undefined, formControl: new FormControl(RelationKind.Adaptation, []), typeaheadSettings: await firstValueFrom(this.createSeriesTypeahead(undefined, RelationKind.Adaptation))});
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
// Focus on the new typeahead
|
||||
setTimeout(() => {
|
||||
const typeahead = document.querySelector(`#relation--${this.relations.length - 1} .typeahead-input input`) as HTMLInputElement;
|
||||
if (typeahead) typeahead.focus();
|
||||
}, 10);
|
||||
|
||||
}
|
||||
|
||||
removeRelation(index: number) {
|
||||
this.relations.splice(index, 1);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
updateSeries(event: Array<SearchResult | undefined>, relation: RelationControl) {
|
||||
if (event[0] === undefined) {
|
||||
relation.series = undefined;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
relation.series = {id: event[0].seriesId, name: event[0].name};
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
createSeriesTypeahead(series: Series | undefined, relationship: RelationKind): Observable<TypeaheadSettings<SearchResult>> {
|
||||
@ -143,8 +166,6 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
|
||||
const doujinshis = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Doujinshi && item.series !== undefined).map(item => item.series!.id);
|
||||
|
||||
// TODO: We can actually emit this onto an observable and in main parent, use mergeMap into the forkJoin
|
||||
|
||||
//this.saveApi.next(this.seriesService.updateRelationships(this.series.id, adaptations, characters, contains, others, prequels, sequels, sideStories, spinOffs, alternativeSettings, alternativeVersions, doujinshis));
|
||||
this.seriesService.updateRelationships(this.series.id, adaptations, characters, contains, others, prequels, sequels, sideStories, spinOffs, alternativeSettings, alternativeVersions, doujinshis).subscribe(() => {});
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
@ -13,7 +13,8 @@ import { SeriesService } from 'src/app/_services/series.service';
|
||||
@Component({
|
||||
selector: 'app-entity-info-cards',
|
||||
templateUrl: './entity-info-cards.component.html',
|
||||
styleUrls: ['./entity-info-cards.component.scss']
|
||||
styleUrls: ['./entity-info-cards.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EntityInfoCardsComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -51,7 +52,7 @@ export class EntityInfoCardsComponent implements OnInit, OnDestroy {
|
||||
return AgeRating;
|
||||
}
|
||||
|
||||
constructor(private utilityService: UtilityService, private seriesService: SeriesService) {}
|
||||
constructor(private utilityService: UtilityService, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.utilityService.isChapter(this.entity);
|
||||
@ -61,6 +62,7 @@ export class EntityInfoCardsComponent implements OnInit, OnDestroy {
|
||||
if (this.includeMetadata) {
|
||||
this.seriesService.getChapterMetadata(this.chapter.id).subscribe(metadata => {
|
||||
this.chapterMetadata = metadata;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -86,11 +88,11 @@ export class EntityInfoCardsComponent implements OnInit, OnDestroy {
|
||||
this.readingTime.maxHours = vol.maxHoursToRead;
|
||||
this.readingTime.avgHours = vol.avgHoursToRead;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
@ -7,7 +7,8 @@ import { Volume } from 'src/app/_models/volume';
|
||||
@Component({
|
||||
selector: 'app-entity-title',
|
||||
templateUrl: './entity-title.component.html',
|
||||
styleUrls: ['./entity-title.component.scss']
|
||||
styleUrls: ['./entity-title.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EntityTitleComponent implements OnInit {
|
||||
|
||||
@ -27,7 +28,6 @@ export class EntityTitleComponent implements OnInit {
|
||||
@Input() prioritizeTitleName: boolean = true;
|
||||
|
||||
isChapter = false;
|
||||
chapter!: Chapter;
|
||||
titleName: string = '';
|
||||
volumeTitle: string = '';
|
||||
|
||||
@ -38,7 +38,7 @@ export class EntityTitleComponent implements OnInit {
|
||||
|
||||
|
||||
|
||||
constructor(private utilityService: UtilityService) {
|
||||
constructor(private utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -53,5 +53,6 @@ export class EntityTitleComponent implements OnInit {
|
||||
this.volumeTitle = v.name || '';
|
||||
this.titleName = v.chapters[0].titleName || '';
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
<li class="list-group-item">
|
||||
<span>{{file.filePath}}</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
Pages: {{file.pages}}
|
||||
</div>
|
||||
<div class="col" *ngIf="created != undefined">
|
||||
Added: {{(created | date: 'short') || '-'}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
@ -1,25 +0,0 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { MangaFile } from 'src/app/_models/manga-file';
|
||||
|
||||
@Component({
|
||||
selector: 'app-file-info',
|
||||
templateUrl: './file-info.component.html',
|
||||
styleUrls: ['./file-info.component.scss']
|
||||
})
|
||||
export class FileInfoComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* MangaFile to display
|
||||
*/
|
||||
@Input() file!: MangaFile;
|
||||
/**
|
||||
* DateTime the entity this file belongs to was created
|
||||
*/
|
||||
@Input() created: string | undefined = undefined;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
<ng-container *ngIf="data !== undefined">
|
||||
<div class="card" style="width: 18rem;">
|
||||
<div class="overlay" (click)="handleClick()">
|
||||
<i class="fa {{icon}} card-img-top text-center" aria-hidden="true"></i>
|
||||
<div class="card-actions">
|
||||
<app-card-actionables [actions]="actions" [labelBy]="data.name" iconClass="fa-ellipsis-v" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body text-center" *ngIf="data.name.length > 0 || actions.length > 0">
|
||||
<span class="card-data.name" (click)="handleClick()">
|
||||
{{data.name}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
@ -1,56 +0,0 @@
|
||||
|
||||
.card {
|
||||
margin: 10px;
|
||||
max-width: 160px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin-top: 5px;
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: normal;
|
||||
max-width: 150px;
|
||||
height: 52px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
height: 160px;
|
||||
margin-top: 40% !important;
|
||||
margin: auto;
|
||||
font-size: 52px;
|
||||
|
||||
}
|
||||
|
||||
.overlay {
|
||||
height: 160px;
|
||||
&:hover {
|
||||
visibility: visible;
|
||||
|
||||
.overlay-item {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-item {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
position: absolute;
|
||||
top: 125px;
|
||||
right: -5px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 5px !important;
|
||||
}
|
||||
|
||||
.dropdown-toggle:after {
|
||||
content: none !important;
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { ActionService } from 'src/app/_services/action.service';
|
||||
|
||||
// Represents a library type card.
|
||||
@Component({
|
||||
selector: 'app-library-card',
|
||||
templateUrl: './library-card.component.html',
|
||||
styleUrls: ['./library-card.component.scss']
|
||||
})
|
||||
export class LibraryCardComponent implements OnInit, OnChanges {
|
||||
@Input() data!: Library;
|
||||
@Output() clicked = new EventEmitter<Library>();
|
||||
|
||||
isAdmin = false;
|
||||
actions: ActionItem<Library>[] = [];
|
||||
icon = 'fa-book-open';
|
||||
|
||||
constructor(private accountService: AccountService, private router: Router,
|
||||
private actionFactoryService: ActionFactoryService, private actionService: ActionService) {
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
ngOnChanges(changes: any) {
|
||||
if (this.data) {
|
||||
if (this.data.type === 0 || this.data.type === 1) {
|
||||
this.icon = 'fa-book-open';
|
||||
} else {
|
||||
this.icon = 'fa-book';
|
||||
}
|
||||
|
||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
handleAction(action: Action, library: Library) {
|
||||
switch (action) {
|
||||
case(Action.ScanLibrary):
|
||||
this.actionService.scanLibrary(library);
|
||||
break;
|
||||
case(Action.RefreshMetadata):
|
||||
this.actionService.refreshMetadata(library);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
performAction(action: ActionItem<Library>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, this.data);
|
||||
}
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
this.clicked.emit(this.data);
|
||||
this.router.navigate(['library', this.data?.id]);
|
||||
}
|
||||
|
||||
preventClick(event: any) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
}
|
@ -2,6 +2,8 @@ $image-height: 230px;
|
||||
$image-width: 160px;
|
||||
$triangle-size: 30px;
|
||||
|
||||
// with summary and cards, we have a height of 220px, we might want to default to 220px and let it grow from there to help with virtualization
|
||||
|
||||
.download {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { finalize, Observable, of, take, takeWhile } from 'rxjs';
|
||||
import { finalize, Observable, take, takeWhile } from 'rxjs';
|
||||
import { Download } from 'src/app/shared/_models/download';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
@ -14,7 +13,8 @@ import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
@Component({
|
||||
selector: 'app-list-item',
|
||||
templateUrl: './list-item.component.html',
|
||||
styleUrls: ['./list-item.component.scss']
|
||||
styleUrls: ['./list-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ListItemComponent implements OnInit {
|
||||
|
||||
@ -70,7 +70,6 @@ export class ListItemComponent implements OnInit {
|
||||
@Output() read: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
actionInProgress: boolean = false;
|
||||
summary$: Observable<string> = of('');
|
||||
summary: string = '';
|
||||
isChapter: boolean = false;
|
||||
|
||||
@ -84,16 +83,17 @@ export class ListItemComponent implements OnInit {
|
||||
}
|
||||
|
||||
|
||||
constructor(private utilityService: UtilityService, private downloadService: DownloadService, private toastr: ToastrService) { }
|
||||
constructor(private utilityService: UtilityService, private downloadService: DownloadService,
|
||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.isChapter = this.utilityService.isChapter(this.entity);
|
||||
if (this.isChapter) {
|
||||
this.summary = this.utilityService.asChapter(this.entity).summary || '';
|
||||
} else {
|
||||
this.summary = this.utilityService.asVolume(this.entity).chapters[0].summary || '';
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
@ -109,6 +109,7 @@ export class ListItemComponent implements OnInit {
|
||||
const wantToDownload = await this.downloadService.confirmSize(size, 'volume');
|
||||
if (!wantToDownload) { return; }
|
||||
this.downloadInProgress = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.download$ = this.downloadService.downloadVolume(volume).pipe(
|
||||
takeWhile(val => {
|
||||
return val.state != 'DONE';
|
||||
@ -116,7 +117,9 @@ export class ListItemComponent implements OnInit {
|
||||
finalize(() => {
|
||||
this.download$ = null;
|
||||
this.downloadInProgress = false;
|
||||
this.cdRef.markForCheck();
|
||||
}));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
} else if (this.utilityService.isChapter(this.entity)) {
|
||||
const chapter = this.utilityService.asChapter(this.entity);
|
||||
@ -124,6 +127,7 @@ export class ListItemComponent implements OnInit {
|
||||
const wantToDownload = await this.downloadService.confirmSize(size, 'chapter');
|
||||
if (!wantToDownload) { return; }
|
||||
this.downloadInProgress = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.download$ = this.downloadService.downloadChapter(chapter).pipe(
|
||||
takeWhile(val => {
|
||||
return val.state != 'DONE';
|
||||
@ -131,7 +135,9 @@ export class ListItemComponent implements OnInit {
|
||||
finalize(() => {
|
||||
this.download$ = null;
|
||||
this.downloadInProgress = false;
|
||||
this.cdRef.markForCheck();
|
||||
}));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
return; // Don't propagate the download from a card
|
||||
|
@ -44,33 +44,28 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
||||
*/
|
||||
@Output() selection = new EventEmitter<boolean>();
|
||||
|
||||
isAdmin = false;
|
||||
actions: ActionItem<Series>[] = [];
|
||||
imageUrl: string = '';
|
||||
onDestroy: Subject<void> = new Subject<void>();
|
||||
|
||||
constructor(private accountService: AccountService, private router: Router,
|
||||
constructor(private router: Router, private cdRef: ChangeDetectorRef,
|
||||
private seriesService: SeriesService, private toastr: ToastrService,
|
||||
private modalService: NgbModal, private imageService: ImageService,
|
||||
private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService, private changeDetectionRef: ChangeDetectorRef) {
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
private actionService: ActionService) {}
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data) {
|
||||
this.imageUrl = this.imageService.getSeriesCoverImage(this.data.id);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: any) {
|
||||
if (this.data) {
|
||||
this.actions = this.actionFactoryService.getSeriesActions((action: Action, series: Series) => this.handleSeriesActionCallback(action, series));
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,7 +115,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (closeResult.success) {
|
||||
this.seriesService.getSeries(data.id).subscribe(series => {
|
||||
this.data = series;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
this.cdRef.markForCheck();
|
||||
this.reload.emit(true);
|
||||
this.dataChanged.emit(series);
|
||||
});
|
||||
@ -150,7 +145,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.actionService.markSeriesAsUnread(series, () => {
|
||||
if (this.data) {
|
||||
this.data.pagesRead = 0;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
this.dataChanged.emit(series);
|
||||
@ -161,7 +156,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.actionService.markSeriesAsRead(series, () => {
|
||||
if (this.data) {
|
||||
this.data.pagesRead = series.pages;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
this.dataChanged.emit(series);
|
||||
});
|
||||
|
@ -1,19 +1,24 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { debounceTime, filter, map, Subject, takeUntil } from 'rxjs';
|
||||
import { FilterQueryParam } from 'src/app/shared/_services/filter-utilities.service';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { UserProgressUpdateEvent } from 'src/app/_models/events/user-progress-update-event';
|
||||
import { HourEstimateRange } from 'src/app/_models/hour-estimate-range';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { SeriesMetadata } from 'src/app/_models/series-metadata';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
|
||||
import { MetadataService } from 'src/app/_services/metadata.service';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-info-cards',
|
||||
templateUrl: './series-info-cards.component.html',
|
||||
styleUrls: ['./series-info-cards.component.scss']
|
||||
styleUrls: ['./series-info-cards.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SeriesInfoCardsComponent implements OnInit {
|
||||
export class SeriesInfoCardsComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
@Input() series!: Series;
|
||||
@Input() seriesMetadata!: SeriesMetadata;
|
||||
@ -27,6 +32,8 @@ export class SeriesInfoCardsComponent implements OnInit {
|
||||
|
||||
readingTime: HourEstimateRange = {avgHours: 0, maxHours: 0, minHours: 0};
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
get MangaFormat() {
|
||||
return MangaFormat;
|
||||
}
|
||||
@ -35,21 +42,50 @@ export class SeriesInfoCardsComponent implements OnInit {
|
||||
return FilterQueryParam;
|
||||
}
|
||||
|
||||
constructor(public utilityService: UtilityService, public metadataService: MetadataService, private readerService: ReaderService) { }
|
||||
constructor(public utilityService: UtilityService, public metadataService: MetadataService,
|
||||
private readerService: ReaderService, private readonly cdRef: ChangeDetectorRef,
|
||||
private messageHub: MessageHubService, private accountService: AccountService) {
|
||||
// Listen for progress events and re-calculate getTimeLeft
|
||||
this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate),
|
||||
map(evt => evt.payload as UserProgressUpdateEvent),
|
||||
debounceTime(500),
|
||||
takeUntil(this.onDestroy))
|
||||
.subscribe(updateEvent => {
|
||||
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => {
|
||||
if (user === undefined || user.username !== updateEvent.username) return;
|
||||
if (updateEvent.seriesId !== this.series.id) return;
|
||||
this.getReadingTimeLeft();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.series !== null) {
|
||||
if (this.showReadingTimeLeft) this.readerService.getTimeLeft(this.series.id).subscribe((timeLeft) => this.readingTimeLeft = timeLeft);
|
||||
this.getReadingTimeLeft();
|
||||
this.readingTime.minHours = this.series.minHoursToRead;
|
||||
this.readingTime.maxHours = this.series.maxHoursToRead;
|
||||
this.readingTime.avgHours = this.series.avgHoursToRead;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
handleGoTo(queryParamName: FilterQueryParam, filter: any) {
|
||||
this.goTo.emit({queryParamName, filter});
|
||||
}
|
||||
|
||||
|
||||
|
||||
private getReadingTimeLeft() {
|
||||
if (this.showReadingTimeLeft) this.readerService.getTimeLeft(this.series.id).subscribe((timeLeft) => {
|
||||
this.readingTimeLeft = timeLeft;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -13,15 +13,17 @@ export class CarouselReelComponent {
|
||||
@Input() items: any[] = [];
|
||||
@Input() title = '';
|
||||
@Input() clickableTitle: boolean = true;
|
||||
/**
|
||||
* Track by identity. By default, this has an implementation based on title, item's name, pagesRead, and index
|
||||
*/
|
||||
@Input() trackByIdentity: (index: number, item: any) => string = (index: number, item: any) => `${this.title}_${item.id}_${item?.name}_${item?.pagesRead}_${index}`;
|
||||
@Output() sectionClick = new EventEmitter<string>();
|
||||
|
||||
swiper: Swiper | undefined;
|
||||
|
||||
trackByIdentity: (index: number, item: any) => string;
|
||||
|
||||
|
||||
constructor(private readonly cdRef: ChangeDetectorRef) {
|
||||
this.trackByIdentity = (index: number, item: any) => `${this.title}_${item.id}_${item?.name}_${item?.pagesRead}_${index}`;
|
||||
}
|
||||
constructor(private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
nextPage() {
|
||||
if (this.swiper) {
|
||||
|
@ -1,19 +1,21 @@
|
||||
import { Component, EventEmitter, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
||||
import { Tag } from 'src/app/_models/tag';
|
||||
import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service';
|
||||
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-all-collections',
|
||||
templateUrl: './all-collections.component.html',
|
||||
styleUrls: ['./all-collections.component.scss']
|
||||
styleUrls: ['./all-collections.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AllCollectionsComponent implements OnInit {
|
||||
|
||||
@ -26,7 +28,8 @@ export class AllCollectionsComponent implements OnInit {
|
||||
|
||||
constructor(private collectionService: CollectionTagService, private router: Router,
|
||||
private actionFactoryService: ActionFactoryService, private modalService: NgbModal,
|
||||
private titleService: Title, private imageService: ImageService) {
|
||||
private titleService: Title, private utilityService: UtilityService,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.titleService.setTitle('Kavita - Collections');
|
||||
}
|
||||
@ -34,6 +37,7 @@ export class AllCollectionsComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
this.loadPage();
|
||||
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this));
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
@ -43,34 +47,14 @@ export class AllCollectionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
loadPage() {
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.collectionService.allTags().subscribe(tags => {
|
||||
this.collections = tags;
|
||||
this.isLoading = false;
|
||||
|
||||
const keys: {[key: string]: number} = {};
|
||||
tags.forEach(s => {
|
||||
let ch = s.title.charAt(0);
|
||||
if (/\d|\#|!|%|@|\(|\)|\^|\*/g.test(ch)) {
|
||||
ch = '#';
|
||||
}
|
||||
if (!keys.hasOwnProperty(ch)) {
|
||||
keys[ch] = 0;
|
||||
}
|
||||
keys[ch] += 1;
|
||||
});
|
||||
this.jumpbarKeys = Object.keys(keys).map(k => {
|
||||
return {
|
||||
key: k,
|
||||
size: keys[k],
|
||||
title: k.toUpperCase()
|
||||
}
|
||||
}).sort((a, b) => {
|
||||
if (a.key < b.key) return -1;
|
||||
if (a.key > b.key) return 1;
|
||||
return 0;
|
||||
});
|
||||
this.jumpbarKeys = this.utilityService.getJumpKeys(tags, (t: Tag) => t.title);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
handleCollectionActionCallback(action: Action, collectionTag: CollectionTag) {
|
||||
@ -79,9 +63,8 @@ export class AllCollectionsComponent implements OnInit {
|
||||
const modalRef = this.modalService.open(EditCollectionTagsComponent, { size: 'lg', scrollable: true });
|
||||
modalRef.componentInstance.tag = collectionTag;
|
||||
modalRef.closed.subscribe((results: {success: boolean, coverImageUpdated: boolean}) => {
|
||||
this.loadPage();
|
||||
if (results.coverImageUpdated) {
|
||||
collectionTag.coverImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(collectionTag.id));
|
||||
if (results.success) {
|
||||
this.loadPage();
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
@ -28,7 +28,7 @@
|
||||
[pagination]="seriesPagination"
|
||||
[filterSettings]="filterSettings"
|
||||
[filterOpen]="filterOpen"
|
||||
|
||||
[parentScroll]="scrollingBlock"
|
||||
[jumpBarKeys]="jumpbarKeys"
|
||||
(applyFilter)="updateFilter($event)">
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
@ -27,7 +27,8 @@ import { SeriesService } from 'src/app/_services/series.service';
|
||||
@Component({
|
||||
selector: 'app-collection-detail',
|
||||
templateUrl: './collection-detail.component.html',
|
||||
styleUrls: ['./collection-detail.component.scss']
|
||||
styleUrls: ['./collection-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -63,29 +64,34 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
case Action.AddToReadingList:
|
||||
this.actionService.addMultipleSeriesToReadingList(selectedSeries, () => {
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
case Action.AddToCollection:
|
||||
this.actionService.addMultipleSeriesToCollectionTag(selectedSeries, () => {
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
case Action.MarkAsRead:
|
||||
this.actionService.markMultipleSeriesAsRead(selectedSeries, () => {
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.loadPage();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
case Action.MarkAsUnread:
|
||||
this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => {
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.loadPage();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
case Action.Delete:
|
||||
this.actionService.deleteMultipleSeries(selectedSeries, () => {
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.loadPage();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
}
|
||||
@ -106,7 +112,8 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService,
|
||||
private modalService: NgbModal, private titleService: Title,
|
||||
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
|
||||
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document) {
|
||||
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
|
||||
const routeId = this.route.snapshot.paramMap.get('id');
|
||||
@ -121,6 +128,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
this.filterSettings.presets.collectionTags = [tagId];
|
||||
this.filterActiveCheck = this.seriesService.createSeriesFilter();
|
||||
this.filterActiveCheck.collectionTags = [tagId];
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.updateTag(tagId);
|
||||
}
|
||||
@ -172,45 +180,22 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
this.summary = (this.collectionTag.summary === null ? '' : this.collectionTag.summary).replace(/\n/g, '<br>');
|
||||
this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(this.collectionTag.id));
|
||||
this.titleService.setTitle('Kavita - ' + this.collectionTag.title + ' Collection');
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
// onPageChange(pagination: Pagination) {
|
||||
// this.filterUtilityService.updateUrlFromFilter(this.seriesPagination, undefined);
|
||||
// this.loadPage();
|
||||
// }
|
||||
|
||||
loadPage() {
|
||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.seriesService.getAllSeries(undefined, undefined, this.filter).pipe(take(1)).subscribe(series => {
|
||||
this.series = series.result;
|
||||
this.seriesPagination = series.pagination;
|
||||
|
||||
const keys: {[key: string]: number} = {};
|
||||
series.result.forEach(s => {
|
||||
let ch = s.name.charAt(0);
|
||||
if (/\d|\#|!|%|@|\(|\)|\^|\*/g.test(ch)) {
|
||||
ch = '#';
|
||||
}
|
||||
if (!keys.hasOwnProperty(ch)) {
|
||||
keys[ch] = 0;
|
||||
}
|
||||
keys[ch] += 1;
|
||||
});
|
||||
this.jumpbarKeys = Object.keys(keys).map(k => {
|
||||
return {
|
||||
key: k,
|
||||
size: keys[k],
|
||||
title: k.toUpperCase()
|
||||
}
|
||||
}).sort((a, b) => {
|
||||
if (a.key < b.key) return -1;
|
||||
if (a.key > b.key) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
this.jumpbarKeys = this.utilityService.getJumpKeys(this.series, (series: Series) => series.name);
|
||||
this.isLoading = false;
|
||||
window.scrollTo(0, 0);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,17 @@
|
||||
<app-side-nav-companion-bar>
|
||||
</app-side-nav-companion-bar>
|
||||
<ng-container *ngIf="libraries.length === 0 && !isLoading">
|
||||
<div class="mt-3">
|
||||
<div *ngIf="isAdmin" class="d-flex justify-content-center">
|
||||
<p>There are no libraries setup yet. Configure some in <a routerLink="/admin/dashboard" fragment="libraries">Server settings</a>.</p>
|
||||
|
||||
<ng-container *ngIf="libraries$ | async as libraries">
|
||||
<ng-container *ngIf="libraries.length === 0 && !isLoading">
|
||||
<div class="mt-3" *ngIf="isAdmin$ | async as isAdmin">
|
||||
<div *ngIf="isAdmin" class="d-flex justify-content-center">
|
||||
<p>There are no libraries setup yet. Configure some in <a routerLink="/admin/dashboard" fragment="libraries">Server settings</a>.</p>
|
||||
</div>
|
||||
<div *ngIf="!isAdmin" class="d-flex justify-content-center">
|
||||
<p>You haven't been granted access to any libraries.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!isAdmin" class="d-flex justify-content-center">
|
||||
<p>You haven't been granted access to any libraries.</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<app-carousel-reel [items]="inProgress" title="On Deck" (sectionClick)="handleSectionClick($event)">
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { ReplaySubject, Subject } from 'rxjs';
|
||||
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
|
||||
import { debounceTime, map, take, takeUntil, tap, shareReplay } from 'rxjs/operators';
|
||||
import { FilterQueryParam } from '../shared/_services/filter-utilities.service';
|
||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
|
||||
@ -11,7 +11,6 @@ import { RecentlyAddedItem } from '../_models/recently-added-item';
|
||||
import { Series } from '../_models/series';
|
||||
import { SortField } from '../_models/series-filter';
|
||||
import { SeriesGroup } from '../_models/series-group';
|
||||
import { User } from '../_models/user';
|
||||
import { AccountService } from '../_services/account.service';
|
||||
import { ImageService } from '../_services/image.service';
|
||||
import { LibraryService } from '../_services/library.service';
|
||||
@ -21,7 +20,8 @@ import { SeriesService } from '../_services/series.service';
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrls: ['./dashboard.component.scss']
|
||||
styleUrls: ['./dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DashboardComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -30,10 +30,10 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
@Input() libraryId: number = 0;
|
||||
|
||||
user: User | undefined;
|
||||
libraries: Library[] = [];
|
||||
isLoading = false;
|
||||
isAdmin = false;
|
||||
libraries$: Observable<Library[]> = of([]);
|
||||
isLoading = true;
|
||||
|
||||
isAdmin$: Observable<boolean> = of(false);
|
||||
|
||||
recentlyUpdatedSeries: SeriesGroup[] = [];
|
||||
inProgress: Series[] = [];
|
||||
@ -49,13 +49,15 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
constructor(public accountService: AccountService, private libraryService: LibraryService,
|
||||
private seriesService: SeriesService, private router: Router,
|
||||
private titleService: Title, public imageService: ImageService,
|
||||
private messageHub: MessageHubService) {
|
||||
private messageHub: MessageHubService, private readonly cdRef: ChangeDetectorRef) {
|
||||
|
||||
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(res => {
|
||||
if (res.event === EVENTS.SeriesAdded) {
|
||||
const seriesAddedEvent = res.payload as SeriesAddedEvent;
|
||||
|
||||
this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => {
|
||||
this.recentlyAddedSeries.unshift(series);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
} else if (res.event === EVENTS.SeriesRemoved) {
|
||||
const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
|
||||
@ -63,28 +65,34 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||
this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||
this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
|
||||
this.cdRef.markForCheck();
|
||||
} else if (res.event === EVENTS.ScanSeries) {
|
||||
// We don't have events for when series are updated, but we do get events when a scan update occurs. Refresh recentlyAdded at that time.
|
||||
this.loadRecentlyAdded$.next();
|
||||
}
|
||||
});
|
||||
|
||||
this.loadRecentlyAdded$.pipe(debounceTime(1000), takeUntil(this.onDestroy)).subscribe(() => this.loadRecentlyAdded());
|
||||
this.isAdmin$ = this.accountService.currentUser$.pipe(
|
||||
takeUntil(this.onDestroy),
|
||||
map(user => (user && this.accountService.hasAdminRole(user)) || false),
|
||||
shareReplay()
|
||||
);
|
||||
|
||||
this.loadRecentlyAdded$.pipe(debounceTime(1000), takeUntil(this.onDestroy)).subscribe(() => {
|
||||
this.loadRecentlyAdded();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.titleService.setTitle('Kavita - Dashboard');
|
||||
this.isLoading = true;
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
this.user = user;
|
||||
if (this.user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(this.user);
|
||||
this.libraryService.getLibrariesForMember().pipe(take(1)).subscribe(libraries => {
|
||||
this.libraries = libraries;
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.libraries$ = this.libraryService.getLibrariesForMember().pipe(take(1), tap((libs) => {
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
}));
|
||||
|
||||
this.reloadSeries();
|
||||
}
|
||||
@ -120,6 +128,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||
this.inProgress = updatedSeries.result;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -130,6 +139,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
|
||||
this.recentlyAddedSeries = updatedSeries.result;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -144,6 +154,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
if (this.libraryId === 0) return true;
|
||||
return group.libraryId === this.libraryId;
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, HostListener, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Subject } from 'rxjs';
|
||||
@ -24,7 +24,8 @@ import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
|
||||
@Component({
|
||||
selector: 'app-library-detail',
|
||||
templateUrl: './library-detail.component.html',
|
||||
styleUrls: ['./library-detail.component.scss']
|
||||
styleUrls: ['./library-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -58,30 +59,32 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
case Action.AddToReadingList:
|
||||
this.actionService.addMultipleSeriesToReadingList(selectedSeries, () => {
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
case Action.AddToCollection:
|
||||
this.actionService.addMultipleSeriesToCollectionTag(selectedSeries, () => {
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
case Action.MarkAsRead:
|
||||
this.actionService.markMultipleSeriesAsRead(selectedSeries, () => {
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.loadPage();
|
||||
});
|
||||
|
||||
break;
|
||||
case Action.MarkAsUnread:
|
||||
this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => {
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.loadPage();
|
||||
});
|
||||
break;
|
||||
case Action.Delete:
|
||||
this.actionService.deleteMultipleSeries(selectedSeries, () => {
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.loadPage();
|
||||
});
|
||||
break;
|
||||
}
|
||||
@ -90,7 +93,8 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService,
|
||||
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
||||
private utilityService: UtilityService, public navService: NavService, private filterUtilityService: FilterUtilitiesService) {
|
||||
private utilityService: UtilityService, public navService: NavService, private filterUtilityService: FilterUtilitiesService,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
const routeId = this.route.snapshot.paramMap.get('libraryId');
|
||||
if (routeId === null) {
|
||||
this.router.navigateByUrl('/libraries');
|
||||
@ -103,11 +107,14 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
this.libraryService.getLibraryNames().pipe(take(1)).subscribe(names => {
|
||||
this.libraryName = names[this.libraryId];
|
||||
this.titleService.setTitle('Kavita - ' + this.libraryName);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.libraryService.getJumpBar(this.libraryId).subscribe(barDetails => {
|
||||
this.jumpKeys = barDetails;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||
|
||||
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
|
||||
@ -118,6 +125,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
this.filterActiveCheck.libraries = [this.libraryId];
|
||||
|
||||
this.filterSettings.libraryDisabled = true;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -188,14 +196,18 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
if (this.filter == undefined) {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
this.filter.libraries.push(this.libraryId);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
this.loadingSeries = true;
|
||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.seriesService.getSeriesForLibrary(0, undefined, undefined, this.filter).pipe(take(1)).subscribe(series => {
|
||||
this.series = series.result;
|
||||
this.pagination = series.pagination;
|
||||
this.loadingSeries = false;
|
||||
this.cdRef.markForCheck();
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
|
||||
<ng-container *ngIf="noData">
|
||||
<p>Nothing to show here. Add some metadata to your library, read something or rate something.</p>
|
||||
<ng-container *ngIf="all$ | async as all">
|
||||
<p *ngIf="all.length === 0">Nothing to show here. Add some metadata to your library, read something or rate something.</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="onDeck$ | async as onDeck">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { map, merge, Observable, shareReplay, Subject, takeUntil } from 'rxjs';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { filter, map, merge, Observable, shareReplay, Subject, takeUntil } from 'rxjs';
|
||||
import { Genre } from 'src/app/_models/genre';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { MetadataService } from 'src/app/_services/metadata.service';
|
||||
@ -9,7 +9,8 @@ import { SeriesService } from 'src/app/_services/series.service';
|
||||
@Component({
|
||||
selector: 'app-library-recommended',
|
||||
templateUrl: './library-recommended.component.html',
|
||||
styleUrls: ['./library-recommended.component.scss']
|
||||
styleUrls: ['./library-recommended.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LibraryRecommendedComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -20,48 +21,48 @@ export class LibraryRecommendedComponent implements OnInit, OnDestroy {
|
||||
highlyRated$!: Observable<Series[]>;
|
||||
onDeck$!: Observable<Series[]>;
|
||||
rediscover$!: Observable<Series[]>;
|
||||
|
||||
moreIn$!: Observable<Series[]>;
|
||||
genre: string = '';
|
||||
genre$!: Observable<Genre>;
|
||||
|
||||
all$!: Observable<any>;
|
||||
noData: boolean = true;
|
||||
|
||||
private onDestroy: Subject<void> = new Subject();
|
||||
|
||||
constructor(private recommendationService: RecommendationService, private seriesService: SeriesService, private metadataService: MetadataService) { }
|
||||
constructor(private recommendationService: RecommendationService, private seriesService: SeriesService,
|
||||
private metadataService: MetadataService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.quickReads$ = this.recommendationService.getQuickReads(this.libraryId)
|
||||
this.quickReads$ = this.recommendationService.getQuickReads(this.libraryId, 0, 30)
|
||||
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay());
|
||||
|
||||
this.quickCatchups$ = this.recommendationService.getQuickCatchupReads(this.libraryId)
|
||||
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay());
|
||||
this.quickCatchups$ = this.recommendationService.getQuickCatchupReads(this.libraryId, 0, 30)
|
||||
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay());
|
||||
|
||||
this.highlyRated$ = this.recommendationService.getHighlyRated(this.libraryId)
|
||||
this.highlyRated$ = this.recommendationService.getHighlyRated(this.libraryId, 0, 30)
|
||||
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay());
|
||||
|
||||
this.rediscover$ = this.recommendationService.getRediscover(this.libraryId)
|
||||
this.rediscover$ = this.recommendationService.getRediscover(this.libraryId, 0, 30)
|
||||
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay());
|
||||
|
||||
this.onDeck$ = this.seriesService.getOnDeck(this.libraryId)
|
||||
this.onDeck$ = this.seriesService.getOnDeck(this.libraryId, 0, 30)
|
||||
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay());
|
||||
|
||||
this.genre$ = this.metadataService.getAllGenres([this.libraryId]).pipe(takeUntil(this.onDestroy), map(genres => genres[Math.floor(Math.random() * genres.length)]), shareReplay());
|
||||
this.genre$ = this.metadataService.getAllGenres([this.libraryId]).pipe(
|
||||
takeUntil(this.onDestroy),
|
||||
map(genres => genres[Math.floor(Math.random() * genres.length)]),
|
||||
shareReplay()
|
||||
);
|
||||
this.genre$.subscribe(genre => {
|
||||
this.moreIn$ = this.recommendationService.getMoreIn(this.libraryId, genre.id).pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay());
|
||||
this.moreIn$ = this.recommendationService.getMoreIn(this.libraryId, genre.id, 0, 30).pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay());
|
||||
});
|
||||
|
||||
this.all$ = merge(this.quickReads$, this.quickCatchups$, this.highlyRated$, this.rediscover$, this.onDeck$, this.genre$).pipe(takeUntil(this.onDestroy));
|
||||
this.all$.subscribe(() => this.noData = false);
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
|
||||
@ -74,8 +75,9 @@ export class LibraryRecommendedComponent implements OnInit, OnDestroy {
|
||||
if (seriesObj.pagesRead !== seriesObj.pages && seriesObj.pagesRead !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
//this.loadOnDeck();
|
||||
|
||||
this.quickReads$ = this.quickReads$.pipe(filter(series => !series.includes(seriesObj)));
|
||||
this.quickCatchups$ = this.quickCatchups$.pipe(filter(series => !series.includes(seriesObj)));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -14,4 +14,6 @@ export interface ChapterInfo {
|
||||
isSpecial: boolean;
|
||||
volumeId: number;
|
||||
pages: number;
|
||||
subtitle: string;
|
||||
title: string;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core';
|
||||
import { BehaviorSubject, fromEvent, ReplaySubject, Subject } from 'rxjs';
|
||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||
@ -39,7 +39,8 @@ const enum DEBUG_MODES {
|
||||
@Component({
|
||||
selector: 'app-infinite-scroller',
|
||||
templateUrl: './infinite-scroller.component.html',
|
||||
styleUrls: ['./infinite-scroller.component.scss']
|
||||
styleUrls: ['./infinite-scroller.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
@ -150,11 +151,11 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
constructor(private readerService: ReaderService, private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, private scrollService: ScrollService) {
|
||||
constructor(private readerService: ReaderService, private renderer: Renderer2,
|
||||
@Inject(DOCUMENT) private document: Document, private scrollService: ScrollService,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
// This will always exist at this point in time since this is used within manga reader
|
||||
const reader = document.querySelector('.reader');
|
||||
if (reader !== null) {
|
||||
@ -165,6 +166,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.hasOwnProperty('totalPages') && changes['totalPages'].previousValue != changes['totalPages'].currentValue) {
|
||||
this.totalPages = changes['totalPages'].currentValue;
|
||||
this.cdRef.markForCheck();
|
||||
this.initWebtoonReader();
|
||||
}
|
||||
}
|
||||
@ -211,6 +213,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
const image = document.querySelector('img[id^="page-' + page + '"]');
|
||||
if (image) {
|
||||
this.renderer.addClass(image, 'bookmark-effect');
|
||||
|
||||
setTimeout(() => {
|
||||
this.renderer.removeClass(image, 'bookmark-effect');
|
||||
}, 1000);
|
||||
@ -222,6 +225,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.fullscreenToggled.pipe(takeUntil(this.onDestroy)).subscribe(isFullscreen => {
|
||||
this.debugLog('[FullScreen] Fullscreen mode: ', isFullscreen);
|
||||
this.isFullscreenMode = isFullscreen;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.recalculateImageWidth();
|
||||
this.initScrollHandler();
|
||||
this.setPageNum(this.pageNum, true);
|
||||
@ -232,6 +237,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
recalculateImageWidth() {
|
||||
const [_, innerWidth] = this.getInnerDimensions();
|
||||
this.webtoonImageWidth = innerWidth || document.body.clientWidth || document.documentElement.clientWidth;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
getVerticalOffset() {
|
||||
@ -256,9 +262,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
* @param event Scroll Event
|
||||
*/
|
||||
handleScrollEvent(event?: any) {
|
||||
// Need a fullscreen handler here too
|
||||
let verticalOffset = this.getVerticalOffset();
|
||||
|
||||
const verticalOffset = this.getVerticalOffset();
|
||||
|
||||
if (verticalOffset > this.prevScrollPosition) {
|
||||
this.scrollingDirection = PAGING_DIRECTION.FORWARD;
|
||||
@ -270,6 +274,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (this.isScrolling && this.currentPageElem != null && this.isElementVisible(this.currentPageElem)) {
|
||||
this.debugLog('[Scroll] Image is visible from scroll, isScrolling is now false');
|
||||
this.isScrolling = false;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
if (!this.isScrolling) {
|
||||
@ -282,11 +287,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.setPageNum(parseInt(midlineImages[0].getAttribute('page') || this.pageNum + '', 10));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check if we hit the last page
|
||||
this.checkIfShouldTriggerContinuousReader();
|
||||
|
||||
}
|
||||
|
||||
getTotalHeight() {
|
||||
@ -294,17 +297,18 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
document.querySelectorAll('img[id^="page-"]').forEach(img => totalHeight += img.getBoundingClientRect().height);
|
||||
return Math.round(totalHeight);
|
||||
}
|
||||
|
||||
getTotalScroll() {
|
||||
if (this.isFullscreenMode) {
|
||||
return this.readerElemRef.nativeElement.offsetHeight + this.readerElemRef.nativeElement.scrollTop;
|
||||
}
|
||||
return document.body.offsetHeight + document.body.scrollTop;
|
||||
}
|
||||
|
||||
getScrollTop() {
|
||||
if (this.isFullscreenMode) {
|
||||
return this.readerElemRef.nativeElement.scrollTop;
|
||||
}
|
||||
|
||||
return document.body.scrollTop;
|
||||
}
|
||||
|
||||
@ -318,25 +322,32 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// If we were at top but have started scrolling down past page 0, remove top spacer
|
||||
if (this.atTop && this.pageNum > 0) {
|
||||
this.atTop = false;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
if (totalScroll === totalHeight && !this.atBottom) {
|
||||
this.atBottom = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.setPageNum(this.totalPages);
|
||||
|
||||
// Scroll user back to original location
|
||||
this.previousScrollHeightMinusTop = this.getScrollTop();
|
||||
requestAnimationFrame(() => document.body.scrollTop = this.previousScrollHeightMinusTop + (SPACER_SCROLL_INTO_PX / 2));
|
||||
requestAnimationFrame(() => {
|
||||
document.body.scrollTop = this.previousScrollHeightMinusTop + (SPACER_SCROLL_INTO_PX / 2);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
} else if (totalScroll >= totalHeight + SPACER_SCROLL_INTO_PX && this.atBottom) {
|
||||
// This if statement will fire once we scroll into the spacer at all
|
||||
this.loadNextChapter.emit();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
} else {
|
||||
// < 5 because debug mode and FF (mobile) can report non 0, despite being at 0
|
||||
if (this.getScrollTop() < 5 && this.pageNum === 0 && !this.atTop) {
|
||||
this.atBottom = false;
|
||||
|
||||
this.atTop = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
// Scroll user back to original location
|
||||
this.previousScrollHeightMinusTop = document.body.scrollHeight - document.body.scrollTop;
|
||||
|
||||
@ -345,9 +356,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
} else if (this.getScrollTop() < 5 && this.pageNum === 0 && this.atTop) {
|
||||
// If already at top, then we moving on
|
||||
this.loadPrevChapter.emit();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -376,9 +387,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
this.debugLog('[Visibility] Checking if Page ' + elem.getAttribute('id') + ' is visible');
|
||||
// NOTE: This will say an element is visible if it is 1 px offscreen on top
|
||||
var rect = elem.getBoundingClientRect();
|
||||
const rect = elem.getBoundingClientRect();
|
||||
|
||||
let [innerHeight, innerWidth] = this.getInnerDimensions();
|
||||
const [innerHeight, innerWidth] = this.getInnerDimensions();
|
||||
|
||||
return (rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
@ -396,9 +407,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
shouldElementCountAsCurrentPage(elem: Element) {
|
||||
if (elem === null || elem === undefined) { return false; }
|
||||
|
||||
var rect = elem.getBoundingClientRect();
|
||||
const rect = elem.getBoundingClientRect();
|
||||
|
||||
let [innerHeight, innerWidth] = this.getInnerDimensions();
|
||||
const [innerHeight, innerWidth] = this.getInnerDimensions();
|
||||
|
||||
|
||||
if (rect.bottom >= 0 &&
|
||||
@ -420,13 +431,14 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.webtoonImages.next([]);
|
||||
this.atBottom = false;
|
||||
this.checkIfShouldTriggerContinuousReader();
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
const [startingIndex, endingIndex] = this.calculatePrefetchIndecies();
|
||||
|
||||
this.debugLog('[INIT] Prefetching pages ' + startingIndex + ' to ' + endingIndex + '. Current page: ', this.pageNum);
|
||||
for(let i = startingIndex; i <= endingIndex; i++) {
|
||||
this.loadWebtoonImage(i);
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -460,9 +472,11 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.scrollToCurrentPage();
|
||||
} else {
|
||||
this.initFinished = true;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
this.allImagesLoaded = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -496,6 +510,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
this.pageNum = pageNum;
|
||||
this.pageNumberChange.emit(this.pageNum);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.prefetchWebtoonImages();
|
||||
|
||||
@ -519,26 +534,27 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Update prevScrollPosition, so the next scroll event properly calculates direction
|
||||
this.prevScrollPosition = this.currentPageElem.getBoundingClientRect().top;
|
||||
this.isScrolling = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.currentPageElem) {
|
||||
this.debugLog('[Scroll] Scrolling to page ', this.pageNum);
|
||||
this.currentPageElem.scrollIntoView({behavior: 'smooth'});
|
||||
this.initFinished = true;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}, 600);
|
||||
}
|
||||
|
||||
loadWebtoonImage(page: number) {
|
||||
let data = this.webtoonImages.value;
|
||||
|
||||
if (this.imagesLoaded.hasOwnProperty(page)) {
|
||||
this.debugLog('\t[PREFETCH] Skipping prefetch of ', page);
|
||||
return;
|
||||
}
|
||||
this.debugLog('\t[PREFETCH] Prefetching ', page);
|
||||
|
||||
data = data.concat({src: this.urlProvider(page), page});
|
||||
this.debugLog('\t[PREFETCH] Prefetching ', page);
|
||||
|
||||
const data = this.webtoonImages.value.concat({src: this.urlProvider(page), page});
|
||||
|
||||
data.sort((a: WebtoonImage, b: WebtoonImage) => {
|
||||
if (a.page < b.page) { return -1; }
|
||||
@ -547,6 +563,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
});
|
||||
|
||||
this.allImagesLoaded = false;
|
||||
this.cdRef.markForCheck();
|
||||
this.webtoonImages.next(data);
|
||||
|
||||
if (!this.imagesLoaded.hasOwnProperty(page)) {
|
||||
@ -603,7 +620,6 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
prefetchWebtoonImages(pageNum: number = -1) {
|
||||
|
||||
if (pageNum === -1) {
|
||||
pageNum = this.pageNum;
|
||||
}
|
||||
@ -621,6 +637,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
.map((img: any) => new Promise(resolve => { img.onload = img.onerror = resolve; })))
|
||||
.then(() => {
|
||||
this.allImagesLoaded = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { DOCUMENT, Location } from '@angular/common';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||
import { User } from '../_models/user';
|
||||
@ -18,7 +18,6 @@ import { MemberService } from '../_services/member.service';
|
||||
import { Stack } from '../shared/data-structures/stack';
|
||||
import { ChangeContext, LabelType, Options } from '@angular-slider/ngx-slider';
|
||||
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 { layoutModes, pageSplitOptions, scalingOptions } from '../_models/preferences/preferences';
|
||||
import { ReaderMode } from '../_models/preferences/reader-mode';
|
||||
@ -43,6 +42,7 @@ const CLICK_OVERLAY_TIMEOUT = 3000;
|
||||
selector: 'app-manga-reader',
|
||||
templateUrl: './manga-reader.component.html',
|
||||
styleUrls: ['./manga-reader.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [
|
||||
trigger('slideFromTop', [
|
||||
state('in', style({ transform: 'translateY(0)'})),
|
||||
@ -342,20 +342,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
@HostListener('window:orientationchange', ['$event'])
|
||||
onResize() {
|
||||
if (window.innerWidth > window.innerHeight) {
|
||||
this.generalSettingsForm.get('layoutMode')?.enable();
|
||||
return;
|
||||
};
|
||||
if (this.layoutMode === LayoutMode.Single || this.readerMode === ReaderMode.Webtoon) return;
|
||||
|
||||
this.generalSettingsForm.get('layoutMode')?.setValue(LayoutMode.Single);
|
||||
this.generalSettingsForm.get('layoutMode')?.disable();
|
||||
this.toastr.info('Layout mode switched to Single due to insufficient space to render double layout');
|
||||
}
|
||||
|
||||
get CurrentPageBookmarked() {
|
||||
return this.bookmarks.hasOwnProperty(this.pageNum);
|
||||
}
|
||||
@ -448,13 +434,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
||||
public readerService: ReaderService, private location: Location,
|
||||
private formBuilder: FormBuilder, private navService: NavService,
|
||||
public readerService: ReaderService, private formBuilder: FormBuilder, private navService: NavService,
|
||||
private toastr: ToastrService, private memberService: MemberService,
|
||||
public utilityService: UtilityService, private renderer: Renderer2,
|
||||
@Inject(DOCUMENT) private document: Document, private modalService: NgbModal) {
|
||||
@Inject(DOCUMENT) private document: Document, private modalService: NgbModal,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
this.navService.hideNavBar();
|
||||
this.navService.hideSideNav();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -519,6 +506,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.generalSettingsForm.get('fittingOption')?.setValue(this.translateScalingOption(ScalingOption.FitToHeight));
|
||||
this.generalSettingsForm.get('fittingOption')?.disable();
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
// Re-render the current page when we switch layouts
|
||||
if (changeOccurred) {
|
||||
@ -527,7 +515,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => {
|
||||
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
|
||||
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value; // TODO: Do I need cd check here?
|
||||
const needsSplitting = this.isWideImage();
|
||||
// If we need to split on a menu change, then we need to re-render.
|
||||
if (needsSplitting) {
|
||||
@ -551,9 +539,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.readerMode === ReaderMode.Webtoon) return;
|
||||
if (this.readerMode === ReaderMode.LeftRight && this.FittingOption === FITTING_OPTION.HEIGHT) {
|
||||
this.rightPaginationOffset = (this.readingArea.nativeElement.scrollLeft) * -1;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
this.rightPaginationOffset = 0;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
if (this.canvas) {
|
||||
@ -573,6 +563,22 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.goToPageEvent !== undefined) this.goToPageEvent.complete();
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
@HostListener('window:orientationchange', ['$event'])
|
||||
onResize() {
|
||||
if (window.innerWidth > window.innerHeight) {
|
||||
this.generalSettingsForm.get('layoutMode')?.enable();
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
};
|
||||
if (this.layoutMode === LayoutMode.Single || this.readerMode === ReaderMode.Webtoon) return;
|
||||
|
||||
this.generalSettingsForm.get('layoutMode')?.setValue(LayoutMode.Single);
|
||||
this.generalSettingsForm.get('layoutMode')?.disable();
|
||||
this.toastr.info('Layout mode switched to Single due to insufficient space to render double layout');
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
@HostListener('window:keyup', ['$event'])
|
||||
handleKeyPress(event: KeyboardEvent) {
|
||||
switch (this.readerMode) {
|
||||
@ -651,6 +657,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.pageNum = 0;
|
||||
this.pagingDirection = PAGING_DIRECTION.FORWARD;
|
||||
this.inSetup = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
if (this.goToPageEvent) {
|
||||
// There was a bug where goToPage was emitting old values into infinite scroller between chapter loads. We explicity clear it out between loads
|
||||
@ -670,8 +677,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
newOptions.ceil = this.maxPages - 1; // We -1 so that the slider UI shows us hitting the end, since visually we +1 everything.
|
||||
this.pageOptions = newOptions;
|
||||
this.inSetup = false;
|
||||
this.prevChapterDisabled = true;
|
||||
this.nextChapterDisabled = true;
|
||||
|
||||
const images = [];
|
||||
for (let i = 0; i < PREFETCH_PAGES + 2; i++) {
|
||||
@ -691,8 +696,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
chapterInfo: this.readerService.getChapterInfo(this.chapterId),
|
||||
bookmarks: this.readerService.getBookmarks(this.chapterId),
|
||||
}).pipe(take(1)).subscribe(results => {
|
||||
|
||||
|
||||
if (this.readingListMode && (results.chapterInfo.seriesFormat === MangaFormat.EPUB || results.chapterInfo.seriesFormat === MangaFormat.PDF)) {
|
||||
// Redirect to the book reader.
|
||||
const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId);
|
||||
@ -718,7 +721,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.pageOptions = newOptions;
|
||||
|
||||
this.libraryType = results.chapterInfo.libraryType;
|
||||
this.updateTitle(results.chapterInfo, this.libraryType);
|
||||
this.title = results.chapterInfo.title;
|
||||
this.subtitle = results.chapterInfo.subtitle;
|
||||
|
||||
this.inSetup = false;
|
||||
|
||||
@ -729,17 +733,20 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
results.bookmarks.forEach(bookmark => {
|
||||
this.bookmarks[bookmark.page] = 1;
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
|
||||
this.nextChapterId = chapterId;
|
||||
if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {
|
||||
this.nextChapterDisabled = true;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
|
||||
this.prevChapterId = chapterId;
|
||||
if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {
|
||||
this.prevChapterDisabled = true;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
@ -760,43 +767,19 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
closeReader() {
|
||||
this.readerService.closeReader(this.readingListMode, this.readingListId);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.readerMode === ReaderMode.Webtoon) {
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
} else {
|
||||
this.loadPage();
|
||||
}
|
||||
}
|
||||
|
||||
closeReader() {
|
||||
if (this.readingListMode) {
|
||||
this.router.navigateByUrl('lists/' + this.readingListId);
|
||||
} else {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
|
||||
updateTitle(chapterInfo: ChapterInfo, type: LibraryType) {
|
||||
this.title = chapterInfo.seriesName;
|
||||
if (chapterInfo.chapterTitle != null && chapterInfo.chapterTitle.length > 0) {
|
||||
this.title += ' - ' + chapterInfo.chapterTitle;
|
||||
}
|
||||
|
||||
// TODO: Move this to the backend
|
||||
this.subtitle = '';
|
||||
if (chapterInfo.isSpecial && chapterInfo.volumeNumber === '0') {
|
||||
this.subtitle = chapterInfo.fileName;
|
||||
} else if (!chapterInfo.isSpecial && chapterInfo.volumeNumber === '0') {
|
||||
this.subtitle = this.utilityService.formatChapterName(type, true, true) + chapterInfo.chapterNumber;
|
||||
} else {
|
||||
this.subtitle = 'Volume ' + chapterInfo.volumeNumber;
|
||||
|
||||
if (chapterInfo.chapterNumber !== '0') {
|
||||
this.subtitle += ' ' + this.utilityService.formatChapterName(type, true, true) + chapterInfo.chapterNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
translateScalingOption(option: ScalingOption) {
|
||||
switch (option) {
|
||||
case (ScalingOption.Automatic):
|
||||
@ -907,6 +890,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
toggleMenu() {
|
||||
this.menuOpen = !this.menuOpen;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
if (this.menuTimeout) {
|
||||
clearTimeout(this.menuTimeout);
|
||||
@ -917,6 +901,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
} else {
|
||||
this.showClickOverlay = false;
|
||||
this.settingsOpen = false;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1084,8 +1069,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
loadNextChapter() {
|
||||
if (this.nextPageDisabled) { return; }
|
||||
if (this.nextChapterDisabled) { return; }
|
||||
if (this.nextChapterDisabled) {
|
||||
this.toastr.info('No Next Chapter');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
if (this.nextChapterId === CHAPTER_ID_NOT_FETCHED || this.nextChapterId === this.chapterId) {
|
||||
this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
|
||||
this.nextChapterId = chapterId;
|
||||
@ -1098,8 +1088,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
loadPrevChapter() {
|
||||
if (this.prevPageDisabled) { return; }
|
||||
if (this.prevChapterDisabled) { return; }
|
||||
if (this.prevChapterDisabled) {
|
||||
this.toastr.info('No Previous Chapter');
|
||||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.continuousChaptersStack.pop();
|
||||
const prevChapter = this.continuousChaptersStack.peek();
|
||||
if (prevChapter != this.chapterId) {
|
||||
@ -1138,7 +1132,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
} else {
|
||||
this.nextPageDisabled = true;
|
||||
}
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1170,44 +1164,55 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.canvas.nativeElement.width = this.canvasImage.width;
|
||||
this.canvas.nativeElement.height = this.canvasImage.height;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
renderPage() {
|
||||
const pageSplit = this.generalSettingsForm.get('pageSplitOption')?.value;
|
||||
if (!this.ctx || !this.canvas || pageSplit !== PageSplitOption.FitSplit || pageSplit !== PageSplitOption.NoSplit) {
|
||||
const pageSplit = parseInt(this.generalSettingsForm.get('pageSplitOption')?.value, 10);
|
||||
if (!this.ctx || !this.canvas || (pageSplit === PageSplitOption.FitSplit || pageSplit === PageSplitOption.NoSplit)) {
|
||||
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
|
||||
this.readingArea.nativeElement.scroll(0,0);
|
||||
}
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
const needsSplitting = this.isWideImage();
|
||||
if (!needsSplitting) {
|
||||
this.renderWithCanvas = false;
|
||||
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
|
||||
this.readingArea.nativeElement.scroll(0,0);
|
||||
}
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
this.canvasImage.onload = null;
|
||||
|
||||
this.setCanvasSize();
|
||||
|
||||
const needsSplitting = this.isWideImage();
|
||||
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;
|
||||
this.cdRef.markForCheck();
|
||||
} 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;
|
||||
} else {
|
||||
this.renderWithCanvas = false;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
// Reset scroll on non HEIGHT Fits
|
||||
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
|
||||
this.readingArea.nativeElement.scroll(0,0);
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateScalingForFirstPageRender() {
|
||||
@ -1233,6 +1238,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.firstPageRendered = true;
|
||||
this.generalSettingsForm.get('fittingOption')?.setValue(newScale, {emitEvent: false});
|
||||
//this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1330,9 +1336,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.renderPage();
|
||||
this.prefetch();
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
setReadingDirection() {
|
||||
@ -1376,18 +1385,21 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
setPageNum(pageNum: number) {
|
||||
this.pageNum = Math.max(Math.min(pageNum, this.maxPages - 1), 0);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
if (this.pageNum >= this.maxPages - 10) {
|
||||
// Tell server to cache the next chapter
|
||||
if (this.nextChapterId > 0 && !this.nextChapterPrefetched) {
|
||||
this.readerService.getChapterInfo(this.nextChapterId).pipe(take(1)).subscribe(res => {
|
||||
this.nextChapterPrefetched = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
} else if (this.pageNum <= 10) {
|
||||
if (this.prevChapterId > 0 && !this.prevChapterPrefetched) {
|
||||
this.readerService.getChapterInfo(this.prevChapterId).pipe(take(1)).subscribe(res => {
|
||||
this.prevChapterPrefetched = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1481,7 +1493,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
updateForm() {
|
||||
|
||||
if ( this.readerMode === ReaderMode.Webtoon) {
|
||||
this.generalSettingsForm.get('pageSplitOption')?.disable()
|
||||
this.generalSettingsForm.get('fittingOption')?.disable()
|
||||
@ -1496,6 +1507,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.generalSettingsForm.get('fittingOption')?.disable();
|
||||
}
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
handleWebtoonPageChange(updatedPageNum: number) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { distinctUntilChanged, forkJoin, map, Observable, of, ReplaySubject, Subject, takeUntil } from 'rxjs';
|
||||
@ -24,7 +24,8 @@ import { FilterSettings } from './filter-settings';
|
||||
@Component({
|
||||
selector: 'app-metadata-filter',
|
||||
templateUrl: './metadata-filter.component.html',
|
||||
styleUrls: ['./metadata-filter.component.scss']
|
||||
styleUrls: ['./metadata-filter.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -86,18 +87,21 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService,
|
||||
private utilityService: UtilityService, private collectionTagService: CollectionTagService, public toggleService: ToggleService) {
|
||||
private utilityService: UtilityService, private collectionTagService: CollectionTagService, public toggleService: ToggleService,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.filterSettings === undefined) {
|
||||
this.filterSettings = new FilterSettings();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
if (this.filterOpen) {
|
||||
this.filterOpen.pipe(takeUntil(this.onDestroy)).subscribe(openState => {
|
||||
this.filteringCollapsed = !openState;
|
||||
this.toggleService.set(!this.filteringCollapsed);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -135,6 +139,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
this.readProgressGroup.get('notRead')?.enable({ emitEvent: false });
|
||||
this.readProgressGroup.get('inProgress')?.enable({ emitEvent: false });
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.sortGroup.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(changes => {
|
||||
@ -145,14 +150,17 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
}
|
||||
this.filter.sortOptions.sortField = parseInt(this.sortGroup.get('sortField')?.value, 10);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.seriesNameGroup.get('seriesNameQuery')?.valueChanges.pipe(
|
||||
map(val => (val || '').trim()),
|
||||
distinctUntilChanged(),
|
||||
takeUntil(this.onDestroy))
|
||||
.subscribe(changes => {
|
||||
this.filter.seriesNameQuery = changes;
|
||||
takeUntil(this.onDestroy)
|
||||
)
|
||||
.subscribe(changes => {
|
||||
this.filter.seriesNameQuery = changes; // TODO: See if we can make this into observable
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.loadFromPresetsAndSetup();
|
||||
@ -162,6 +170,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
this.filterOpen.emit(false);
|
||||
this.filteringCollapsed = true;
|
||||
this.toggleService.set(!this.filteringCollapsed);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@ -199,6 +208,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.setupFormatTypeahead();
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
forkJoin([
|
||||
this.setupLibraryTypeahead(),
|
||||
@ -216,6 +226,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
this.filteringCollapsed = false;
|
||||
this.toggleService.set(!this.filteringCollapsed);
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
this.apply();
|
||||
});
|
||||
}
|
||||
@ -591,6 +602,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
this.readProgressGroup.get('inProgress')?.setValue(true);
|
||||
this.sortGroup.get('sortField')?.setValue(SortField.SortName);
|
||||
this.isAscendingSort = true;
|
||||
this.cdRef.markForCheck();
|
||||
// Apply any presets which will trigger the apply
|
||||
this.loadFromPresetsAndSetup();
|
||||
}
|
||||
@ -598,10 +610,12 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
apply() {
|
||||
this.applyFilter.emit({filter: this.filter, isFirst: this.updateApplied === 0});
|
||||
this.updateApplied++;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
toggleSelected() {
|
||||
this.toggleService.toggle();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
setToggle(event: any) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
<ng-container *ngIf="isAdmin">
|
||||
<ng-container *ngIf="isAdmin$ | async">
|
||||
|
||||
<ng-container *ngIf="errors$ | async as errors">
|
||||
<button type="button" class="btn btn-icon" [ngClass]="{'colored': activeEvents > 0, 'colored-error': errors.length > 0}"
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
|
||||
import { map, shareReplay, takeUntil } from 'rxjs/operators';
|
||||
import { ConfirmConfig } from 'src/app/shared/confirm-dialog/_models/confirm-config';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { UpdateNotificationModalComponent } from 'src/app/shared/update-notification/update-notification-modal.component';
|
||||
@ -15,12 +15,13 @@ import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hu
|
||||
@Component({
|
||||
selector: 'app-nav-events-toggle',
|
||||
templateUrl: './events-widget.component.html',
|
||||
styleUrls: ['./events-widget.component.scss']
|
||||
styleUrls: ['./events-widget.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EventsWidgetComponent implements OnInit, OnDestroy {
|
||||
@Input() user!: User;
|
||||
|
||||
isAdmin: boolean = false;
|
||||
isAdmin$: Observable<boolean> = of(false);
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
@ -48,7 +49,8 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
constructor(public messageHub: MessageHubService, private modalService: NgbModal,
|
||||
private accountService: AccountService, private confirmService: ConfirmService) { }
|
||||
private accountService: AccountService, private confirmService: ConfirmService,
|
||||
private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
@ -59,7 +61,6 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Debounce for testing. Kavita's too fast
|
||||
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => {
|
||||
if (event.event === EVENTS.NotificationProgress) {
|
||||
this.processNotificationProgressEvent(event);
|
||||
@ -68,27 +69,27 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
||||
values.push(event.payload as ErrorEvent);
|
||||
this.errorSource.next(values);
|
||||
this.activeEvents += 1;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
} else {
|
||||
this.isAdmin = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.isAdmin$ = this.accountService.currentUser$.pipe(
|
||||
takeUntil(this.onDestroy),
|
||||
map(user => (user && this.accountService.hasAdminRole(user)) || false),
|
||||
shareReplay()
|
||||
);
|
||||
}
|
||||
|
||||
processNotificationProgressEvent(event: Message<NotificationProgressEvent>) {
|
||||
const message = event.payload as NotificationProgressEvent;
|
||||
let data;
|
||||
let index = -1;
|
||||
switch (event.payload.eventType) {
|
||||
case 'single':
|
||||
const values = this.singleUpdateSource.getValue();
|
||||
values.push(message);
|
||||
this.singleUpdateSource.next(values);
|
||||
this.activeEvents += 1;
|
||||
this.cdRef.markForCheck();
|
||||
break;
|
||||
case 'started':
|
||||
// Sometimes we can receive 2 started on long running scans, so better to just treat as a merge then.
|
||||
@ -104,6 +105,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
||||
data = data.filter(m => m.name !== message.name);
|
||||
this.progressEventsSource.next(data);
|
||||
this.activeEvents = Math.max(this.activeEvents - 1, 0);
|
||||
this.cdRef.markForCheck();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -116,6 +118,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
||||
if (index < 0) {
|
||||
data.push(message);
|
||||
this.activeEvents += 1;
|
||||
this.cdRef.markForCheck();
|
||||
} else {
|
||||
data[index] = message;
|
||||
}
|
||||
@ -158,6 +161,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
||||
data = data.filter(m => m !== error);
|
||||
this.errorSource.next(data);
|
||||
this.activeEvents = Math.max(this.activeEvents - 1, 0);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
prettyPrintProgress(progress: number) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||
@ -8,7 +8,8 @@ import { SearchResultGroup } from '../../_models/search/search-result-group';
|
||||
@Component({
|
||||
selector: 'app-grouped-typeahead',
|
||||
templateUrl: './grouped-typeahead.component.html',
|
||||
styleUrls: ['./grouped-typeahead.component.scss']
|
||||
styleUrls: ['./grouped-typeahead.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
@ -79,16 +80,15 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
||||
return !(this.noResultsTemplate != undefined && !this.grouppedData.persons.length && !this.grouppedData.collections.length
|
||||
&& !this.grouppedData.series.length && !this.grouppedData.persons.length && !this.grouppedData.tags.length && !this.grouppedData.genres.length && !this.grouppedData.libraries.length
|
||||
&& !this.grouppedData.files.length && !this.grouppedData.chapters.length);
|
||||
|
||||
//return this.grouppedData.persons.length || this.grouppedData.collections.length || this.grouppedData.series.length || this.grouppedData.persons.length || this.grouppedData.tags.length || this.grouppedData.genres.length;
|
||||
}
|
||||
|
||||
|
||||
constructor() { }
|
||||
constructor(private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
@HostListener('window:click', ['$event'])
|
||||
handleDocumentClick(event: any) {
|
||||
this.close();
|
||||
|
||||
}
|
||||
|
||||
@HostListener('window:keydown', ['$event'])
|
||||
@ -107,12 +107,14 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.typeaheadForm.addControl('typeahead', new FormControl(this.initialValue, []));
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.typeaheadForm.valueChanges.pipe(debounceTime(this.debounceTime), takeUntil(this.onDestroy)).subscribe(change => {
|
||||
const value = this.typeaheadForm.get('typeahead')?.value;
|
||||
|
||||
if (value != undefined && value != '' && !this.hasFocus) {
|
||||
this.hasFocus = true;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
if (value != undefined && value.length >= this.minQueryLength) {
|
||||
@ -120,6 +122,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
||||
if (this.prevSearchTerm === value) return;
|
||||
this.inputChanged.emit(value);
|
||||
this.prevSearchTerm = value;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -156,6 +159,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
||||
this.prevSearchTerm = '';
|
||||
this.typeaheadForm.get('typeahead')?.setValue(this.initialValue);
|
||||
this.clearField.emit();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
@ -170,17 +174,20 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
||||
this.resetField();
|
||||
}
|
||||
this.hasFocus = false;
|
||||
this.cdRef.markForCheck();
|
||||
this.focusChanged.emit(this.hasFocus);
|
||||
}
|
||||
|
||||
open(event?: FocusEvent) {
|
||||
this.hasFocus = true;
|
||||
this.focusChanged.emit(this.hasFocus);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.prevSearchTerm = '';
|
||||
this.typeaheadForm.get('typeahead')?.setValue(this.initialValue);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="container-fluid">
|
||||
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
||||
<a class="side-nav-toggle" *ngIf="navService?.sideNavVisibility$ | async" (click)="hideSideNav()"><i class="fas fa-bars"></i></a>
|
||||
<a class="navbar-brand dark-exempt" routerLink="/libraries" routerLinkActive="active"><img class="logo" src="../../assets/images/logo.png" alt="kavita icon" aria-hidden="true"/><span class="d-none d-md-inline"> Kavita</span></a>
|
||||
<a class="navbar-brand dark-exempt" routerLink="/libraries" routerLinkActive="active"><img width="28px" height="28px" class="logo" src="../../assets/images/logo.png" alt="kavita icon" aria-hidden="true"/><span class="d-none d-md-inline"> Kavita</span></a>
|
||||
<ul class="navbar-nav col me-auto">
|
||||
|
||||
<div class="nav-item" *ngIf="(accountService.currentUser$ | async) as user">
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Component, ContentChildren, ElementRef, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { NavigationStart, Router } from '@angular/router';
|
||||
import { fromEvent, Subject } from 'rxjs';
|
||||
import { debounceTime, filter, takeUntil } from 'rxjs/operators';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { MangaFile } from 'src/app/_models/manga-file';
|
||||
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||
@ -22,7 +22,8 @@ import { NavService } from '../../_services/nav.service';
|
||||
@Component({
|
||||
selector: 'app-nav-header',
|
||||
templateUrl: './nav-header.component.html',
|
||||
styleUrls: ['./nav-header.component.scss']
|
||||
styleUrls: ['./nav-header.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class NavHeaderComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -51,7 +52,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(public accountService: AccountService, private router: Router, public navService: NavService,
|
||||
private libraryService: LibraryService, public imageService: ImageService, @Inject(DOCUMENT) private document: Document,
|
||||
private scrollService: ScrollService, private seriesService: SeriesService,) { }
|
||||
private scrollService: ScrollService, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// setTimeout(() => this.setupScrollChecker(), 1000);
|
||||
@ -108,14 +109,17 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
||||
onChangeSearch(val: string) {
|
||||
this.isLoading = true;
|
||||
this.searchTerm = val.trim();
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.libraryService.search(val.trim()).pipe(takeUntil(this.onDestroy)).subscribe(results => {
|
||||
this.searchResults = results;
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
}, err => {
|
||||
this.searchResults.reset();
|
||||
this.isLoading = false;
|
||||
this.searchTerm = '';
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -170,6 +174,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
||||
this.searchViewRef.clear();
|
||||
this.searchTerm = '';
|
||||
this.searchResults = new SearchResultGroup();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
clickSeriesSearchResult(item: SearchResult) {
|
||||
@ -185,7 +190,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
||||
if (series !== undefined && series !== null) {
|
||||
this.router.navigate(['library', series.libraryId, 'series', series.id]);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
clickChapterSearchResult(item: Chapter) {
|
||||
@ -194,10 +199,11 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
||||
if (series !== undefined && series !== null) {
|
||||
this.router.navigate(['library', series.libraryId, 'series', series.id]);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
clickLibraryResult(item: Library) {
|
||||
this.clearSearch();
|
||||
this.router.navigate(['library', item.id]);
|
||||
}
|
||||
|
||||
@ -217,8 +223,8 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
focusUpdate(searchFocused: boolean) {
|
||||
this.searchFocused = searchFocused
|
||||
return searchFocused;
|
||||
this.searchFocused = searchFocused;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
hideSideNav() {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { PageViewModeType } from 'ngx-extended-pdf-viewer';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
@ -8,7 +7,6 @@ import { BookService } from 'src/app/book-reader/book.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { User } from 'src/app/_models/user';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { MemberService } from 'src/app/_services/member.service';
|
||||
import { NavService } from 'src/app/_services/nav.service';
|
||||
import { CHAPTER_ID_DOESNT_EXIST, ReaderService } from 'src/app/_services/reader.service';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
@ -17,7 +15,8 @@ import { ThemeService } from 'src/app/_services/theme.service';
|
||||
@Component({
|
||||
selector: 'app-pdf-reader',
|
||||
templateUrl: './pdf-reader.component.html',
|
||||
styleUrls: ['./pdf-reader.component.scss']
|
||||
styleUrls: ['./pdf-reader.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -76,7 +75,8 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
||||
private seriesService: SeriesService, public readerService: ReaderService,
|
||||
private navService: NavService, private toastr: ToastrService,
|
||||
private bookService: BookService, private themeService: ThemeService, private location: Location) {
|
||||
private bookService: BookService, private themeService: ThemeService,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
this.navService.hideNavBar();
|
||||
this.themeService.clearThemes();
|
||||
this.navService.hideSideNav();
|
||||
@ -117,6 +117,8 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
this.readingListId = parseInt(readingListId, 10);
|
||||
}
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.user = user;
|
||||
@ -129,18 +131,22 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
this.bookService.getBookInfo(this.chapterId).subscribe(info => {
|
||||
this.volumeId = info.volumeId;
|
||||
this.bookTitle = info.bookTitle;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.readerService.getProgress(this.chapterId).subscribe(progress => {
|
||||
this.currentPage = progress.pageNum || 1;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.seriesService.getChapter(this.chapterId).subscribe(chapter => {
|
||||
this.maxPages = chapter.pages;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
if (this.currentPage >= this.maxPages) {
|
||||
this.currentPage = this.maxPages - 1;
|
||||
this.saveProgress();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
@ -155,6 +161,7 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
window.history.replaceState({}, '', newRoute);
|
||||
this.toastr.info('Incognito mode is off. Progress will now start being tracked.');
|
||||
this.saveProgress();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
toggleTheme() {
|
||||
@ -165,6 +172,7 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.backgroundColor = this.themeMap[this.theme].background;
|
||||
this.fontColor = this.themeMap[this.theme].font;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
toggleBookPageMode() {
|
||||
@ -173,6 +181,7 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
this.bookMode = 'book';
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
saveProgress() {
|
||||
@ -181,11 +190,7 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
closeReader() {
|
||||
if (this.readingListMode) {
|
||||
this.router.navigateByUrl('lists/' + this.readingListId);
|
||||
} else {
|
||||
this.location.back();
|
||||
}
|
||||
this.readerService.closeReader(this.readingListMode, this.readingListId);
|
||||
}
|
||||
|
||||
}
|
||||
|
23
UI/Web/src/app/pipe/library-type.pipe.ts
Normal file
23
UI/Web/src/app/pipe/library-type.pipe.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { LibraryType } from '../_models/library';
|
||||
|
||||
/**
|
||||
* Returns the name of the LibraryType
|
||||
*/
|
||||
@Pipe({
|
||||
name: 'libraryType'
|
||||
})
|
||||
export class LibraryTypePipe implements PipeTransform {
|
||||
|
||||
transform(libraryType: LibraryType): string {
|
||||
switch(libraryType) {
|
||||
case LibraryType.Book:
|
||||
return 'Book';
|
||||
case LibraryType.Comic:
|
||||
return 'Comic';
|
||||
case LibraryType.Manga:
|
||||
return 'Manga';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -12,6 +12,7 @@ import { LanguageNamePipe } from './language-name.pipe';
|
||||
import { AgeRatingPipe } from './age-rating.pipe';
|
||||
import { MangaFormatPipe } from './manga-format.pipe';
|
||||
import { MangaFormatIconPipe } from './manga-format-icon.pipe';
|
||||
import { LibraryTypePipe } from './library-type.pipe';
|
||||
|
||||
|
||||
|
||||
@ -28,7 +29,8 @@ import { MangaFormatIconPipe } from './manga-format-icon.pipe';
|
||||
LanguageNamePipe,
|
||||
AgeRatingPipe,
|
||||
MangaFormatPipe,
|
||||
MangaFormatIconPipe
|
||||
MangaFormatIconPipe,
|
||||
LibraryTypePipe
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -45,7 +47,8 @@ import { MangaFormatIconPipe } from './manga-format-icon.pipe';
|
||||
LanguageNamePipe,
|
||||
AgeRatingPipe,
|
||||
MangaFormatPipe,
|
||||
MangaFormatIconPipe
|
||||
MangaFormatIconPipe,
|
||||
LibraryTypePipe
|
||||
]
|
||||
})
|
||||
export class PipeModule { }
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
export interface KeyboardShortcut {
|
||||
@ -15,20 +15,16 @@ export interface KeyboardShortcut {
|
||||
@Component({
|
||||
selector: 'app-shortcuts-modal',
|
||||
templateUrl: './shortcuts-modal.component.html',
|
||||
styleUrls: ['./shortcuts-modal.component.scss']
|
||||
styleUrls: ['./shortcuts-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ShortcutsModalComponent implements OnInit {
|
||||
export class ShortcutsModalComponent {
|
||||
|
||||
@Input() shortcuts: Array<KeyboardShortcut> = [];
|
||||
|
||||
constructor(public modal: NgbActiveModal) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
|
||||
close() {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,19 +1,21 @@
|
||||
<div cdkDropList class="{{items.length > 0 ? 'example-list list-group-flush' : ''}}" (cdkDropListDropped)="drop($event)">
|
||||
<div cdkDropList class="{{items.length > 0 ? 'example-list list-group-flush' : ''}}" (cdkDropListDropped)="drop($event)">
|
||||
<div class="example-box" *ngFor="let item of items; index as i" cdkDrag [cdkDragData]="item" cdkDragBoundary=".example-list">
|
||||
<div class="me-3 align-middle">
|
||||
<i class="fa fa-grip-vertical drag-handle" aria-hidden="true" cdkDragHandle></i>
|
||||
<div class="d-flex">
|
||||
<div class="me-3 align-middle">
|
||||
<i class="fa fa-grip-vertical drag-handle" aria-hidden="true" cdkDragHandle></i>
|
||||
</div>
|
||||
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
|
||||
<div class="align-middle" style="padding-top: 40px">
|
||||
<label for="reorder-{{i}}" class="form-label visually-hidden">Reorder</label>
|
||||
<input *ngIf="accessibilityMode" id="reorder-{{i}}" class="form-control" type="number" min="0" [max]="items.length - 1" [value]="i" style="width: 60px" (focusout)="updateIndex(i, item)" (keydown.enter)="updateIndex(i, item)" aria-describedby="instructions">
|
||||
</div>
|
||||
<button class="btn btn-icon float-end" (click)="removeItem(item, i)">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">Remove item</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-container style="display: inline-block" [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
|
||||
<div class="align-middle" style="padding-top: 40px">
|
||||
<label for="reorder-{{i}}" class="form-label visually-hidden">Reorder</label>
|
||||
<input *ngIf="accessibilityMode" id="reorder-{{i}}" class="form-control" type="number" min="0" [max]="items.length - 1" [value]="i" style="width: 60px" (focusout)="updateIndex(i, item)" (keydown.enter)="updateIndex(i, item)" aria-describedby="instructions">
|
||||
</div>
|
||||
<button class="btn btn-icon pull-right" (click)="removeItem(item, i)">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">Remove item</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,4 +44,12 @@
|
||||
|
||||
.example-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
|
||||
.example-custom-placeholder {
|
||||
background: #ccc;
|
||||
border: dotted 3px #999;
|
||||
min-height: 60px;
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core';
|
||||
|
||||
export interface IndexUpdateEvent {
|
||||
fromPosition: number;
|
||||
@ -15,7 +15,8 @@ export interface ItemRemoveEvent {
|
||||
@Component({
|
||||
selector: 'app-draggable-ordered-list',
|
||||
templateUrl: './draggable-ordered-list.component.html',
|
||||
styleUrls: ['./draggable-ordered-list.component.scss']
|
||||
styleUrls: ['./draggable-ordered-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DraggableOrderedListComponent implements OnInit {
|
||||
|
||||
@ -25,7 +26,7 @@ export class DraggableOrderedListComponent implements OnInit {
|
||||
@Output() itemRemove: EventEmitter<ItemRemoveEvent> = new EventEmitter<ItemRemoveEvent>();
|
||||
@ContentChild('draggableItem') itemTemplate!: TemplateRef<any>;
|
||||
|
||||
constructor() { }
|
||||
constructor(private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
@ -38,6 +39,7 @@ export class DraggableOrderedListComponent implements OnInit {
|
||||
toPosition: event.currentIndex,
|
||||
item: this.items[event.currentIndex]
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateIndex(previousIndex: number, item: any) {
|
||||
@ -51,6 +53,7 @@ export class DraggableOrderedListComponent implements OnInit {
|
||||
toPosition: newIndex,
|
||||
item: this.items[newIndex]
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
removeItem(item: any, position: number) {
|
||||
@ -58,6 +61,6 @@ export class DraggableOrderedListComponent implements OnInit {
|
||||
position,
|
||||
item
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block" *ngIf="readingList.coverImage !== '' || readingList.coverImage !== undefined">
|
||||
<app-image maxWidth="300px" [imageUrl]="readingListImage"></app-image>
|
||||
<app-image maxWidth="300px" maxHeight="400px" [imageUrl]="readingListImage"></app-image>
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
|
||||
<div class="row g-0 mb-3">
|
||||
@ -46,38 +46,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto" *ngIf="items.length === 0" style="width: 200px;">
|
||||
Nothing added
|
||||
<div class="mx-auto" style="width: 200px;">
|
||||
<ng-container *ngIf="items.length === 0 && !isLoading">
|
||||
Nothing added
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isLoading">
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<!-- TODO: This needs virtualization -->
|
||||
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" (itemRemove)="itemRemoved($event)" [accessibilityMode]="accessibilityMode">
|
||||
<ng-template #draggableItem let-item let-position="idx">
|
||||
<div class="d-flex" style="width: 100%;">
|
||||
<app-image width="74px" class="img-top me-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1" id="item.id--{{position}}">
|
||||
{{formatTitle(item)}}
|
||||
<!-- TODO: Create a read/unread badge -->
|
||||
<span class="badge bg-primary rounded-pill">
|
||||
<span *ngIf="item.pagesRead > 0 && item.pagesRead < item.pagesTotal">{{item.pagesRead}} / {{item.pagesTotal}}</span>
|
||||
<span *ngIf="item.pagesRead === 0">UNREAD</span>
|
||||
<span *ngIf="item.pagesRead === item.pagesTotal">READ</span>
|
||||
</span>
|
||||
</h5>
|
||||
<ng-container *ngIf="item.seriesFormat | mangaFormat as formatString">
|
||||
<i class="fa {{item.seriesFormat | mangaFormatIcon}}" aria-hidden="true" *ngIf="item.seriesFormat != MangaFormat.UNKNOWN" title="{{formatString}}"></i>
|
||||
<span class="visually-hidden">{{formatString}}</span>
|
||||
</ng-container>
|
||||
|
||||
<a href="/library/{{item.libraryId}}/series/{{item.seriesId}}">{{item.seriesName}}</a>
|
||||
<span *ngIf="item.promoted">
|
||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
</span>
|
||||
<br/>
|
||||
<a href="javascript:void(0);" (click)="readChapter(item)">Read</a>
|
||||
</div>
|
||||
</div>
|
||||
<app-reading-list-item [item]="item" [position]="position" [libraryTypes]="libraryTypes"
|
||||
[promoted]="item.promoted" (read)="readChapter($event)"></app-reading-list-item>
|
||||
</ng-template>
|
||||
</app-draggable-ordered-list>
|
||||
</div>
|
||||
|
@ -1,10 +0,0 @@
|
||||
.container-sm {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.virtual-scroller, virtual-scroller {
|
||||
width: 100%;
|
||||
height: calc(100vh - 85px);
|
||||
max-height: calc(var(--vh)*100 - 170px);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { shareReplay, take } from 'rxjs/operators';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
@ -20,7 +20,8 @@ import { ReaderService } from 'src/app/_services/reader.service';
|
||||
@Component({
|
||||
selector: 'app-reading-list-detail',
|
||||
templateUrl: './reading-list-detail.component.html',
|
||||
styleUrls: ['./reading-list-detail.component.scss']
|
||||
styleUrls: ['./reading-list-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ReadingListDetailComponent implements OnInit {
|
||||
items: Array<ReadingListItem> = [];
|
||||
@ -48,7 +49,8 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
constructor(private route: ActivatedRoute, private router: Router, private readingListService: ReadingListService,
|
||||
private actionService: ActionService, private actionFactoryService: ActionFactoryService, public utilityService: UtilityService,
|
||||
public imageService: ImageService, private accountService: AccountService, private toastr: ToastrService,
|
||||
private confirmService: ConfirmService, private libraryService: LibraryService, private readerService: ReaderService) {}
|
||||
private confirmService: ConfirmService, private libraryService: LibraryService, private readerService: ReaderService,
|
||||
private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
const listId = this.route.snapshot.paramMap.get('id');
|
||||
@ -57,15 +59,8 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
this.router.navigateByUrl('/libraries');
|
||||
return;
|
||||
}
|
||||
|
||||
this.listId = parseInt(listId, 10);
|
||||
|
||||
//this.readingListImage = this.imageService.randomize(this.imageService.getReadingListCoverImage(this.listId));
|
||||
|
||||
this.libraryService.getLibraries().subscribe(libs => {
|
||||
|
||||
});
|
||||
|
||||
forkJoin([
|
||||
this.libraryService.getLibraries(),
|
||||
this.readingListService.getReadingList(this.listId)
|
||||
@ -86,12 +81,15 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
this.readingList = readingList;
|
||||
this.readingListSummary = (this.readingList.summary === null ? '' : this.readingList.summary).replace(/\n/g, '<br>');
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
this.hasDownloadingRole = this.accountService.hasDownloadRole(user);
|
||||
|
||||
this.actions = this.actionFactoryService.getReadingListActions(this.handleReadingListActionCallback.bind(this)).filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin));
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -100,9 +98,12 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
|
||||
getListItems() {
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.readingListService.getListItems(this.listId).subscribe(items => {
|
||||
this.items = items;
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -131,6 +132,7 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
// Reload information around list
|
||||
this.readingList = readingList;
|
||||
this.readingListSummary = (this.readingList.summary === null ? '' : this.readingList.summary).replace(/\n/g, '<br>');
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
}
|
||||
@ -145,24 +147,6 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
formatTitle(item: ReadingListItem) {
|
||||
// TODO: Use new app-entity-title component instead
|
||||
if (item.chapterNumber === '0') {
|
||||
return 'Volume ' + item.volumeNumber;
|
||||
}
|
||||
|
||||
if (item.seriesFormat === MangaFormat.EPUB) {
|
||||
return 'Volume ' + this.utilityService.cleanSpecialTitle(item.chapterNumber);
|
||||
}
|
||||
|
||||
let chapterNum = item.chapterNumber;
|
||||
if (!item.chapterNumber.match(/^\d+$/)) {
|
||||
chapterNum = this.utilityService.cleanSpecialTitle(item.chapterNumber);
|
||||
}
|
||||
|
||||
return this.utilityService.formatChapterName(this.libraryTypes[item.libraryId], true, true) + chapterNum;
|
||||
}
|
||||
|
||||
orderUpdated(event: IndexUpdateEvent) {
|
||||
this.readingListService.updatePosition(this.readingList.id, event.item.id, event.fromPosition, event.toPosition).subscribe(() => { /* No Operation */ });
|
||||
}
|
||||
@ -170,12 +154,14 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
itemRemoved(event: ItemRemoveEvent) {
|
||||
this.readingListService.deleteItem(this.readingList.id, event.item.id).subscribe(() => {
|
||||
this.items.splice(event.position, 1);
|
||||
this.cdRef.markForCheck();
|
||||
this.toastr.success('Item removed');
|
||||
});
|
||||
}
|
||||
|
||||
removeRead() {
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.readingListService.removeRead(this.readingList.id).subscribe((resp) => {
|
||||
if (resp === 'Nothing to remove') {
|
||||
this.toastr.info(resp);
|
||||
@ -186,6 +172,7 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
}
|
||||
|
||||
read() {
|
||||
// TODO: Can I do this in the backend?
|
||||
let currentlyReadingChapter = this.items[0];
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
if (this.items[i].pagesRead >= this.items[i].pagesTotal) {
|
||||
|
@ -0,0 +1,25 @@
|
||||
<div class="d-flex" style="width: 100%;">
|
||||
<app-image width="74px" maxHeight="104px" class="img-top me-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1" id="item.id--{{position}}">
|
||||
{{title}}
|
||||
<!-- TODO: Create a read/unread badge -->
|
||||
<span class="badge bg-primary rounded-pill">
|
||||
<span *ngIf="item.pagesRead > 0 && item.pagesRead < item.pagesTotal">{{item.pagesRead}} / {{item.pagesTotal}}</span>
|
||||
<span *ngIf="item.pagesRead === 0">UNREAD</span>
|
||||
<span *ngIf="item.pagesRead === item.pagesTotal">READ</span>
|
||||
</span>
|
||||
</h5>
|
||||
<ng-container *ngIf="item.seriesFormat | mangaFormat as formatString">
|
||||
<i class="fa {{item.seriesFormat | mangaFormatIcon}}" aria-hidden="true" *ngIf="item.seriesFormat != MangaFormat.UNKNOWN" title="{{formatString}}"></i>
|
||||
<span class="visually-hidden">{{formatString}}</span>
|
||||
</ng-container>
|
||||
|
||||
<a href="/library/{{item.libraryId}}/series/{{item.seriesId}}">{{item.seriesName}}</a>
|
||||
<span *ngIf="promoted">
|
||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
</span>
|
||||
<br/>
|
||||
<a href="javascript:void(0);" (click)="readChapter(item)">Read</a>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,61 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { ReadingListItem } from 'src/app/_models/reading-list';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reading-list-item',
|
||||
templateUrl: './reading-list-item.component.html',
|
||||
styleUrls: ['./reading-list-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ReadingListItemComponent implements OnInit {
|
||||
|
||||
@Input() item!: ReadingListItem;
|
||||
@Input() position: number = 0;
|
||||
@Input() libraryTypes: {[key: number]: LibraryType} = {};
|
||||
/**
|
||||
* If the Reading List is promoted or not
|
||||
*/
|
||||
@Input() promoted: boolean = false;
|
||||
|
||||
@Output() read: EventEmitter<ReadingListItem> = new EventEmitter();
|
||||
|
||||
title: string = '';
|
||||
|
||||
get MangaFormat(): typeof MangaFormat {
|
||||
return MangaFormat;
|
||||
}
|
||||
|
||||
constructor(public imageService: ImageService, private utilityService: UtilityService,
|
||||
private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.formatTitle(this.item);
|
||||
}
|
||||
|
||||
formatTitle(item: ReadingListItem) {
|
||||
if (item.chapterNumber === '0') {
|
||||
this.title = 'Volume ' + item.volumeNumber;
|
||||
}
|
||||
|
||||
if (item.seriesFormat === MangaFormat.EPUB) {
|
||||
this.title = 'Volume ' + this.utilityService.cleanSpecialTitle(item.chapterNumber);
|
||||
}
|
||||
|
||||
let chapterNum = item.chapterNumber;
|
||||
if (!item.chapterNumber.match(/^\d+$/)) {
|
||||
chapterNum = this.utilityService.cleanSpecialTitle(item.chapterNumber);
|
||||
}
|
||||
|
||||
this.title = this.utilityService.formatChapterName(this.libraryTypes[item.libraryId], true, true) + chapterNum;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
readChapter(item: ReadingListItem) {
|
||||
this.read.emit(item);
|
||||
}
|
||||
|
||||
}
|
@ -12,6 +12,7 @@ import { PipeModule } from '../pipe/pipe.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module';
|
||||
import { ReadingListItemComponent } from './reading-list-item/reading-list-item.component';
|
||||
|
||||
|
||||
|
||||
@ -21,7 +22,8 @@ import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-n
|
||||
ReadingListDetailComponent,
|
||||
AddToListModalComponent,
|
||||
ReadingListsComponent,
|
||||
EditReadingListModalComponent
|
||||
EditReadingListModalComponent,
|
||||
ReadingListItemComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -1,4 +1,4 @@
|
||||
<app-side-nav-companion-bar pageHeader="Home">
|
||||
<app-side-nav-companion-bar>
|
||||
<h2 title>
|
||||
Reading Lists
|
||||
</h2>
|
||||
@ -8,11 +8,12 @@
|
||||
<app-card-detail-layout
|
||||
[isLoading]="loadingLists"
|
||||
[items]="lists"
|
||||
[actions]="actions"
|
||||
[pagination]="pagination"
|
||||
[filteringDisabled]="true"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-card-item [title]="item.title" [entity]="item" [actions]="getActions(item)" [suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)" (clicked)="handleClick(item)"></app-card-item>
|
||||
<app-card-item [title]="item.title" [entity]="item" [actions]="getActions(item)"
|
||||
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
|
||||
(clicked)="handleClick(item)"></app-card-item>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
|
||||
import { PaginatedResult, Pagination } from 'src/app/_models/pagination';
|
||||
import { ReadingList } from 'src/app/_models/reading-list';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
@ -14,32 +13,32 @@ import { ReadingListService } from 'src/app/_services/reading-list.service';
|
||||
@Component({
|
||||
selector: 'app-reading-lists',
|
||||
templateUrl: './reading-lists.component.html',
|
||||
styleUrls: ['./reading-lists.component.scss']
|
||||
styleUrls: ['./reading-lists.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ReadingListsComponent implements OnInit {
|
||||
|
||||
lists: ReadingList[] = [];
|
||||
loadingLists = false;
|
||||
pagination!: Pagination;
|
||||
actions: ActionItem<ReadingList>[] = [];
|
||||
isAdmin: boolean = false;
|
||||
|
||||
constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
|
||||
private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService,
|
||||
private filterUtilityService: FilterUtilitiesService) { }
|
||||
private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadPage();
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
this.loadPage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getActions(readingList: ReadingList) {
|
||||
return this.actionFactoryService.getReadingListActions(this.handleReadingListActionCallback.bind(this)).filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin));
|
||||
return this.actionFactoryService.getReadingListActions(this.handleReadingListActionCallback.bind(this))
|
||||
.filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin));
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>, readingList: ReadingList) {
|
||||
@ -60,6 +59,7 @@ export class ReadingListsComponent implements OnInit {
|
||||
this.actionService.editReadingList(readingList, (updatedList: ReadingList) => {
|
||||
// Reload information around list
|
||||
readingList = updatedList;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
}
|
||||
@ -76,20 +76,17 @@ export class ReadingListsComponent implements OnInit {
|
||||
this.pagination.currentPage = parseInt(page, 10);
|
||||
}
|
||||
this.loadingLists = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.readingListService.getReadingLists(true).pipe(take(1)).subscribe((readingLists: PaginatedResult<ReadingList[]>) => {
|
||||
this.lists = readingLists.result;
|
||||
this.pagination = readingLists.pagination;
|
||||
this.loadingLists = false;
|
||||
window.scrollTo(0, 0);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(pagination: Pagination) {
|
||||
this.filterUtilityService.updateUrlFromFilter(this.pagination, undefined);;
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
handleClick(list: ReadingList) {
|
||||
this.router.navigateByUrl('lists/' + list.id);
|
||||
}
|
||||
|
@ -1,18 +1,15 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { SafeUrl } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { MemberService } from 'src/app/_services/member.service';
|
||||
import { ServerService } from 'src/app/_services/server.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-email-to-account-migration-modal',
|
||||
templateUrl: './add-email-to-account-migration-modal.component.html',
|
||||
styleUrls: ['./add-email-to-account-migration-modal.component.scss']
|
||||
styleUrls: ['./add-email-to-account-migration-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AddEmailToAccountMigrationModalComponent implements OnInit {
|
||||
|
||||
@ -26,13 +23,14 @@ export class AddEmailToAccountMigrationModalComponent implements OnInit {
|
||||
error: string = '';
|
||||
|
||||
constructor(private accountService: AccountService, private modal: NgbActiveModal,
|
||||
private serverService: ServerService, private confirmService: ConfirmService, private toastr: ToastrService) {
|
||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.registerForm.addControl('username', new FormControl(this.username, [Validators.required]));
|
||||
this.registerForm.addControl('email', new FormControl('', [Validators.required, Validators.email]));
|
||||
this.registerForm.addControl('password', new FormControl(this.password, [Validators.required]));
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
close() {
|
||||
@ -41,24 +39,12 @@ export class AddEmailToAccountMigrationModalComponent implements OnInit {
|
||||
|
||||
save() {
|
||||
const model = this.registerForm.getRawValue();
|
||||
model.sendEmail = false;
|
||||
this.accountService.migrateUser(model).subscribe(async () => {
|
||||
// if (!canAccess) {
|
||||
// // Display the email to the user
|
||||
// this.emailLink = email;
|
||||
// await this.confirmService.alert('Please click this link to confirm your email. You must confirm to be able to login. The link is in your logs. You may need to log out of the current account before clicking. <br/> <a href="' + this.emailLink + '" target="_blank">' + this.emailLink + '</a>');
|
||||
// this.modal.close(true);
|
||||
// } else {
|
||||
// await this.confirmService.alert('Please check your email (or logs under "Email Link") for the confirmation link. You must confirm to be able to login.');
|
||||
// this.modal.close(true);
|
||||
// }
|
||||
this.toastr.success('Email has been validated');
|
||||
this.modal.close(true);
|
||||
}, err => {
|
||||
this.error = err;
|
||||
});
|
||||
model.sendEmail = false;
|
||||
this.accountService.migrateUser(model).subscribe(async () => {
|
||||
this.toastr.success('Email has been validated');
|
||||
this.modal.close(true);
|
||||
}, err => {
|
||||
this.error = err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
@ -9,9 +9,10 @@ import { NavService } from 'src/app/_services/nav.service';
|
||||
@Component({
|
||||
selector: 'app-confirm-email',
|
||||
templateUrl: './confirm-email.component.html',
|
||||
styleUrls: ['./confirm-email.component.scss']
|
||||
styleUrls: ['./confirm-email.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ConfirmEmailComponent implements OnInit {
|
||||
export class ConfirmEmailComponent {
|
||||
/**
|
||||
* Email token used for validating
|
||||
*/
|
||||
@ -30,11 +31,13 @@ export class ConfirmEmailComponent implements OnInit {
|
||||
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
||||
private toastr: ToastrService, private themeService: ThemeService, private navService: NavService) {
|
||||
private toastr: ToastrService, private themeService: ThemeService, private navService: NavService,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
this.navService.hideSideNav();
|
||||
this.themeService.setTheme(this.themeService.defaultTheme);
|
||||
const token = this.route.snapshot.queryParamMap.get('token');
|
||||
const email = this.route.snapshot.queryParamMap.get('email');
|
||||
this.cdRef.markForCheck();
|
||||
if (token == undefined || token === '' || token === null) {
|
||||
// This is not a valid url, redirect to login
|
||||
this.toastr.error('Invalid confirmation email');
|
||||
@ -43,9 +46,7 @@ export class ConfirmEmailComponent implements OnInit {
|
||||
}
|
||||
this.token = token;
|
||||
this.registerForm.get('email')?.setValue(email || '');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
submit() {
|
||||
@ -57,6 +58,7 @@ export class ConfirmEmailComponent implements OnInit {
|
||||
}, err => {
|
||||
console.log('error: ', err);
|
||||
this.errors = err;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ThemeService } from 'src/app/_services/theme.service';
|
||||
@ -7,15 +7,19 @@ import { AccountService } from 'src/app/_services/account.service';
|
||||
@Component({
|
||||
selector: 'app-confirm-migration-email',
|
||||
templateUrl: './confirm-migration-email.component.html',
|
||||
styleUrls: ['./confirm-migration-email.component.scss']
|
||||
styleUrls: ['./confirm-migration-email.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ConfirmMigrationEmailComponent implements OnInit {
|
||||
export class ConfirmMigrationEmailComponent {
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, private toastr: ToastrService, private themeService: ThemeService) {
|
||||
constructor(private route: ActivatedRoute, private router: Router,
|
||||
private accountService: AccountService, private toastr: ToastrService,
|
||||
private themeService: ThemeService) {
|
||||
|
||||
this.themeService.setTheme(this.themeService.defaultTheme);
|
||||
const token = this.route.snapshot.queryParamMap.get('token');
|
||||
const email = this.route.snapshot.queryParamMap.get('email');
|
||||
|
||||
if (token === undefined || token === '' || token === null || email === undefined || email === '' || email === null) {
|
||||
// This is not a valid url, redirect to login
|
||||
this.toastr.error('Invalid confirmation email');
|
||||
@ -28,9 +32,4 @@ export class ConfirmMigrationEmailComponent implements OnInit {
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
@ -7,9 +7,10 @@ import { AccountService } from 'src/app/_services/account.service';
|
||||
@Component({
|
||||
selector: 'app-confirm-reset-password',
|
||||
templateUrl: './confirm-reset-password.component.html',
|
||||
styleUrls: ['./confirm-reset-password.component.scss']
|
||||
styleUrls: ['./confirm-reset-password.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ConfirmResetPasswordComponent implements OnInit {
|
||||
export class ConfirmResetPasswordComponent {
|
||||
|
||||
token: string = '';
|
||||
registerForm: FormGroup = new FormGroup({
|
||||
@ -17,7 +18,10 @@ export class ConfirmResetPasswordComponent implements OnInit {
|
||||
password: new FormControl('', [Validators.required, Validators.maxLength(32), Validators.minLength(6)]),
|
||||
});
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, private toastr: ToastrService) {
|
||||
constructor(private route: ActivatedRoute, private router: Router,
|
||||
private accountService: AccountService, private toastr: ToastrService,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
|
||||
const token = this.route.snapshot.queryParamMap.get('token');
|
||||
const email = this.route.snapshot.queryParamMap.get('email');
|
||||
if (token == undefined || token === '' || token === null) {
|
||||
@ -29,11 +33,9 @@ export class ConfirmResetPasswordComponent implements OnInit {
|
||||
|
||||
this.token = token;
|
||||
this.registerForm.get('email')?.setValue(email);
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
submit() {
|
||||
const model = this.registerForm.getRawValue();
|
||||
@ -45,6 +47,4 @@ export class ConfirmResetPasswordComponent implements OnInit {
|
||||
console.error(err, 'There was an error trying to confirm reset password');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { MemberService } from 'src/app/_services/member.service';
|
||||
import { NavService } from 'src/app/_services/nav.service';
|
||||
|
||||
/**
|
||||
* This is exclusivly used to register the first user on the server and nothing else
|
||||
@ -13,7 +12,8 @@ import { NavService } from 'src/app/_services/nav.service';
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
templateUrl: './register.component.html',
|
||||
styleUrls: ['./register.component.scss']
|
||||
styleUrls: ['./register.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class RegisterComponent implements OnInit {
|
||||
|
||||
@ -23,9 +23,10 @@ export class RegisterComponent implements OnInit {
|
||||
password: new FormControl('', [Validators.required, Validators.maxLength(32), Validators.minLength(6)]),
|
||||
});
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
||||
constructor(private router: Router, private accountService: AccountService,
|
||||
private toastr: ToastrService, private memberService: MemberService) {
|
||||
this.memberService.adminExists().pipe(take(1)).subscribe(adminExists => {
|
||||
|
||||
this.memberService.adminExists().pipe(take(1)).subscribe(adminExists => {
|
||||
if (adminExists) {
|
||||
this.router.navigateByUrl('login');
|
||||
return;
|
||||
@ -43,5 +44,4 @@ export class RegisterComponent implements OnInit {
|
||||
this.router.navigateByUrl('login');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,24 +1,23 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reset-password',
|
||||
templateUrl: './reset-password.component.html',
|
||||
styleUrls: ['./reset-password.component.scss']
|
||||
styleUrls: ['./reset-password.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ResetPasswordComponent implements OnInit {
|
||||
export class ResetPasswordComponent {
|
||||
|
||||
registerForm: FormGroup = new FormGroup({
|
||||
email: new FormControl('', [Validators.required, Validators.email]),
|
||||
});
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, private toastr: ToastrService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
constructor(private router: Router, private accountService: AccountService,
|
||||
private toastr: ToastrService) {}
|
||||
|
||||
submit() {
|
||||
const model = this.registerForm.get('email')?.value;
|
||||
|
@ -1,15 +1,9 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-splash-container',
|
||||
templateUrl: './splash-container.component.html',
|
||||
styleUrls: ['./splash-container.component.scss']
|
||||
styleUrls: ['./splash-container.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SplashContainerComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
export class SplashContainerComponent {}
|
@ -1,10 +1,9 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { SettingsService } from '../../admin/settings.service';
|
||||
import { AddEmailToAccountMigrationModalComponent } from '../add-email-to-account-migration-modal/add-email-to-account-migration-modal.component';
|
||||
import { User } from '../../_models/user';
|
||||
import { AccountService } from '../../_services/account.service';
|
||||
@ -15,18 +14,17 @@ import { NavService } from '../../_services/nav.service';
|
||||
@Component({
|
||||
selector: 'app-user-login',
|
||||
templateUrl: './user-login.component.html',
|
||||
styleUrls: ['./user-login.component.scss']
|
||||
styleUrls: ['./user-login.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class UserLoginComponent implements OnInit {
|
||||
|
||||
model: any = {username: '', password: ''};
|
||||
//model: any = {username: '', password: ''};
|
||||
loginForm: FormGroup = new FormGroup({
|
||||
username: new FormControl('', [Validators.required]),
|
||||
password: new FormControl('', [Validators.required])
|
||||
});
|
||||
|
||||
memberNames: Array<string> = [];
|
||||
isCollapsed: {[key: string]: boolean} = {};
|
||||
/**
|
||||
* If there are no admins on the server, this will enable the registration to kick in.
|
||||
*/
|
||||
@ -37,7 +35,8 @@ export class UserLoginComponent implements OnInit {
|
||||
isLoaded: boolean = false;
|
||||
|
||||
constructor(private accountService: AccountService, private router: Router, private memberService: MemberService,
|
||||
private toastr: ToastrService, private navService: NavService, private settingsService: SettingsService, private modalService: NgbModal) {
|
||||
private toastr: ToastrService, private navService: NavService, private modalService: NgbModal,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
this.navService.showNavBar();
|
||||
this.navService.hideSideNav();
|
||||
}
|
||||
@ -45,47 +44,42 @@ export class UserLoginComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.navService.showNavBar();
|
||||
this.navService.hideSideNav();
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.navService.showSideNav();
|
||||
this.cdRef.markForCheck();
|
||||
this.router.navigateByUrl('/libraries');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.memberService.adminExists().pipe(take(1)).subscribe(adminExists => {
|
||||
this.firstTimeFlow = !adminExists;
|
||||
this.isLoaded = true;
|
||||
|
||||
if (this.firstTimeFlow) {
|
||||
this.router.navigateByUrl('registration/register');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupAuthenticatedLoginFlow();
|
||||
this.isLoaded = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
setupAuthenticatedLoginFlow() {
|
||||
if (this.memberNames.indexOf(' Login ') >= 0) { return; }
|
||||
this.memberNames.push(' Login ');
|
||||
this.memberNames.forEach(name => this.isCollapsed[name] = false);
|
||||
const lastLogin = localStorage.getItem(this.accountService.lastLoginKey);
|
||||
if (lastLogin != undefined && lastLogin != null && lastLogin != '') {
|
||||
this.loginForm.get('username')?.setValue(lastLogin);
|
||||
}
|
||||
}
|
||||
|
||||
onAdminCreated(user: User | null) {
|
||||
if (user != null) {
|
||||
this.firstTimeFlow = false;
|
||||
this.cdRef.markForCheck();
|
||||
} else {
|
||||
this.toastr.error('There was an issue creating the new user. Please refresh and try again.');
|
||||
}
|
||||
}
|
||||
|
||||
login() {
|
||||
this.model = this.loginForm.getRawValue();
|
||||
this.accountService.login(this.model).subscribe(() => {
|
||||
const model = this.loginForm.getRawValue();
|
||||
this.accountService.login(model).subscribe(() => {
|
||||
this.loginForm.reset();
|
||||
this.navService.showNavBar();
|
||||
this.navService.showSideNav();
|
||||
@ -101,9 +95,8 @@ export class UserLoginComponent implements OnInit {
|
||||
}, err => {
|
||||
if (err.error === 'You are missing an email on your account. Please wait while we migrate your account.') {
|
||||
const modalRef = this.modalService.open(AddEmailToAccountMigrationModalComponent, { scrollable: true, size: 'md' });
|
||||
modalRef.componentInstance.username = this.model.username;
|
||||
modalRef.componentInstance.username = model.username;
|
||||
modalRef.closed.pipe(take(1)).subscribe(() => {
|
||||
|
||||
});
|
||||
} else {
|
||||
this.toastr.error(err.error);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
@ -7,20 +7,22 @@ import { SeriesService } from 'src/app/_services/series.service';
|
||||
@Component({
|
||||
selector: 'app-review-series-modal',
|
||||
templateUrl: './review-series-modal.component.html',
|
||||
styleUrls: ['./review-series-modal.component.scss']
|
||||
styleUrls: ['./review-series-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ReviewSeriesModalComponent implements OnInit {
|
||||
|
||||
@Input() series!: Series;
|
||||
reviewGroup!: FormGroup;
|
||||
|
||||
constructor(public modal: NgbActiveModal, private seriesService: SeriesService) {}
|
||||
constructor(public modal: NgbActiveModal, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reviewGroup = new FormGroup({
|
||||
review: new FormControl(this.series.userReview, []),
|
||||
rating: new FormControl(this.series.userRating, [])
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
close() {
|
||||
@ -29,6 +31,7 @@ export class ReviewSeriesModalComponent implements OnInit {
|
||||
|
||||
clearRating() {
|
||||
this.reviewGroup.get('rating')?.setValue(0);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
save() {
|
||||
@ -37,5 +40,4 @@ export class ReviewSeriesModalComponent implements OnInit {
|
||||
this.modal.close({success: true, review: model.review, rating: model.rating});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user