mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Feature/enhancements and more (#1166)
* Moved libraryType into chapter info * Fixed a bug where you could not reset cover on a series * Patched in relevant changes from another polish branch * Refactored invite user setup to shift the checking for accessibility to the backend and always show the link. This will help with users who have some unique setups in docker. * Refactored invite user to always print the url to setup a new account. * Single page renderer uses canvasImage rather than re-requesting and relying on cache * Fixed a rendering issue where fit to split on single on a cover wouldn't force width scaling just for that image * Fixed a rendering bug with split image functionality * Added title to copy button * Fixed a bug in GetContinuePoint when a chapter is added to an already read volume and a new chapter is added loose leaf. The loose leaf would be prioritized over the volume chapter. Refactored 2 methods from controller into service and unit tested. * Fixed a bug on opening a volume in series detail that had a chapter added to it after the volume (0 chapter) was read would cause a loose leaf chapter to be opened. * Added mark as read/actionables on Files in volume detail modal. Fixed a bug where we were showing the wrong page count in a volume detail modal. * Removed OnDeck page and replaced it with a pre-filtered All-Series. Hooked up the ability to pass read state to the filter via query params. Fixed some spacing on filter post bootstrap update. * Fixed up some poor documentation on FilterDto. * Some string equals enhancements to reduce extra allocations * Fixed an issue when trying to download via a url, to remove query parameters to get the format * Made an optimization to Normalize method to reduce memory pressure by 100MB over the course of a scan (16k files) * Adjusted the styles on dashboard for first time setup and used a routerlink rather than href to avoid a fresh load. * Use framgment on router link * Hooked in the ability to search by release year (along with series optionally) and series will be returned back. * Fixed a bug in the filter format code where it was sending the wrong type * Only show clear all on typeahead when there are at least one selected item * Cleaned up the styles of the styles of the typeahead * Removed some dead code * Implemented the ability to filter against a series name. * Fixed filter top offset * Ensure that when we add or remove libraries, the side nav of users gets updated. * Tweaked the width on the mobile side nav * Close side nav on clicking overlay on mobile viewport * Don't show a pointer if the carousel section title is not actually selectable * Removed the User profile on the side nav so home is always first. Tweaked styles to match * Fixed up some poor documentation on FilterDto. * Fixed a bug where Latest read date wasn't being set due to an early short circuit. * When sending the chapter file, format the title of the FeedEntry more like Series Detail. * Removed dead code
This commit is contained in:
parent
67d8d3d808
commit
4a93b5c715
@ -54,7 +54,7 @@ namespace API.Benchmark
|
|||||||
{
|
{
|
||||||
foreach (var name in _names)
|
foreach (var name in _names)
|
||||||
{
|
{
|
||||||
if ((name + ".epub").ToLower() == ".epub")
|
if ((name).ToLower() == ".epub")
|
||||||
{
|
{
|
||||||
/* No Operation */
|
/* No Operation */
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@ namespace API.Benchmark
|
|||||||
foreach (var name in _names)
|
foreach (var name in _names)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (IsEpub.IsMatch((name + ".epub")))
|
if (Path.GetExtension(name).Equals(".epub", StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
/* No Operation */
|
/* No Operation */
|
||||||
}
|
}
|
||||||
|
@ -163,6 +163,7 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Citrus+", "citrus+")]
|
[InlineData("Citrus+", "citrus+")]
|
||||||
[InlineData("Again!!!!", "again")]
|
[InlineData("Again!!!!", "again")]
|
||||||
[InlineData("카비타", "카비타")]
|
[InlineData("카비타", "카비타")]
|
||||||
|
[InlineData("06", "06")]
|
||||||
[InlineData("", "")]
|
[InlineData("", "")]
|
||||||
public void NormalizeTest(string input, string expected)
|
public void NormalizeTest(string input, string expected)
|
||||||
{
|
{
|
||||||
|
@ -1650,6 +1650,61 @@ public class ReaderServiceTests
|
|||||||
Assert.Equal("Some Special Title", nextChapter.Range);
|
Assert.Equal("Some Special Title", nextChapter.Range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetContinuePoint_ShouldReturnFirstVolumeChapter_WhenPreExistingProgress()
|
||||||
|
{
|
||||||
|
var series = 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("230", false, new List<MangaFile>(), 1),
|
||||||
|
//EntityFactory.CreateChapter("231", false, new List<MangaFile>(), 1), (added later)
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||||
|
EntityFactory.CreateChapter("2", false, new List<MangaFile>(), 1),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("0", false, new List<MangaFile>(), 1),
|
||||||
|
//EntityFactory.CreateChapter("14.9", false, new List<MangaFile>(), 1), (added later)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_context.Series.Add(series);
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress);
|
||||||
|
await readerService.MarkSeriesAsRead(user, 1);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Add 2 new unread series to the Series
|
||||||
|
series.Volumes[0].Chapters.Add(EntityFactory.CreateChapter("231", false, new List<MangaFile>(), 1));
|
||||||
|
series.Volumes[2].Chapters.Add(EntityFactory.CreateChapter("14.9", false, new List<MangaFile>(), 1));
|
||||||
|
_context.Series.Attach(series);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||||
|
Assert.Equal("14.9", nextChapter.Range);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region MarkChaptersUntilAsRead
|
#region MarkChaptersUntilAsRead
|
||||||
@ -1855,5 +1910,126 @@ public class ReaderServiceTests
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region MarkSeriesAsRead
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MarkSeriesAsReadTest()
|
||||||
|
{
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Pages = 1
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Pages = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Pages = 1
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Pages = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||||
|
|
||||||
|
await readerService.MarkSeriesAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
Assert.Equal(4, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region MarkSeriesAsUnread
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MarkSeriesAsUnreadTest()
|
||||||
|
{
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Pages = 1
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Pages = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||||
|
|
||||||
|
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList();
|
||||||
|
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);
|
||||||
|
|
||||||
|
await readerService.MarkSeriesAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var progresses = (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses;
|
||||||
|
Assert.Equal(0, progresses.Max(p => p.PagesRead));
|
||||||
|
Assert.Equal(2, progresses.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -338,6 +338,12 @@ namespace API.Controllers
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invites a user to the server. Will generate a setup link for continuing setup. If the server is not accessible, no
|
||||||
|
/// email will be sent.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpPost("invite")]
|
[HttpPost("invite")]
|
||||||
public async Task<ActionResult<string>> InviteUser(InviteUserDto dto)
|
public async Task<ActionResult<string>> InviteUser(InviteUserDto dto)
|
||||||
@ -417,7 +423,9 @@ namespace API.Controllers
|
|||||||
|
|
||||||
var emailLink = GenerateEmailLink(token, "confirm-email", dto.Email);
|
var emailLink = GenerateEmailLink(token, "confirm-email", dto.Email);
|
||||||
_logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
_logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||||
if (dto.SendEmail)
|
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||||
|
var accessible = await _emailService.CheckIfAccessible(host);
|
||||||
|
if (accessible)
|
||||||
{
|
{
|
||||||
await _emailService.SendConfirmationEmail(new ConfirmationEmailDto()
|
await _emailService.SendConfirmationEmail(new ConfirmationEmailDto()
|
||||||
{
|
{
|
||||||
@ -426,7 +434,11 @@ namespace API.Controllers
|
|||||||
ServerConfirmationLink = emailLink
|
ServerConfirmationLink = emailLink
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Ok(emailLink);
|
return Ok(new InviteUserResponse
|
||||||
|
{
|
||||||
|
EmailLink = emailLink,
|
||||||
|
EmailSent = accessible
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
|
@ -65,6 +65,8 @@ namespace API.Controllers
|
|||||||
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Authorize(Policy="RequireDownloadRole")]
|
[Authorize(Policy="RequireDownloadRole")]
|
||||||
[HttpGet("volume")]
|
[HttpGet("volume")]
|
||||||
public async Task<ActionResult> DownloadVolume(int volumeId)
|
public async Task<ActionResult> DownloadVolume(int volumeId)
|
||||||
|
@ -11,6 +11,7 @@ using API.Entities;
|
|||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
using API.SignalR;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -26,16 +27,18 @@ namespace API.Controllers
|
|||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
private readonly ITaskScheduler _taskScheduler;
|
private readonly ITaskScheduler _taskScheduler;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IEventHub _eventHub;
|
||||||
|
|
||||||
public LibraryController(IDirectoryService directoryService,
|
public LibraryController(IDirectoryService directoryService,
|
||||||
ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
|
ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
|
||||||
IUnitOfWork unitOfWork)
|
IUnitOfWork unitOfWork, IEventHub eventHub)
|
||||||
{
|
{
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
_taskScheduler = taskScheduler;
|
_taskScheduler = taskScheduler;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
|
_eventHub = eventHub;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -73,6 +76,8 @@ namespace API.Controllers
|
|||||||
|
|
||||||
_logger.LogInformation("Created a new library: {LibraryName}", library.Name);
|
_logger.LogInformation("Created a new library: {LibraryName}", library.Name);
|
||||||
_taskScheduler.ScanLibrary(library.Id);
|
_taskScheduler.ScanLibrary(library.Id);
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
|
||||||
|
MessageFactory.LibraryModifiedEvent(library.Id, "create"), false);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,6 +196,9 @@ namespace API.Controllers
|
|||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
_taskScheduler.CleanupChapters(chapterIds);
|
_taskScheduler.CleanupChapters(chapterIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
|
||||||
|
MessageFactory.LibraryModifiedEvent(libraryId, "delete"), false);
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -745,9 +745,18 @@ public class OpdsController : BaseApiController
|
|||||||
var fileType = _downloadService.GetContentTypeFromFile(mangaFile.FilePath);
|
var fileType = _downloadService.GetContentTypeFromFile(mangaFile.FilePath);
|
||||||
var filename = Uri.EscapeDataString(Path.GetFileName(mangaFile.FilePath) ?? string.Empty);
|
var filename = Uri.EscapeDataString(Path.GetFileName(mangaFile.FilePath) ?? string.Empty);
|
||||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||||
|
var volume = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, await GetUser(apiKey));
|
||||||
|
|
||||||
|
var title = $"{series.Name} - ";
|
||||||
var title = $"{series.Name} - {SeriesService.FormatChapterTitle(chapter, libraryType)}";
|
if (volume.Chapters.Count == 1)
|
||||||
|
{
|
||||||
|
SeriesService.RenameVolumeName(volume.Chapters.First(), volume, libraryType);
|
||||||
|
title += $"{volume.Name}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
title = $"{series.Name} - {SeriesService.FormatChapterTitle(chapter, libraryType)}";
|
||||||
|
}
|
||||||
|
|
||||||
// Chunky requires a file at the end. Our API ignores this
|
// Chunky requires a file at the end. Our API ignores this
|
||||||
var accLink =
|
var accLink =
|
||||||
|
@ -109,14 +109,7 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult> MarkRead(MarkReadDto markReadDto)
|
public async Task<ActionResult> MarkRead(MarkReadDto markReadDto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumes(markReadDto.SeriesId);
|
await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId);
|
||||||
user.Progresses ??= new List<AppUserProgress>();
|
|
||||||
foreach (var volume in volumes)
|
|
||||||
{
|
|
||||||
_readerService.MarkChaptersAsRead(user, markReadDto.SeriesId, volume.Chapters);
|
|
||||||
}
|
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
@ -137,14 +130,7 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult> MarkUnread(MarkReadDto markReadDto)
|
public async Task<ActionResult> MarkUnread(MarkReadDto markReadDto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumes(markReadDto.SeriesId);
|
await _readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId);
|
||||||
user.Progresses ??= new List<AppUserProgress>();
|
|
||||||
foreach (var volume in volumes)
|
|
||||||
{
|
|
||||||
_readerService.MarkChaptersAsUnread(user, markReadDto.SeriesId, volume.Chapters);
|
|
||||||
}
|
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
|
@ -46,7 +46,7 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult<string>> GetImageFromFile(UploadUrlDto dto)
|
public async Task<ActionResult<string>> GetImageFromFile(UploadUrlDto dto)
|
||||||
{
|
{
|
||||||
var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace("/", "_").Replace(":", "_");
|
var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace("/", "_").Replace(":", "_");
|
||||||
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url).Replace(".", "");
|
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", "");
|
||||||
var path = await dto.Url
|
var path = await dto.Url
|
||||||
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");
|
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");
|
||||||
|
|
||||||
|
@ -16,6 +16,4 @@ public class InviteUserDto
|
|||||||
/// A list of libraries to grant access to
|
/// A list of libraries to grant access to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Libraries { get; init; }
|
public IList<int> Libraries { get; init; }
|
||||||
|
|
||||||
public bool SendEmail { get; init; } = true;
|
|
||||||
}
|
}
|
||||||
|
13
API/DTOs/Account/InviteUserResponse.cs
Normal file
13
API/DTOs/Account/InviteUserResponse.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace API.DTOs.Account;
|
||||||
|
|
||||||
|
public class InviteUserResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Email link used to setup the user account
|
||||||
|
/// </summary>
|
||||||
|
public string EmailLink { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Was an email sent (ie is this server accessible)
|
||||||
|
/// </summary>
|
||||||
|
public bool EmailSent { get; set; }
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
|
||||||
@ -25,51 +26,51 @@ namespace API.DTOs.Filtering
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Genres { get; init; } = new List<int>();
|
public IList<int> Genres { get; init; } = new List<int>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of Writers to restrict search to. Defaults to all genres by passing an empty list
|
/// A list of Writers to restrict search to. Defaults to all Writers by passing an empty list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Writers { get; init; } = new List<int>();
|
public IList<int> Writers { get; init; } = new List<int>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of Penciller ids to restrict search to. Defaults to all genres by passing an empty list
|
/// A list of Penciller ids to restrict search to. Defaults to all Pencillers by passing an empty list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Penciller { get; init; } = new List<int>();
|
public IList<int> Penciller { get; init; } = new List<int>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of Inker ids to restrict search to. Defaults to all genres by passing an empty list
|
/// A list of Inker ids to restrict search to. Defaults to all Inkers by passing an empty list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Inker { get; init; } = new List<int>();
|
public IList<int> Inker { get; init; } = new List<int>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of Colorist ids to restrict search to. Defaults to all genres by passing an empty list
|
/// A list of Colorist ids to restrict search to. Defaults to all Colorists by passing an empty list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Colorist { get; init; } = new List<int>();
|
public IList<int> Colorist { get; init; } = new List<int>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of Letterer ids to restrict search to. Defaults to all genres by passing an empty list
|
/// A list of Letterer ids to restrict search to. Defaults to all Letterers by passing an empty list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Letterer { get; init; } = new List<int>();
|
public IList<int> Letterer { get; init; } = new List<int>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of CoverArtist ids to restrict search to. Defaults to all genres by passing an empty list
|
/// A list of CoverArtist ids to restrict search to. Defaults to all CoverArtists by passing an empty list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> CoverArtist { get; init; } = new List<int>();
|
public IList<int> CoverArtist { get; init; } = new List<int>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of Editor ids to restrict search to. Defaults to all genres by passing an empty list
|
/// A list of Editor ids to restrict search to. Defaults to all Editors by passing an empty list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Editor { get; init; } = new List<int>();
|
public IList<int> Editor { get; init; } = new List<int>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of Publisher ids to restrict search to. Defaults to all genres by passing an empty list
|
/// A list of Publisher ids to restrict search to. Defaults to all Publishers by passing an empty list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Publisher { get; init; } = new List<int>();
|
public IList<int> Publisher { get; init; } = new List<int>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of Character ids to restrict search to. Defaults to all genres by passing an empty list
|
/// A list of Character ids to restrict search to. Defaults to all Characters by passing an empty list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Character { get; init; } = new List<int>();
|
public IList<int> Character { get; init; } = new List<int>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of Translator ids to restrict search to. Defaults to all genres by passing an empty list
|
/// A list of Translator ids to restrict search to. Defaults to all Translatorss by passing an empty list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Translators { get; init; } = new List<int>();
|
public IList<int> Translators { get; init; } = new List<int>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of Collection Tag ids to restrict search to. Defaults to all genres by passing an empty list
|
/// A list of Collection Tag ids to restrict search to. Defaults to all Collection Tags by passing an empty list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> CollectionTags { get; init; } = new List<int>();
|
public IList<int> CollectionTags { get; init; } = new List<int>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of Tag ids to restrict search to. Defaults to all genres by passing an empty list
|
/// A list of Tag ids to restrict search to. Defaults to all Tags by passing an empty list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Tags { get; init; } = new List<int>();
|
public IList<int> Tags { get; init; } = new List<int>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -94,5 +95,10 @@ namespace API.DTOs.Filtering
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<PublicationStatus> PublicationStatus { get; init; } = new List<PublicationStatus>();
|
public IList<PublicationStatus> PublicationStatus { get; init; } = new List<PublicationStatus>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An optional name string to filter by. Empty string will ignore.
|
||||||
|
/// </summary>
|
||||||
|
public string SeriesNameQuery { get; init; } = string.Empty;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ namespace API.DTOs.Reader
|
|||||||
public MangaFormat SeriesFormat { get; set; }
|
public MangaFormat SeriesFormat { get; set; }
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
public int LibraryId { get; set; }
|
public int LibraryId { get; set; }
|
||||||
|
public LibraryType LibraryType { get; set; }
|
||||||
public string ChapterTitle { get; set; } = string.Empty;
|
public string ChapterTitle { get; set; } = string.Empty;
|
||||||
public int Pages { get; set; }
|
public int Pages { get; set; }
|
||||||
public string FileName { get; set; }
|
public string FileName { get; set; }
|
||||||
|
@ -81,7 +81,8 @@ public class ChapterRepository : IChapterRepository
|
|||||||
data.TitleName,
|
data.TitleName,
|
||||||
SeriesFormat = series.Format,
|
SeriesFormat = series.Format,
|
||||||
SeriesName = series.Name,
|
SeriesName = series.Name,
|
||||||
series.LibraryId
|
series.LibraryId,
|
||||||
|
LibraryType = series.Library.Type
|
||||||
})
|
})
|
||||||
.Select(data => new ChapterInfoDto()
|
.Select(data => new ChapterInfoDto()
|
||||||
{
|
{
|
||||||
@ -89,12 +90,13 @@ public class ChapterRepository : IChapterRepository
|
|||||||
VolumeNumber = data.VolumeNumber + string.Empty,
|
VolumeNumber = data.VolumeNumber + string.Empty,
|
||||||
VolumeId = data.VolumeId,
|
VolumeId = data.VolumeId,
|
||||||
IsSpecial = data.IsSpecial,
|
IsSpecial = data.IsSpecial,
|
||||||
SeriesId =data.SeriesId,
|
SeriesId = data.SeriesId,
|
||||||
SeriesFormat = data.SeriesFormat,
|
SeriesFormat = data.SeriesFormat,
|
||||||
SeriesName = data.SeriesName,
|
SeriesName = data.SeriesName,
|
||||||
LibraryId = data.LibraryId,
|
LibraryId = data.LibraryId,
|
||||||
Pages = data.Pages,
|
Pages = data.Pages,
|
||||||
ChapterTitle = data.TitleName
|
ChapterTitle = data.TitleName,
|
||||||
|
LibraryType = data.LibraryType
|
||||||
})
|
})
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data.Scanner;
|
using API.Data.Scanner;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
@ -79,8 +80,8 @@ public interface ISeriesRepository
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task AddSeriesModifiers(int userId, List<SeriesDto> series);
|
Task AddSeriesModifiers(int userId, List<SeriesDto> series);
|
||||||
Task<string> GetSeriesCoverImageAsync(int seriesId);
|
Task<string> GetSeriesCoverImageAsync(int seriesId);
|
||||||
Task<IEnumerable<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter);
|
Task<IEnumerable<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter, bool cutoffOnDate = true);
|
||||||
Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter); // NOTE: Probably put this in LibraryRepo
|
Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter);
|
||||||
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
||||||
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
||||||
Task<IList<MangaFile>> GetFilesForSeries(int seriesId);
|
Task<IList<MangaFile>> GetFilesForSeries(int seriesId);
|
||||||
@ -283,17 +284,22 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
|
|
||||||
result.Libraries = await _context.Library
|
result.Libraries = await _context.Library
|
||||||
.Where(l => libraryIds.Contains(l.Id))
|
.Where(l => libraryIds.Contains(l.Id))
|
||||||
.Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%"))
|
.Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%"))
|
||||||
.OrderBy(l => l.Name)
|
.OrderBy(l => l.Name)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
var justYear = Regex.Match(searchQuery, @"\d{4}").Value;
|
||||||
|
var hasYearInQuery = !string.IsNullOrEmpty(justYear);
|
||||||
|
var yearComparison = hasYearInQuery ? int.Parse(justYear) : 0;
|
||||||
|
|
||||||
result.Series = await _context.Series
|
result.Series = await _context.Series
|
||||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||||
.Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%")
|
.Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%")
|
||||||
|| EF.Functions.Like(s.OriginalName, $"%{searchQuery}%")
|
|| EF.Functions.Like(s.OriginalName, $"%{searchQuery}%")
|
||||||
|| EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%"))
|
|| EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%")
|
||||||
|
|| (hasYearInQuery && s.Metadata.ReleaseYear == yearComparison))
|
||||||
.Include(s => s.Library)
|
.Include(s => s.Library)
|
||||||
.OrderBy(s => s.SortName)
|
.OrderBy(s => s.SortName)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@ -301,6 +307,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.ProjectTo<SearchResultDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SearchResultDto>(_mapper.ConfigurationProvider)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
|
||||||
result.ReadingLists = await _context.ReadingList
|
result.ReadingLists = await _context.ReadingList
|
||||||
.Where(rl => rl.AppUserId == userId || rl.Promoted)
|
.Where(rl => rl.AppUserId == userId || rl.Promoted)
|
||||||
.Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%"))
|
.Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%"))
|
||||||
@ -466,10 +473,16 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
{
|
{
|
||||||
s.PagesRead = userProgress.Where(p => p.SeriesId == s.Id).Sum(p => p.PagesRead);
|
s.PagesRead = userProgress.Where(p => p.SeriesId == s.Id).Sum(p => p.PagesRead);
|
||||||
var rating = userRatings.SingleOrDefault(r => r.SeriesId == s.Id);
|
var rating = userRatings.SingleOrDefault(r => r.SeriesId == s.Id);
|
||||||
if (rating == null) continue;
|
if (rating != null)
|
||||||
s.UserRating = rating.Rating;
|
{
|
||||||
s.UserReview = rating.Review;
|
s.UserRating = rating.Rating;
|
||||||
s.LatestReadDate = userProgress.Max(p => p.LastModified);
|
s.UserReview = rating.Review;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userProgress.Count > 0)
|
||||||
|
{
|
||||||
|
s.LatestReadDate = userProgress.Max(p => p.LastModified);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,7 +521,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
private IList<MangaFormat> ExtractFilters(int libraryId, int userId, FilterDto filter, ref List<int> userLibraries,
|
private IList<MangaFormat> ExtractFilters(int libraryId, int userId, FilterDto filter, ref List<int> userLibraries,
|
||||||
out List<int> allPeopleIds, out bool hasPeopleFilter, out bool hasGenresFilter, out bool hasCollectionTagFilter,
|
out List<int> allPeopleIds, out bool hasPeopleFilter, out bool hasGenresFilter, out bool hasCollectionTagFilter,
|
||||||
out bool hasRatingFilter, out bool hasProgressFilter, out IList<int> seriesIds, out bool hasAgeRating, out bool hasTagsFilter,
|
out bool hasRatingFilter, out bool hasProgressFilter, out IList<int> seriesIds, out bool hasAgeRating, out bool hasTagsFilter,
|
||||||
out bool hasLanguageFilter, out bool hasPublicationFilter)
|
out bool hasLanguageFilter, out bool hasPublicationFilter, out bool hasSeriesNameFilter)
|
||||||
{
|
{
|
||||||
var formats = filter.GetSqlFilter();
|
var formats = filter.GetSqlFilter();
|
||||||
|
|
||||||
@ -581,6 +594,8 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasSeriesNameFilter = !string.IsNullOrEmpty(filter.SeriesNameQuery);
|
||||||
|
|
||||||
return formats;
|
return formats;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,11 +608,11 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
/// <param name="userParams">Pagination information</param>
|
/// <param name="userParams">Pagination information</param>
|
||||||
/// <param name="filter">Optional (default null) filter on query</param>
|
/// <param name="filter">Optional (default null) filter on query</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<IEnumerable<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter)
|
public async Task<IEnumerable<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter, bool cutoffOnDate = true)
|
||||||
{
|
{
|
||||||
//var allSeriesWithProgress = await _context.AppUserProgresses.Select(p => p.SeriesId).ToListAsync();
|
//var allSeriesWithProgress = await _context.AppUserProgresses.Select(p => p.SeriesId).ToListAsync();
|
||||||
//var allChapters = await GetChapterIdsForSeriesAsync(allSeriesWithProgress);
|
//var allChapters = await GetChapterIdsForSeriesAsync(allSeriesWithProgress);
|
||||||
var cuttoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
|
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
|
||||||
var query = (await CreateFilteredSearchQueryable(userId, libraryId, filter))
|
var query = (await CreateFilteredSearchQueryable(userId, libraryId, filter))
|
||||||
.Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) =>
|
.Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) =>
|
||||||
new
|
new
|
||||||
@ -612,8 +627,12 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
// This is only taking into account chapters that have progress on them, not all chapters in said series
|
// This is only taking into account chapters that have progress on them, not all chapters in said series
|
||||||
LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created)
|
LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created)
|
||||||
//LastChapterCreated = _context.Chapter.Where(c => allChapters.Contains(c.Id)).Max(c => c.Created)
|
//LastChapterCreated = _context.Chapter.Where(c => allChapters.Contains(c.Id)).Max(c => c.Created)
|
||||||
})
|
});
|
||||||
.Where(d => d.LastReadingProgress >= cuttoffProgressPoint);
|
if (cutoffOnDate)
|
||||||
|
{
|
||||||
|
query = query.Where(d => d.LastReadingProgress >= cutoffProgressPoint);
|
||||||
|
}
|
||||||
|
|
||||||
// I think I need another Join statement. The problem is the chapters are still limited to progress
|
// I think I need another Join statement. The problem is the chapters are still limited to progress
|
||||||
|
|
||||||
|
|
||||||
@ -638,7 +657,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
var formats = ExtractFilters(libraryId, userId, filter, ref userLibraries,
|
var formats = ExtractFilters(libraryId, userId, filter, ref userLibraries,
|
||||||
out var allPeopleIds, out var hasPeopleFilter, out var hasGenresFilter,
|
out var allPeopleIds, out var hasPeopleFilter, out var hasGenresFilter,
|
||||||
out var hasCollectionTagFilter, out var hasRatingFilter, out var hasProgressFilter,
|
out var hasCollectionTagFilter, out var hasRatingFilter, out var hasProgressFilter,
|
||||||
out var seriesIds, out var hasAgeRating, out var hasTagsFilter, out var hasLanguageFilter, out var hasPublicationFilter);
|
out var seriesIds, out var hasAgeRating, out var hasTagsFilter, out var hasLanguageFilter, out var hasPublicationFilter, out var hasSeriesNameFilter);
|
||||||
|
|
||||||
var query = _context.Series
|
var query = _context.Series
|
||||||
.Where(s => userLibraries.Contains(s.LibraryId)
|
.Where(s => userLibraries.Contains(s.LibraryId)
|
||||||
@ -652,8 +671,11 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
&& (!hasAgeRating || filter.AgeRating.Contains(s.Metadata.AgeRating))
|
&& (!hasAgeRating || filter.AgeRating.Contains(s.Metadata.AgeRating))
|
||||||
&& (!hasTagsFilter || s.Metadata.Tags.Any(t => filter.Tags.Contains(t.Id)))
|
&& (!hasTagsFilter || s.Metadata.Tags.Any(t => filter.Tags.Contains(t.Id)))
|
||||||
&& (!hasLanguageFilter || filter.Languages.Contains(s.Metadata.Language))
|
&& (!hasLanguageFilter || filter.Languages.Contains(s.Metadata.Language))
|
||||||
&& (!hasPublicationFilter || filter.PublicationStatus.Contains(s.Metadata.PublicationStatus))
|
&& (!hasPublicationFilter || filter.PublicationStatus.Contains(s.Metadata.PublicationStatus)))
|
||||||
)
|
.Where(s => !hasSeriesNameFilter ||
|
||||||
|
EF.Functions.Like(s.Name, $"%{filter.SeriesNameQuery}%")
|
||||||
|
|| EF.Functions.Like(s.OriginalName, $"%{filter.SeriesNameQuery}%")
|
||||||
|
|| EF.Functions.Like(s.LocalizedName, $"%{filter.SeriesNameQuery}%"))
|
||||||
.AsNoTracking();
|
.AsNoTracking();
|
||||||
|
|
||||||
// If no sort options, default to using SortName
|
// If no sort options, default to using SortName
|
||||||
|
@ -220,7 +220,8 @@ public class UserRepository : IUserRepository
|
|||||||
|
|
||||||
public async Task<AppUser> GetUserByEmailAsync(string email)
|
public async Task<AppUser> GetUserByEmailAsync(string email)
|
||||||
{
|
{
|
||||||
return await _context.AppUser.SingleOrDefaultAsync(u => u.Email.ToLower().Equals(email.ToLower()));
|
var lowerEmail = email.ToLower();
|
||||||
|
return await _context.AppUser.SingleOrDefaultAsync(u => u.Email.ToLower().Equals(lowerEmail));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<AppUser>> GetAllUsers()
|
public async Task<IEnumerable<AppUser>> GetAllUsers()
|
||||||
|
@ -57,7 +57,7 @@ namespace API.Parser
|
|||||||
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)((?<!back)cover|folder)(?![\w\d])",
|
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)((?<!back)cover|folder)(?![\w\d])",
|
||||||
MatchOptions, RegexTimeout);
|
MatchOptions, RegexTimeout);
|
||||||
|
|
||||||
private static readonly Regex NormalizeRegex = new Regex(@"[^a-zA-Z0-9\+]",
|
private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+]",
|
||||||
MatchOptions, RegexTimeout);
|
MatchOptions, RegexTimeout);
|
||||||
|
|
||||||
|
|
||||||
@ -966,8 +966,7 @@ namespace API.Parser
|
|||||||
|
|
||||||
public static string Normalize(string name)
|
public static string Normalize(string name)
|
||||||
{
|
{
|
||||||
var normalized = NormalizeRegex.Replace(name, string.Empty).ToLower();
|
return NormalizeRegex.Replace(name, string.Empty).ToLower();
|
||||||
return string.IsNullOrEmpty(normalized) ? name : normalized;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -1009,12 +1008,12 @@ namespace API.Parser
|
|||||||
|
|
||||||
public static bool IsEpub(string filePath)
|
public static bool IsEpub(string filePath)
|
||||||
{
|
{
|
||||||
return Path.GetExtension(filePath).ToLower() == ".epub";
|
return Path.GetExtension(filePath).Equals(".epub", StringComparison.InvariantCultureIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsPdf(string filePath)
|
public static bool IsPdf(string filePath)
|
||||||
{
|
{
|
||||||
return Path.GetExtension(filePath).ToLower() == ".pdf";
|
return Path.GetExtension(filePath).Equals(".pdf", StringComparison.InvariantCultureIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -1025,8 +1024,7 @@ namespace API.Parser
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string CleanAuthor(string author)
|
public static string CleanAuthor(string author)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(author)) return string.Empty;
|
return string.IsNullOrEmpty(author) ? string.Empty : author.Trim();
|
||||||
return author.Trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -98,7 +98,7 @@ public class EmailService : IEmailService
|
|||||||
return await SendEmailWithPost(emailLink + "/api/email/email-password-reset", data);
|
return await SendEmailWithPost(emailLink + "/api/email/email-password-reset", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<bool> SendEmailWithGet(string url)
|
private static async Task<bool> SendEmailWithGet(string url, int timeoutSecs = 30)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -108,7 +108,7 @@ public class EmailService : IEmailService
|
|||||||
.WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
|
.WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
|
||||||
.WithHeader("x-kavita-version", BuildInfo.Version)
|
.WithHeader("x-kavita-version", BuildInfo.Version)
|
||||||
.WithHeader("Content-Type", "application/json")
|
.WithHeader("Content-Type", "application/json")
|
||||||
.WithTimeout(TimeSpan.FromSeconds(30))
|
.WithTimeout(TimeSpan.FromSeconds(timeoutSecs))
|
||||||
.GetStringAsync();
|
.GetStringAsync();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(response) && bool.Parse(response))
|
if (!string.IsNullOrEmpty(response) && bool.Parse(response))
|
||||||
@ -124,7 +124,7 @@ public class EmailService : IEmailService
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static async Task<bool> SendEmailWithPost(string url, object data)
|
private static async Task<bool> SendEmailWithPost(string url, object data, int timeoutSecs = 30)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -134,7 +134,7 @@ public class EmailService : IEmailService
|
|||||||
.WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
|
.WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
|
||||||
.WithHeader("x-kavita-version", BuildInfo.Version)
|
.WithHeader("x-kavita-version", BuildInfo.Version)
|
||||||
.WithHeader("Content-Type", "application/json")
|
.WithHeader("Content-Type", "application/json")
|
||||||
.WithTimeout(TimeSpan.FromSeconds(30))
|
.WithTimeout(TimeSpan.FromSeconds(timeoutSecs))
|
||||||
.PostJsonAsync(data);
|
.PostJsonAsync(data);
|
||||||
|
|
||||||
if (response.StatusCode != StatusCodes.Status200OK)
|
if (response.StatusCode != StatusCodes.Status200OK)
|
||||||
|
@ -16,6 +16,8 @@ namespace API.Services;
|
|||||||
|
|
||||||
public interface IReaderService
|
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 MarkChaptersAsRead(AppUser user, int seriesId, IEnumerable<Chapter> chapters);
|
||||||
void MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable<Chapter> chapters);
|
void MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable<Chapter> chapters);
|
||||||
Task<bool> SaveReadingProgress(ProgressDto progressDto, int userId);
|
Task<bool> SaveReadingProgress(ProgressDto progressDto, int userId);
|
||||||
@ -45,6 +47,40 @@ public class ReaderService : IReaderService
|
|||||||
return Parser.Parser.NormalizePath(Path.Join(baseDirectory, $"{userId}", $"{seriesId}", $"{chapterId}"));
|
return Parser.Parser.NormalizePath(Path.Join(baseDirectory, $"{userId}", $"{seriesId}", $"{chapterId}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Does not commit. Marks all entities under the series as read.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
public async Task MarkSeriesAsRead(AppUser user, int seriesId)
|
||||||
|
{
|
||||||
|
var volumes = await _unitOfWork.VolumeRepository.GetVolumes(seriesId);
|
||||||
|
user.Progresses ??= new List<AppUserProgress>();
|
||||||
|
foreach (var volume in volumes)
|
||||||
|
{
|
||||||
|
MarkChaptersAsRead(user, seriesId, volume.Chapters);
|
||||||
|
}
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Does not commit. Marks all entities under the series as unread.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
public async Task MarkSeriesAsUnread(AppUser user, int seriesId)
|
||||||
|
{
|
||||||
|
var volumes = await _unitOfWork.VolumeRepository.GetVolumes(seriesId);
|
||||||
|
user.Progresses ??= new List<AppUserProgress>();
|
||||||
|
foreach (var volume in volumes)
|
||||||
|
{
|
||||||
|
MarkChaptersAsUnread(user, seriesId, volume.Chapters);
|
||||||
|
}
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks all Chapters as Read by creating or updating UserProgress rows. Does not commit.
|
/// Marks all Chapters as Read by creating or updating UserProgress rows. Does not commit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -367,7 +403,7 @@ public class ReaderService : IReaderService
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// If there are any volumes that have progress, return those. If not, move on.
|
// If there are any volumes that have progress, return those. If not, move on.
|
||||||
var currentlyReadingChapter = volumeChapters.FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages && chapter.PagesRead > 0);
|
var currentlyReadingChapter = volumeChapters.FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages); // (removed for GetContinuePoint_ShouldReturnFirstVolumeChapter_WhenPreExistingProgress), not sure if needed && chapter.PagesRead > 0
|
||||||
if (currentlyReadingChapter != null) return currentlyReadingChapter;
|
if (currentlyReadingChapter != null) return currentlyReadingChapter;
|
||||||
|
|
||||||
// Check loose leaf chapters (and specials). First check if there are any
|
// Check loose leaf chapters (and specials). First check if there are any
|
||||||
|
@ -445,20 +445,7 @@ public class SeriesService : ISeriesService
|
|||||||
var firstChapter = volume.Chapters.First();
|
var firstChapter = volume.Chapters.First();
|
||||||
// On Books, skip volumes that are specials, since these will be shown
|
// On Books, skip volumes that are specials, since these will be shown
|
||||||
if (firstChapter.IsSpecial) continue;
|
if (firstChapter.IsSpecial) continue;
|
||||||
if (string.IsNullOrEmpty(firstChapter.TitleName))
|
RenameVolumeName(firstChapter, volume, libraryType);
|
||||||
{
|
|
||||||
if (!firstChapter.Range.Equals(Parser.Parser.DefaultVolume))
|
|
||||||
{
|
|
||||||
var title = Path.GetFileNameWithoutExtension(firstChapter.Range);
|
|
||||||
if (string.IsNullOrEmpty(title)) continue;
|
|
||||||
volume.Name += $" - {title}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
volume.Name += $" - {firstChapter.TitleName}";
|
|
||||||
}
|
|
||||||
|
|
||||||
processedVolumes.Add(volume);
|
processedVolumes.Add(volume);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -517,48 +504,64 @@ public class SeriesService : ISeriesService
|
|||||||
return !c.IsSpecial && !c.Number.Equals(Parser.Parser.DefaultChapter);
|
return !c.IsSpecial && !c.Number.Equals(Parser.Parser.DefaultChapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string FormatChapterTitle(ChapterDto chapter, LibraryType libraryType)
|
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType)
|
||||||
{
|
{
|
||||||
if (chapter.IsSpecial)
|
if (libraryType == LibraryType.Book)
|
||||||
{
|
{
|
||||||
return Parser.Parser.CleanSpecialTitle(chapter.Title);
|
if (string.IsNullOrEmpty(firstChapter.TitleName))
|
||||||
|
{
|
||||||
|
if (firstChapter.Range.Equals(Parser.Parser.DefaultVolume)) return;
|
||||||
|
var title = Path.GetFileNameWithoutExtension(firstChapter.Range);
|
||||||
|
if (string.IsNullOrEmpty(title)) return;
|
||||||
|
volume.Name += $" - {title}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
volume.Name += $" - {firstChapter.TitleName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
volume.Name = $"Volume {volume.Name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static string FormatChapterTitle(bool isSpecial, LibraryType libraryType, string chapterTitle, bool withHash)
|
||||||
|
{
|
||||||
|
if (isSpecial)
|
||||||
|
{
|
||||||
|
return Parser.Parser.CleanSpecialTitle(chapterTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hashSpot = withHash ? "#" : string.Empty;
|
||||||
return libraryType switch
|
return libraryType switch
|
||||||
{
|
{
|
||||||
LibraryType.Book => $"Book {chapter.Title}",
|
LibraryType.Book => $"Book {chapterTitle}",
|
||||||
LibraryType.Comic => $"Issue #{chapter.Title}",
|
LibraryType.Comic => $"Issue {hashSpot}{chapterTitle}",
|
||||||
LibraryType.Manga => $"Chapter {chapter.Title}",
|
LibraryType.Manga => $"Chapter {chapterTitle}",
|
||||||
_ => "Chapter "
|
_ => "Chapter "
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string FormatChapterTitle(Chapter chapter, LibraryType libraryType)
|
public static string FormatChapterTitle(ChapterDto chapter, LibraryType libraryType, bool withHash = true)
|
||||||
{
|
{
|
||||||
if (chapter.IsSpecial)
|
return FormatChapterTitle(chapter.IsSpecial, libraryType, chapter.Title, withHash);
|
||||||
{
|
}
|
||||||
return Parser.Parser.CleanSpecialTitle(chapter.Title);
|
|
||||||
}
|
public static string FormatChapterTitle(Chapter chapter, LibraryType libraryType, bool withHash = true)
|
||||||
return libraryType switch
|
{
|
||||||
{
|
return FormatChapterTitle(chapter.IsSpecial, libraryType, chapter.Title, withHash);
|
||||||
LibraryType.Book => $"Book {chapter.Title}",
|
|
||||||
LibraryType.Comic => $"Issue #{chapter.Title}",
|
|
||||||
LibraryType.Manga => $"Chapter {chapter.Title}",
|
|
||||||
_ => "Chapter "
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string FormatChapterName(LibraryType libraryType, bool withHash = false)
|
public static string FormatChapterName(LibraryType libraryType, bool withHash = false)
|
||||||
{
|
{
|
||||||
switch (libraryType)
|
return libraryType switch
|
||||||
{
|
{
|
||||||
case LibraryType.Manga:
|
LibraryType.Manga => "Chapter",
|
||||||
return "Chapter";
|
LibraryType.Comic => withHash ? "Issue #" : "Issue",
|
||||||
case LibraryType.Comic:
|
LibraryType.Book => "Book",
|
||||||
return withHash ? "Issue #" : "Issue";
|
_ => "Chapter"
|
||||||
case LibraryType.Book:
|
};
|
||||||
return "Book";
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,10 @@ namespace API.SignalR
|
|||||||
/// When DB updates are occuring during a library/series scan
|
/// When DB updates are occuring during a library/series scan
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const string ScanProgress = "ScanProgress";
|
private const string ScanProgress = "ScanProgress";
|
||||||
|
/// <summary>
|
||||||
|
/// When a library is created/deleted in the Server
|
||||||
|
/// </summary>
|
||||||
|
public const string LibraryModified = "LibraryModified";
|
||||||
|
|
||||||
|
|
||||||
public static SignalRMessage ScanSeriesEvent(int seriesId, string seriesName)
|
public static SignalRMessage ScanSeriesEvent(int seriesId, string seriesName)
|
||||||
@ -225,6 +229,22 @@ namespace API.SignalR
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SignalRMessage LibraryModifiedEvent(int libraryId, string action)
|
||||||
|
{
|
||||||
|
return new SignalRMessage
|
||||||
|
{
|
||||||
|
Name = LibraryModified,
|
||||||
|
Title = "Library modified",
|
||||||
|
Progress = ProgressType.None,
|
||||||
|
EventType = ProgressEventType.Single,
|
||||||
|
Body = new
|
||||||
|
{
|
||||||
|
LibrayId = libraryId,
|
||||||
|
Action = action,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static SignalRMessage DownloadProgressEvent(string username, string downloadName, float progress, string eventType = "updated")
|
public static SignalRMessage DownloadProgressEvent(string username, string downloadName, float progress, string eventType = "updated")
|
||||||
{
|
{
|
||||||
return new SignalRMessage()
|
return new SignalRMessage()
|
||||||
@ -270,27 +290,8 @@ namespace API.SignalR
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalRMessage DbUpdateProgressEvent(Series series, string eventType)
|
|
||||||
{
|
|
||||||
// TODO: I want this as a detail of a Scanning Series and we can put more information like Volume or Chapter here
|
|
||||||
return new SignalRMessage()
|
|
||||||
{
|
|
||||||
Name = ScanProgress,
|
|
||||||
Title = $"Scanning {series.Library.Name}",
|
|
||||||
SubTitle = series.Name,
|
|
||||||
EventType = eventType,
|
|
||||||
Progress = ProgressType.Indeterminate,
|
|
||||||
Body = new
|
|
||||||
{
|
|
||||||
Title = "Updating Series",
|
|
||||||
SubTitle = series.Name
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SignalRMessage LibraryScanProgressEvent(string libraryName, string eventType, string seriesName = "")
|
public static SignalRMessage LibraryScanProgressEvent(string libraryName, string eventType, string seriesName = "")
|
||||||
{
|
{
|
||||||
// TODO: I want this as a detail of a Scanning Series and we can put more information like Volume or Chapter here
|
|
||||||
return new SignalRMessage()
|
return new SignalRMessage()
|
||||||
{
|
{
|
||||||
Name = ScanProgress,
|
Name = ScanProgress,
|
||||||
|
4
UI/Web/src/app/_models/events/library-modified-event.ts
Normal file
4
UI/Web/src/app/_models/events/library-modified-event.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface LibraryModifiedEvent {
|
||||||
|
libraryId: number;
|
||||||
|
action: 'create' | 'delelte';
|
||||||
|
}
|
10
UI/Web/src/app/_models/invite-user-response.ts
Normal file
10
UI/Web/src/app/_models/invite-user-response.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export interface InviteUserResponse {
|
||||||
|
/**
|
||||||
|
* Link to register new user
|
||||||
|
*/
|
||||||
|
emailLink: string;
|
||||||
|
/**
|
||||||
|
* If an email was sent to the invited user
|
||||||
|
*/
|
||||||
|
emailSent: boolean;
|
||||||
|
}
|
@ -29,6 +29,7 @@ export interface SeriesFilter {
|
|||||||
tags: Array<number>;
|
tags: Array<number>;
|
||||||
languages: Array<string>;
|
languages: Array<string>;
|
||||||
publicationStatus: Array<number>;
|
publicationStatus: Array<number>;
|
||||||
|
seriesNameQuery: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SortOptions {
|
export interface SortOptions {
|
||||||
|
@ -8,6 +8,7 @@ import { User } from '../_models/user';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { MessageHubService } from './message-hub.service';
|
import { MessageHubService } from './message-hub.service';
|
||||||
import { ThemeService } from '../theme.service';
|
import { ThemeService } from '../theme.service';
|
||||||
|
import { InviteUserResponse } from '../_models/invite-user-response';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -130,8 +131,8 @@ export class AccountService implements OnDestroy {
|
|||||||
return this.httpClient.post<string>(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}, {responseType: 'text' as 'json'});
|
return this.httpClient.post<string>(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}, {responseType: 'text' as 'json'});
|
||||||
}
|
}
|
||||||
|
|
||||||
inviteUser(model: {email: string, roles: Array<string>, libraries: Array<number>, sendEmail: boolean}) {
|
inviteUser(model: {email: string, roles: Array<string>, libraries: Array<number>}) {
|
||||||
return this.httpClient.post<string>(this.baseUrl + 'account/invite', model, {responseType: 'text' as 'json'});
|
return this.httpClient.post<InviteUserResponse>(this.baseUrl + 'account/invite', model);
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmEmail(model: {email: string, username: string, password: string, token: string}) {
|
confirmEmail(model: {email: string, username: string, password: string, token: string}) {
|
||||||
|
@ -4,6 +4,7 @@ import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
|
|||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { BehaviorSubject, ReplaySubject } from 'rxjs';
|
import { BehaviorSubject, ReplaySubject } from 'rxjs';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { LibraryModifiedEvent } from '../_models/events/library-modified-event';
|
||||||
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
|
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
|
||||||
import { SiteThemeProgressEvent } from '../_models/events/site-theme-progress-event';
|
import { SiteThemeProgressEvent } from '../_models/events/site-theme-progress-event';
|
||||||
import { User } from '../_models/user';
|
import { User } from '../_models/user';
|
||||||
@ -49,6 +50,10 @@ export enum EVENTS {
|
|||||||
* A subtype of NotificationProgress that represents a file being processed for cover image extraction
|
* A subtype of NotificationProgress that represents a file being processed for cover image extraction
|
||||||
*/
|
*/
|
||||||
CoverUpdateProgress = 'CoverUpdateProgress',
|
CoverUpdateProgress = 'CoverUpdateProgress',
|
||||||
|
/**
|
||||||
|
* A library is created or removed from the instance
|
||||||
|
*/
|
||||||
|
LibraryModified = 'LibraryModified'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Message<T> {
|
export interface Message<T> {
|
||||||
@ -130,6 +135,13 @@ export class MessageHubService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.hubConnection.on(EVENTS.LibraryModified, resp => {
|
||||||
|
this.messagesSource.next({
|
||||||
|
event: EVENTS.LibraryModified,
|
||||||
|
payload: resp.body as LibraryModifiedEvent
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
this.hubConnection.on(EVENTS.NotificationProgress, (resp: NotificationProgressEvent) => {
|
this.hubConnection.on(EVENTS.NotificationProgress, (resp: NotificationProgressEvent) => {
|
||||||
this.messagesSource.next({
|
this.messagesSource.next({
|
||||||
|
@ -226,6 +226,7 @@ export class SeriesService {
|
|||||||
tags: [],
|
tags: [],
|
||||||
languages: [],
|
languages: [],
|
||||||
publicationStatus: [],
|
publicationStatus: [],
|
||||||
|
seriesNameQuery: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (filter === undefined) return data;
|
if (filter === undefined) return data;
|
||||||
|
@ -13,12 +13,12 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|||||||
import { ResetPasswordModalComponent } from './_modals/reset-password-modal/reset-password-modal.component';
|
import { ResetPasswordModalComponent } from './_modals/reset-password-modal/reset-password-modal.component';
|
||||||
import { ManageSettingsComponent } from './manage-settings/manage-settings.component';
|
import { ManageSettingsComponent } from './manage-settings/manage-settings.component';
|
||||||
import { ManageSystemComponent } from './manage-system/manage-system.component';
|
import { ManageSystemComponent } from './manage-system/manage-system.component';
|
||||||
import { ChangelogComponent } from '../announcements/changelog/changelog.component';
|
|
||||||
import { PipeModule } from '../pipe/pipe.module';
|
import { PipeModule } from '../pipe/pipe.module';
|
||||||
import { InviteUserComponent } from './invite-user/invite-user.component';
|
import { InviteUserComponent } from './invite-user/invite-user.component';
|
||||||
import { RoleSelectorComponent } from './role-selector/role-selector.component';
|
import { RoleSelectorComponent } from './role-selector/role-selector.component';
|
||||||
import { LibrarySelectorComponent } from './library-selector/library-selector.component';
|
import { LibrarySelectorComponent } from './library-selector/library-selector.component';
|
||||||
import { EditUserComponent } from './edit-user/edit-user.component';
|
import { EditUserComponent } from './edit-user/edit-user.component';
|
||||||
|
import { UserSettingsModule } from '../user-settings/user-settings.module';
|
||||||
import { SidenavModule } from '../sidenav/sidenav.module';
|
import { SidenavModule } from '../sidenav/sidenav.module';
|
||||||
|
|
||||||
|
|
||||||
@ -50,7 +50,8 @@ import { SidenavModule } from '../sidenav/sidenav.module';
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
PipeModule,
|
PipeModule,
|
||||||
SidenavModule
|
SidenavModule,
|
||||||
|
UserSettingsModule // API-key componet
|
||||||
],
|
],
|
||||||
providers: []
|
providers: []
|
||||||
})
|
})
|
||||||
|
@ -9,13 +9,7 @@
|
|||||||
Invite a user to your server. Enter their email in and we will send them an email to create an account.
|
Invite a user to your server. Enter their email in and we will send them an email to create an account.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p *ngIf="!checkedAccessibility">
|
<form [formGroup]="inviteForm" *ngIf="emailLink === ''">
|
||||||
<span class="spinner-border text-primary" style="width: 1.5rem; height: 1.5rem;" role="status" aria-hidden="true"></span>
|
|
||||||
Checking accessibility of server...
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
<form [formGroup]="inviteForm">
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="mb-3" style="width:100%">
|
<div class="mb-3" style="width:100%">
|
||||||
<label for="email" class="form-label">Email</label>
|
<label for="email" class="form-label">Email</label>
|
||||||
@ -28,11 +22,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="emailLink !== '' && checkedAccessibility && !accessible">
|
|
||||||
<p>Use this link to finish setting up the user account due to your server not being accessible outside your local network.</p>
|
|
||||||
<a class="email-link" href="{{emailLink}}" target="_blank">{{emailLink}}</a>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true"></app-role-selector>
|
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true"></app-role-selector>
|
||||||
@ -44,12 +33,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<ng-container *ngIf="emailLink !== ''">
|
||||||
|
<h4>User invited</h4>
|
||||||
|
<p>You can use the following link below to setup the account for your user or use the copy button. You may need to log out before using the link to register a new user.
|
||||||
|
If your server is externallyaccessible, an email will have been sent to the user and the links can be used by them to finish setting up their account.
|
||||||
|
</p>
|
||||||
|
<a class="email-link" href="{{emailLink}}" target="_blank">Setup user's account</a>
|
||||||
|
<app-api-key title="Invite Url" tooltipText="Copy this and paste in a new tab. You may need to log out." [showRefresh]="false" [transform]="makeLink"></app-api-key>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" (click)="close()">
|
<button type="button" class="btn btn-secondary" (click)="close()">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-primary" (click)="invite()" [disabled]="isSending || !inviteForm.valid || !checkedAccessibility || emailLink !== ''">
|
<button type="button" class="btn btn-primary" (click)="invite()" [disabled]="isSending || !inviteForm.valid || emailLink !== ''">
|
||||||
<span *ngIf="isSending" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
<span *ngIf="isSending" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
<span>{{isSending ? 'Inviting...' : 'Invite'}}</span>
|
<span>{{isSending ? 'Inviting...' : 'Invite'}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -3,6 +3,7 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
|
|||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||||
|
import { InviteUserResponse } from 'src/app/_models/invite-user-response';
|
||||||
import { Library } from 'src/app/_models/library';
|
import { Library } from 'src/app/_models/library';
|
||||||
import { AccountService } from 'src/app/_services/account.service';
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
import { ServerService } from 'src/app/_services/server.service';
|
import { ServerService } from 'src/app/_services/server.service';
|
||||||
@ -19,15 +20,12 @@ export class InviteUserComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
isSending: boolean = false;
|
isSending: boolean = false;
|
||||||
inviteForm: FormGroup = new FormGroup({});
|
inviteForm: FormGroup = new FormGroup({});
|
||||||
/**
|
|
||||||
* If a user would be able to load this server up externally
|
|
||||||
*/
|
|
||||||
accessible: boolean = true;
|
|
||||||
checkedAccessibility: boolean = false;
|
|
||||||
selectedRoles: Array<string> = [];
|
selectedRoles: Array<string> = [];
|
||||||
selectedLibraries: Array<number> = [];
|
selectedLibraries: Array<number> = [];
|
||||||
emailLink: string = '';
|
emailLink: string = '';
|
||||||
|
|
||||||
|
makeLink: (val: string) => string = (val: string) => {return this.emailLink};
|
||||||
|
|
||||||
public get email() { return this.inviteForm.get('email'); }
|
public get email() { return this.inviteForm.get('email'); }
|
||||||
|
|
||||||
constructor(public modal: NgbActiveModal, private accountService: AccountService, private serverService: ServerService,
|
constructor(public modal: NgbActiveModal, private accountService: AccountService, private serverService: ServerService,
|
||||||
@ -35,14 +33,6 @@ export class InviteUserComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.inviteForm.addControl('email', new FormControl('', [Validators.required]));
|
this.inviteForm.addControl('email', new FormControl('', [Validators.required]));
|
||||||
|
|
||||||
this.serverService.isServerAccessible().subscribe(async (accessibile) => {
|
|
||||||
if (!accessibile) {
|
|
||||||
await this.confirmService.alert('This server is not accessible outside the network. You cannot invite via Email. You wil be given a link to finish registration with instead.');
|
|
||||||
this.accessible = accessibile;
|
|
||||||
}
|
|
||||||
this.checkedAccessibility = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
@ -57,11 +47,10 @@ export class InviteUserComponent implements OnInit {
|
|||||||
email,
|
email,
|
||||||
libraries: this.selectedLibraries,
|
libraries: this.selectedLibraries,
|
||||||
roles: this.selectedRoles,
|
roles: this.selectedRoles,
|
||||||
sendEmail: this.accessible
|
}).subscribe((data: InviteUserResponse) => {
|
||||||
}).subscribe(emailLink => {
|
this.emailLink = data.emailLink;
|
||||||
this.emailLink = emailLink;
|
|
||||||
this.isSending = false;
|
this.isSending = false;
|
||||||
if (this.accessible) {
|
if (data.emailSent) {
|
||||||
this.toastr.info('Email sent to ' + email);
|
this.toastr.info('Email sent to ' + email);
|
||||||
this.modal.close(true);
|
this.modal.close(true);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<app-card-actionables [actions]="actions"></app-card-actionables>
|
<app-card-actionables [actions]="actions"></app-card-actionables>
|
||||||
All Series
|
All Series
|
||||||
</h2>
|
</h2>
|
||||||
<h6 subtitle style="margin-left:40px;">{{pagination?.totalItems}} Series</h6>
|
<h6 subtitle>{{pagination?.totalItems}} Series</h6>
|
||||||
</app-side-nav-companion-bar>
|
</app-side-nav-companion-bar>
|
||||||
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||||
<app-card-detail-layout
|
<app-card-detail-layout
|
||||||
|
@ -6,7 +6,6 @@ import { take, debounceTime, takeUntil } from 'rxjs/operators';
|
|||||||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||||
import { FilterSettings } from '../metadata-filter/filter-settings';
|
import { FilterSettings } from '../metadata-filter/filter-settings';
|
||||||
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
||||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
|
||||||
import { Library } from '../_models/library';
|
import { Library } from '../_models/library';
|
||||||
import { Pagination } from '../_models/pagination';
|
import { Pagination } from '../_models/pagination';
|
||||||
import { Series } from '../_models/series';
|
import { Series } from '../_models/series';
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
<h1>Announcements</h1>
|
<app-side-nav-companion-bar>
|
||||||
|
<h2 title>
|
||||||
|
Announcements
|
||||||
|
</h2>
|
||||||
|
</app-side-nav-companion-bar>
|
||||||
|
|
||||||
<app-changelog></app-changelog>
|
<app-changelog></app-changelog>
|
@ -5,6 +5,7 @@ import { ChangelogComponent } from './changelog/changelog.component';
|
|||||||
import { AnnouncementsRoutingModule } from './announcements-routing.module';
|
import { AnnouncementsRoutingModule } from './announcements-routing.module';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { PipeModule } from '../pipe/pipe.module';
|
import { PipeModule } from '../pipe/pipe.module';
|
||||||
|
import { SidenavModule } from '../sidenav/sidenav.module';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -17,7 +18,8 @@ import { PipeModule } from '../pipe/pipe.module';
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
AnnouncementsRoutingModule,
|
AnnouncementsRoutingModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
PipeModule
|
PipeModule,
|
||||||
|
SidenavModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AnnouncementsModule { }
|
export class AnnouncementsModule { }
|
||||||
|
@ -6,7 +6,6 @@ import { RecentlyAddedComponent } from './recently-added/recently-added.componen
|
|||||||
import { UserLoginComponent } from './user-login/user-login.component';
|
import { UserLoginComponent } from './user-login/user-login.component';
|
||||||
import { AuthGuard } from './_guards/auth.guard';
|
import { AuthGuard } from './_guards/auth.guard';
|
||||||
import { LibraryAccessGuard } from './_guards/library-access.guard';
|
import { LibraryAccessGuard } from './_guards/library-access.guard';
|
||||||
import { OnDeckComponent } from './on-deck/on-deck.component';
|
|
||||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
import { AllSeriesComponent } from './all-series/all-series.component';
|
import { AllSeriesComponent } from './all-series/all-series.component';
|
||||||
import { AdminGuard } from './_guards/admin.guard';
|
import { AdminGuard } from './_guards/admin.guard';
|
||||||
@ -70,7 +69,6 @@ const routes: Routes = [
|
|||||||
children: [
|
children: [
|
||||||
{path: 'library', component: DashboardComponent},
|
{path: 'library', component: DashboardComponent},
|
||||||
{path: 'recently-added', component: RecentlyAddedComponent},
|
{path: 'recently-added', component: RecentlyAddedComponent},
|
||||||
{path: 'on-deck', component: OnDeckComponent},
|
|
||||||
{path: 'all-series', component: AllSeriesComponent},
|
{path: 'all-series', component: AllSeriesComponent},
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -23,7 +23,6 @@ import { CarouselModule } from './carousel/carousel.module';
|
|||||||
|
|
||||||
import { TypeaheadModule } from './typeahead/typeahead.module';
|
import { TypeaheadModule } from './typeahead/typeahead.module';
|
||||||
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
|
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
|
||||||
import { OnDeckComponent } from './on-deck/on-deck.component';
|
|
||||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
import { CardsModule } from './cards/cards.module';
|
import { CardsModule } from './cards/cards.module';
|
||||||
import { CollectionsModule } from './collections/collections.module';
|
import { CollectionsModule } from './collections/collections.module';
|
||||||
@ -50,7 +49,6 @@ import { SidenavModule } from './sidenav/sidenav.module';
|
|||||||
SeriesDetailComponent,
|
SeriesDetailComponent,
|
||||||
ReviewSeriesModalComponent,
|
ReviewSeriesModalComponent,
|
||||||
RecentlyAddedComponent,
|
RecentlyAddedComponent,
|
||||||
OnDeckComponent,
|
|
||||||
DashboardComponent,
|
DashboardComponent,
|
||||||
EventsWidgetComponent,
|
EventsWidgetComponent,
|
||||||
SeriesMetadataDetailComponent,
|
SeriesMetadataDetailComponent,
|
||||||
@ -104,7 +102,6 @@ import { SidenavModule } from './sidenav/sidenav.module';
|
|||||||
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
|
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
|
||||||
Title,
|
Title,
|
||||||
{provide: SAVER, useFactory: getSaver},
|
{provide: SAVER, useFactory: getSaver},
|
||||||
// { provide: APP_BASE_HREF, useFactory: (config: ConfigData) => config.baseUrl, deps: [ConfigData] },
|
|
||||||
],
|
],
|
||||||
entryComponents: [],
|
entryComponents: [],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
<span>
|
<span>
|
||||||
<span *ngIf="chapterMetadata && chapterMetadata.releaseDate !== null">Release Date: {{chapterMetadata.releaseDate | date: 'shortDate' || '-'}}</span>
|
<span *ngIf="chapterMetadata && chapterMetadata.releaseDate !== null">Release Date: {{chapterMetadata.releaseDate | date: 'shortDate' || '-'}}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-accent">{{chapter.pages}} pages</span>
|
<span class="text-accent">{{data.pages}} pages</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@ -106,24 +106,26 @@
|
|||||||
<h4 *ngIf="!utilityService.isChapter(data)">{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
|
<h4 *ngIf="!utilityService.isChapter(data)">{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
||||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{libraryType !== LibraryType.Comic ? 'Chapter ' : 'Issue #'}} {{chapter.number}}">
|
<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]="chapter.coverImage"></app-image>
|
||||||
</a>
|
</a>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h5 class="mt-0 mb-1">
|
<h5 class="mt-0 mb-1">
|
||||||
<span *ngIf="chapter.number !== '0'; else specialHeader">
|
<span >
|
||||||
<span>
|
<span>
|
||||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||||
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
<ng-container *ngIf="chapter.number !== '0'; else specialHeader">
|
||||||
|
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||||
|
</ng-container>
|
||||||
</span>
|
</span>
|
||||||
<span class="badge bg-primary rounded-pill">
|
<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 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
||||||
<span *ngIf="chapter.pagesRead === 0">UNREAD</span>
|
<span *ngIf="chapter.pagesRead === 0">UNREAD</span>
|
||||||
<span *ngIf="chapter.pagesRead === chapter.pages">READ</span>
|
<span *ngIf="chapter.pagesRead === chapter.pages">READ</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<ng-template #specialHeader>File(s)</ng-template>
|
<ng-template #specialHeader>Files</ng-template>
|
||||||
</h5>
|
</h5>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<li *ngFor="let file of chapter.files" class="list-group-item no-hover">
|
<li *ngFor="let file of chapter.files" class="list-group-item no-hover">
|
||||||
|
@ -98,8 +98,6 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.imageUrls.push(this.imageService.getSeriesCoverImage(this.series.id));
|
this.imageUrls.push(this.imageService.getSeriesCoverImage(this.series.id));
|
||||||
|
|
||||||
this.initSeries = Object.assign({}, this.series);
|
|
||||||
|
|
||||||
this.libraryService.getLibraryNames().pipe(takeUntil(this.onDestroy)).subscribe(names => {
|
this.libraryService.getLibraryNames().pipe(takeUntil(this.onDestroy)).subscribe(names => {
|
||||||
this.libraryName = names[this.series.libraryId];
|
this.libraryName = names[this.series.libraryId];
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="carousel-container" *ngIf="items.length > 0">
|
<div class="carousel-container" *ngIf="items.length > 0">
|
||||||
<div>
|
<div>
|
||||||
<h2 style="display: inline-block;"><a href="javascript:void(0)" (click)="sectionClicked($event)" class="section-title">{{title}}</a></h2>
|
<h3 style="display: inline-block;"><a href="javascript:void(0)" (click)="sectionClicked($event)" class="section-title" [ngClass]="{'non-selectable': !clickableTitle}">{{title}}</a></h3>
|
||||||
<div class="float-end">
|
<div class="float-end">
|
||||||
<button class="btn btn-icon" [disabled]="isBeginning" (click)="prevPage()"><i class="fa fa-angle-left" aria-hidden="true"></i><span class="visually-hidden">Previous Items</span></button>
|
<button class="btn btn-icon" [disabled]="isBeginning" (click)="prevPage()"><i class="fa fa-angle-left" aria-hidden="true"></i><span class="visually-hidden">Previous Items</span></button>
|
||||||
<button class="btn btn-icon" [disabled]="isEnd" (click)="nextPage()"><i class="fa fa-angle-right" aria-hidden="true"></i><span class="visually-hidden">Next Items</span></button>
|
<button class="btn btn-icon" [disabled]="isEnd" (click)="nextPage()"><i class="fa fa-angle-right" aria-hidden="true"></i><span class="visually-hidden">Next Items</span></button>
|
||||||
|
@ -17,6 +17,10 @@
|
|||||||
text-decoration: var(--carousel-hover-header-text-decoration);
|
text-decoration: var(--carousel-hover-header-text-decoration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.non-selectable {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ export class CarouselReelComponent implements OnInit {
|
|||||||
@ContentChild('carouselItem') carouselItemTemplate!: TemplateRef<any>;
|
@ContentChild('carouselItem') carouselItemTemplate!: TemplateRef<any>;
|
||||||
@Input() items: any[] = [];
|
@Input() items: any[] = [];
|
||||||
@Input() title = '';
|
@Input() title = '';
|
||||||
|
@Input() clickableTitle: boolean = true;
|
||||||
@Output() sectionClick = new EventEmitter<string>();
|
@Output() sectionClick = new EventEmitter<string>();
|
||||||
|
|
||||||
swiper: Swiper | undefined;
|
swiper: Swiper | undefined;
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
<div *ngIf="libraries.length === 0 && !isLoading && isAdmin" class="d-flex justify-content-center">
|
<ng-container *ngIf="libraries.length === 0 && !isLoading">
|
||||||
<p>There are no libraries setup yet. Configure some in <a href="/admin/dashboard#libraries">Server settings</a>.</p>
|
<div class="mt-3">
|
||||||
</div>
|
<div *ngIf="isAdmin" class="d-flex justify-content-center">
|
||||||
<div *ngIf="libraries.length === 0 && !isLoading && !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>
|
||||||
<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>
|
||||||
|
|
||||||
<app-carousel-reel [items]="inProgress" title="On Deck" (sectionClick)="handleSectionClick($event)">
|
<app-carousel-reel [items]="inProgress" title="On Deck" (sectionClick)="handleSectionClick($event)">
|
||||||
<ng-template #carouselItem let-item let-position="idx">
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
@ -12,14 +16,14 @@
|
|||||||
</app-carousel-reel>
|
</app-carousel-reel>
|
||||||
|
|
||||||
<!-- TODO: Refactor this so we can use series actions here -->
|
<!-- TODO: Refactor this so we can use series actions here -->
|
||||||
<app-carousel-reel [items]="recentlyUpdatedSeries" title="Recently Updated Series" (sectionClick)="handleSectionClick($event)">
|
<app-carousel-reel [items]="recentlyUpdatedSeries" title="Recently Updated Series" (sectionClick)="handleSectionClick($event)">
|
||||||
<ng-template #carouselItem let-item let-position="idx">
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
<app-card-item [entity]="item" [title]="item.seriesName" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
|
<app-card-item [entity]="item" [title]="item.seriesName" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
|
||||||
[supressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"></app-card-item>
|
[supressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"></app-card-item>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-carousel-reel>
|
</app-carousel-reel>
|
||||||
|
|
||||||
<app-carousel-reel [items]="recentlyAddedSeries" title="Newly Added Series">
|
<app-carousel-reel [items]="recentlyAddedSeries" title="Newly Added Series" [clickableTitle]="false">
|
||||||
<ng-template #carouselItem let-item let-position="idx">
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (dataChanged)="loadRecentlyAddedSeries()"></app-series-card>
|
<app-series-card [data]="item" [libraryId]="item.libraryId" (dataChanged)="loadRecentlyAddedSeries()"></app-series-card>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -141,7 +141,10 @@ export class LibraryComponent implements OnInit, OnDestroy {
|
|||||||
} else if (sectionTitle.toLowerCase() === 'recently updated series') {
|
} else if (sectionTitle.toLowerCase() === 'recently updated series') {
|
||||||
this.router.navigate(['recently-added']);
|
this.router.navigate(['recently-added']);
|
||||||
} else if (sectionTitle.toLowerCase() === 'on deck') {
|
} else if (sectionTitle.toLowerCase() === 'on deck') {
|
||||||
this.router.navigate(['on-deck']);
|
const params: any = {};
|
||||||
|
params['readStatus'] = 'true,false,false';
|
||||||
|
params['page'] = 1;
|
||||||
|
this.router.navigate(['all-series'], {queryParams: params});
|
||||||
} else if (sectionTitle.toLowerCase() === 'libraries') {
|
} else if (sectionTitle.toLowerCase() === 'libraries') {
|
||||||
this.router.navigate(['all-series']);
|
this.router.navigate(['all-series']);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { LibraryType } from "src/app/_models/library";
|
||||||
import { MangaFormat } from "src/app/_models/manga-format";
|
import { MangaFormat } from "src/app/_models/manga-format";
|
||||||
|
|
||||||
export interface ChapterInfo {
|
export interface ChapterInfo {
|
||||||
@ -8,6 +9,7 @@ export interface ChapterInfo {
|
|||||||
seriesFormat: MangaFormat;
|
seriesFormat: MangaFormat;
|
||||||
seriesId: number;
|
seriesId: number;
|
||||||
libraryId: number;
|
libraryId: number;
|
||||||
|
libraryType: LibraryType;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
isSpecial: boolean;
|
isSpecial: boolean;
|
||||||
volumeId: number;
|
volumeId: number;
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
'fit-to-height-double-offset': this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.HEIGHT && ShouldRenderDoublePage,
|
'fit-to-height-double-offset': this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.HEIGHT && ShouldRenderDoublePage,
|
||||||
'original-double-offset' : this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.ORIGINAL && ShouldRenderDoublePage,
|
'original-double-offset' : this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.ORIGINAL && ShouldRenderDoublePage,
|
||||||
'reverse': ShouldRenderReverseDouble}">
|
'reverse': ShouldRenderReverseDouble}">
|
||||||
<img [src]="readerService.getPageUrl(this.chapterId, this.pageNum)" id="image-1"
|
<img [src]="canvasImage.src" id="image-1"
|
||||||
class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}">
|
class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}">
|
||||||
|
|
||||||
<ng-container *ngIf="ShouldRenderDoublePage && (this.pageNum + 1 <= maxPages - 1 && this.pageNum > 0)">
|
<ng-container *ngIf="ShouldRenderDoublePage && (this.pageNum + 1 <= maxPages - 1 && this.pageNum > 0)">
|
||||||
@ -132,27 +132,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bottom-menu" *ngIf="settingsOpen && generalSettingsForm">
|
<div class="bottom-menu" *ngIf="settingsOpen && generalSettingsForm">
|
||||||
<form [formGroup]="generalSettingsForm">
|
<form [formGroup]="generalSettingsForm">
|
||||||
<div class="row">
|
<div class="row mb-2">
|
||||||
<div class="col-6">
|
<div class="col-md-6 col-sm-12">
|
||||||
<label for="page-splitting" class="form-label">Image Splitting</label>
|
<label for="page-splitting" class="form-label">Image Splitting</label>
|
||||||
<div class="split fa fa-image">
|
<div class="split fa fa-image">
|
||||||
<div class="{{splitIconClass}}"></div>
|
<div class="{{splitIconClass}}"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<select class="form-control" id="page-splitting" formControlName="pageSplitOption">
|
||||||
|
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text}}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<select class="form-control" id="page-splitting" formControlName="pageSplitOption">
|
|
||||||
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="col-md-6 col-sm-12">
|
||||||
<div class="col-6">
|
|
||||||
<label for="page-fitting" class="form-label">Image Scaling</label> <i class="fa {{getFittingIcon()}}" aria-hidden="true"></i>
|
<label for="page-fitting" class="form-label">Image Scaling</label> <i class="fa {{getFittingIcon()}}" aria-hidden="true"></i>
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<select class="form-control" id="page-fitting" formControlName="fittingOption">
|
<select class="form-control" id="page-fitting" formControlName="fittingOption">
|
||||||
<option value="full-height">Height</option>
|
<option value="full-height">Height</option>
|
||||||
<option value="full-width">Width</option>
|
<option value="full-width">Width</option>
|
||||||
@ -161,11 +153,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-2 mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-6">
|
<div class="col-md-6 col-sm-12">
|
||||||
<label for="autoCloseMenu" class="form-check-label">Auto Close Menu</label>
|
<label for="layout-mode" class="form-label">Layout Mode</label>
|
||||||
|
<select class="form-control" id="page-fitting" formControlName="layoutMode">
|
||||||
|
<option [value]="opt.value" *ngFor="let opt of layoutModes">{{opt.text}}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-md-6 col-sm-12">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label id="auto-close-label" class="form-label"></label>
|
<label id="auto-close-label" class="form-label"></label>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -177,17 +172,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-2 mb-2">
|
|
||||||
<div class="col-6">
|
|
||||||
<label for="layout-mode" class="form-label">Layout Mode</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<select class="form-control" id="page-fitting" formControlName="layoutMode">
|
|
||||||
<option [value]="opt.value" *ngFor="let opt of layoutModes">{{opt.text}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,9 +47,9 @@ img {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
// canvas {
|
||||||
position: absolute;
|
// //position: absolute; // JOE: Not sure why we have this, but it breaks the renderer
|
||||||
}
|
// }
|
||||||
|
|
||||||
.reader {
|
.reader {
|
||||||
background-color: var(--manga-reader-bg-color);
|
background-color: var(--manga-reader-bg-color);
|
||||||
|
@ -122,7 +122,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
* Used soley for LayoutMode.Double rendering. Will always hold the next image in buffer.
|
* Used soley for LayoutMode.Double rendering. Will always hold the next image in buffer.
|
||||||
*/
|
*/
|
||||||
canvasImage2 = new Image();
|
canvasImage2 = new Image();
|
||||||
renderWithCanvas: boolean = false; // Dictates if we use render with canvas or with image
|
/**
|
||||||
|
* Dictates if we use render with canvas or with image. This is only for Splitting.
|
||||||
|
*/
|
||||||
|
renderWithCanvas: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A circular array of size PREFETCH_PAGES + 2. Maintains prefetched Images around the current page to load from to avoid loading animation.
|
* A circular array of size PREFETCH_PAGES + 2. Maintains prefetched Images around the current page to load from to avoid loading animation.
|
||||||
@ -534,11 +537,8 @@ 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.
|
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.pageOptions = newOptions;
|
||||||
|
|
||||||
// TODO: Move this into ChapterInfo
|
this.libraryType = results.chapterInfo.libraryType;
|
||||||
this.libraryService.getLibraryType(results.chapterInfo.libraryId).pipe(take(1)).subscribe(type => {
|
this.updateTitle(results.chapterInfo, this.libraryType);
|
||||||
this.libraryType = type;
|
|
||||||
this.updateTitle(results.chapterInfo, type);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.inSetup = false;
|
this.inSetup = false;
|
||||||
|
|
||||||
@ -660,14 +660,19 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
return FITTING_OPTION.WIDTH;
|
return FITTING_OPTION.WIDTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (this.isCoverImage() && this.layoutMode !== LayoutMode.Single) {
|
if (this.isCoverImage() && this.layoutMode !== LayoutMode.Single) {
|
||||||
return val + ' cover double';
|
return val + ' cover double';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isCoverImage() && this.layoutMode !== LayoutMode.Single) {
|
// Code from feature/manga-reader. Validate which fix is better
|
||||||
return val + ' double';
|
// if (this.layoutMode !== LayoutMode.Single) {
|
||||||
}
|
// val = val + (this.isCoverImage() ? 'cover' : '') + 'double';
|
||||||
|
// } else if (this.isCoverImage() && this.shouldRenderAsFitSplit()) {
|
||||||
|
// // JOE: If we are Fit to Screen, we should use fitting as width just for cover images
|
||||||
|
// // Rewriting to fit to width for this cover image
|
||||||
|
// val = FITTING_OPTION.WIDTH;
|
||||||
|
// }
|
||||||
|
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,14 +5,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="not-phone-hidden">
|
<div class="not-phone-hidden">
|
||||||
<app-drawer #commentDrawer="drawer" [isOpen]="!filteringCollapsed" [style.--drawer-width]="'300px'" [style.--drawer-background-color]="'#010409'" (drawerClosed)="filteringCollapsed = !filteringCollapsed">
|
<app-drawer #commentDrawer="drawer" [isOpen]="!filteringCollapsed" [style.--drawer-width]="'300px'" [style.--drawer-background-color]="'#010409'" [options]="{topOffset: 56}" (drawerClosed)="filteringCollapsed = !filteringCollapsed">
|
||||||
<div header>
|
<div header>
|
||||||
<h2 style="margin-top: 0.5rem">Book Settings
|
<h2 style="margin-top: 0.5rem">Book Settings
|
||||||
<button type="button" class="btn-close" aria-label="Close" (click)="commentDrawer.close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="commentDrawer.close()"></button>
|
||||||
|
|
||||||
</button>
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div body class="drawer-body">
|
<div body class="drawer-body">
|
||||||
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
||||||
@ -306,6 +303,16 @@
|
|||||||
<div class="col-md-2 me-3"></div>
|
<div class="col-md-2 me-3"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-content-center g-0">
|
<div class="row justify-content-center g-0">
|
||||||
|
<div class="col-md-2 me-3">
|
||||||
|
<form [formGroup]="seriesNameGroup">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="series-name" class="form-label">Series Name</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="seriesNameFilterTooltip" role="button" tabindex="0"></i>
|
||||||
|
<span class="visually-hidden" id="filter-series-name-help"><ng-container [ngTemplateOutlet]="seriesNameFilterTooltip"></ng-container></span>
|
||||||
|
<ng-template #seriesNameFilterTooltip>Series name will filter against Name, Sort Name, or Localized Name</ng-template>
|
||||||
|
<input type="text" id="series-name" formControlName="seriesNameQuery" class="form-control" aria-describedby="filter-series-name-help">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<div class="col-md-2 me-3" *ngIf="!filterSettings.sortDisabled">
|
<div class="col-md-2 me-3" *ngIf="!filterSettings.sortDisabled">
|
||||||
<form [formGroup]="sortGroup">
|
<form [formGroup]="sortGroup">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -326,11 +333,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 me-3" *ngIf="filterSettings.sortDisabled"></div>
|
<div class="col-md-2 me-3" *ngIf="filterSettings.sortDisabled"></div>
|
||||||
<div class="col-md-2 me-3"></div>
|
<div class="col-md-2 me-3"></div>
|
||||||
<div class="col-md-2 me-3"></div>
|
<div class="col-md-2 me-3 mt-4">
|
||||||
<div class="col-md-2 me-3">
|
|
||||||
<button class="btn btn-secondary col-12" (click)="clear()">Clear</button>
|
<button class="btn btn-secondary col-12" (click)="clear()">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 me-3">
|
<div class="col-md-2 me-3 mt-4">
|
||||||
<button class="btn btn-primary col-12" (click)="apply()">Apply</button>
|
<button class="btn btn-primary col-12" (click)="apply()">Apply</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
import { Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { forkJoin, map, Observable, of, ReplaySubject, Subject, takeUntil } from 'rxjs';
|
import { distinctUntilChanged, forkJoin, map, Observable, of, ReplaySubject, Subject, takeUntil } from 'rxjs';
|
||||||
import { UtilityService } from '../shared/_services/utility.service';
|
import { Breakpoint, UtilityService } from '../shared/_services/utility.service';
|
||||||
import { TypeaheadSettings } from '../typeahead/typeahead-settings';
|
import { TypeaheadSettings } from '../typeahead/typeahead-settings';
|
||||||
import { CollectionTag } from '../_models/collection-tag';
|
import { CollectionTag } from '../_models/collection-tag';
|
||||||
import { Genre } from '../_models/genre';
|
import { Genre } from '../_models/genre';
|
||||||
@ -66,6 +66,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
readProgressGroup!: FormGroup;
|
readProgressGroup!: FormGroup;
|
||||||
sortGroup!: FormGroup;
|
sortGroup!: FormGroup;
|
||||||
|
seriesNameGroup!: FormGroup;
|
||||||
isAscendingSort: boolean = true;
|
isAscendingSort: boolean = true;
|
||||||
|
|
||||||
updateApplied: number = 0;
|
updateApplied: number = 0;
|
||||||
@ -83,7 +84,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService,
|
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService,
|
||||||
private utilityService: UtilityService, private collectionTagService: CollectionTagService) {
|
private utilityService: UtilityService, private collectionTagService: CollectionTagService) {
|
||||||
this.filter = this.seriesService.createSeriesFilter();
|
|
||||||
|
this.filter = this.seriesService.createSeriesFilter();
|
||||||
this.readProgressGroup = new FormGroup({
|
this.readProgressGroup = new FormGroup({
|
||||||
read: new FormControl(this.filter.readStatus.read, []),
|
read: new FormControl(this.filter.readStatus.read, []),
|
||||||
notRead: new FormControl(this.filter.readStatus.notRead, []),
|
notRead: new FormControl(this.filter.readStatus.notRead, []),
|
||||||
@ -94,6 +96,10 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
sortField: new FormControl(this.filter.sortOptions?.sortField || SortField.SortName, []),
|
sortField: new FormControl(this.filter.sortOptions?.sortField || SortField.SortName, []),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.seriesNameGroup = new FormGroup({
|
||||||
|
seriesNameQuery: new FormControl(this.filter.seriesNameQuery || '', [])
|
||||||
|
});
|
||||||
|
|
||||||
this.readProgressGroup.valueChanges.pipe(takeUntil(this.onDestory)).subscribe(changes => {
|
this.readProgressGroup.valueChanges.pipe(takeUntil(this.onDestory)).subscribe(changes => {
|
||||||
this.filter.readStatus.read = this.readProgressGroup.get('read')?.value;
|
this.filter.readStatus.read = this.readProgressGroup.get('read')?.value;
|
||||||
this.filter.readStatus.inProgress = this.readProgressGroup.get('inProgress')?.value;
|
this.filter.readStatus.inProgress = this.readProgressGroup.get('inProgress')?.value;
|
||||||
@ -124,6 +130,13 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.filter.sortOptions.sortField = parseInt(this.sortGroup.get('sortField')?.value, 10);
|
this.filter.sortOptions.sortField = parseInt(this.sortGroup.get('sortField')?.value, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.seriesNameGroup.get('seriesNameQuery')?.valueChanges.pipe(
|
||||||
|
map(val => (val || '').trim()),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
takeUntil(this.onDestory)).subscribe(changes => {
|
||||||
|
this.filter.seriesNameQuery = changes;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -137,6 +150,12 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.filterSettings.presets) {
|
||||||
|
this.readProgressGroup.get('read')?.patchValue(this.filterSettings.presets?.readStatus.read);
|
||||||
|
this.readProgressGroup.get('notRead')?.patchValue(this.filterSettings.presets?.readStatus.notRead);
|
||||||
|
this.readProgressGroup.get('inProgress')?.patchValue(this.filterSettings.presets?.readStatus.inProgress);
|
||||||
|
}
|
||||||
|
|
||||||
this.setupTypeaheads();
|
this.setupTypeaheads();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,8 +462,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
return personSettings;
|
return personSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFormatFilters(formats: MangaFormat[]) {
|
updateFormatFilters(formats: FilterItem<MangaFormat>[]) {
|
||||||
this.filter.formats = formats.map(item => item) || [];
|
this.filter.formats = formats.map(item => item.value) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLibraryFilters(libraries: Library[]) {
|
updateLibraryFilters(libraries: Library[]) {
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)">
|
|
||||||
<h2 title>
|
|
||||||
On Deck
|
|
||||||
</h2>
|
|
||||||
</app-side-nav-companion-bar>
|
|
||||||
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
|
||||||
<app-card-detail-layout
|
|
||||||
[isLoading]="isLoading"
|
|
||||||
[items]="series"
|
|
||||||
[pagination]="pagination"
|
|
||||||
[filterSettings]="filterSettings"
|
|
||||||
[filterOpen]="filterOpen"
|
|
||||||
(pageChange)="onPageChange($event)"
|
|
||||||
(applyFilter)="updateFilter($event)"
|
|
||||||
>
|
|
||||||
<ng-template #cardItem let-item let-position="idx">
|
|
||||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()" (selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
|
||||||
</ng-template>
|
|
||||||
</app-card-detail-layout>
|
|
@ -1,133 +0,0 @@
|
|||||||
import { Component, EventEmitter, HostListener, OnInit } from '@angular/core';
|
|
||||||
import { Title } from '@angular/platform-browser';
|
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
|
||||||
import { FilterSettings } from '../metadata-filter/filter-settings';
|
|
||||||
import { KEY_CODES } from '../shared/_services/utility.service';
|
|
||||||
import { Pagination } from '../_models/pagination';
|
|
||||||
import { Series } from '../_models/series';
|
|
||||||
import { FilterEvent, SeriesFilter} from '../_models/series-filter';
|
|
||||||
import { Action } from '../_services/action-factory.service';
|
|
||||||
import { ActionService } from '../_services/action.service';
|
|
||||||
import { SeriesService } from '../_services/series.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-on-deck',
|
|
||||||
templateUrl: './on-deck.component.html',
|
|
||||||
styleUrls: ['./on-deck.component.scss']
|
|
||||||
})
|
|
||||||
export class OnDeckComponent implements OnInit {
|
|
||||||
|
|
||||||
isLoading: boolean = true;
|
|
||||||
series: Series[] = [];
|
|
||||||
pagination!: Pagination;
|
|
||||||
libraryId!: number;
|
|
||||||
filter: SeriesFilter | undefined = undefined;
|
|
||||||
filterSettings: FilterSettings = new FilterSettings();
|
|
||||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
|
||||||
|
|
||||||
constructor(private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private titleService: Title,
|
|
||||||
private actionService: ActionService, public bulkSelectionService: BulkSelectionService) {
|
|
||||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
|
||||||
this.titleService.setTitle('Kavita - On Deck');
|
|
||||||
if (this.pagination === undefined || this.pagination === null) {
|
|
||||||
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
|
||||||
}
|
|
||||||
this.filterSettings.readProgressDisabled = true;
|
|
||||||
this.filterSettings.sortDisabled = true;
|
|
||||||
this.loadPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
@HostListener('document:keydown.shift', ['$event'])
|
|
||||||
handleKeypress(event: KeyboardEvent) {
|
|
||||||
if (event.key === KEY_CODES.SHIFT) {
|
|
||||||
this.bulkSelectionService.isShiftDown = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@HostListener('document:keyup.shift', ['$event'])
|
|
||||||
handleKeyUp(event: KeyboardEvent) {
|
|
||||||
if (event.key === KEY_CODES.SHIFT) {
|
|
||||||
this.bulkSelectionService.isShiftDown = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {}
|
|
||||||
|
|
||||||
seriesClicked(series: Series) {
|
|
||||||
this.router.navigate(['library', this.libraryId, 'series', series.id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
onPageChange(pagination: Pagination) {
|
|
||||||
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?page=' + this.pagination.currentPage);
|
|
||||||
this.loadPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFilter(event: FilterEvent) {
|
|
||||||
this.filter = event.filter;
|
|
||||||
const page = this.getPage();
|
|
||||||
if (page === undefined || page === null || !event.isFirst) {
|
|
||||||
this.pagination.currentPage = 1;
|
|
||||||
this.onPageChange(this.pagination);
|
|
||||||
} else {
|
|
||||||
this.loadPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPage() {
|
|
||||||
const page = this.getPage();
|
|
||||||
if (page != null) {
|
|
||||||
this.pagination.currentPage = parseInt(page, 10);
|
|
||||||
}
|
|
||||||
this.isLoading = true;
|
|
||||||
this.seriesService.getOnDeck(this.libraryId, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
|
||||||
this.series = series.result;
|
|
||||||
this.pagination = series.pagination;
|
|
||||||
this.isLoading = false;
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getPage() {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
return urlParams.get('page');
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkActionCallback = (action: Action, data: any) => {
|
|
||||||
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
|
|
||||||
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + ''));
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case Action.AddToReadingList:
|
|
||||||
this.actionService.addMultipleSeriesToReadingList(selectedSeries, () => {
|
|
||||||
this.bulkSelectionService.deselectAll();
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case Action.AddToCollection:
|
|
||||||
this.actionService.addMultipleSeriesToCollectionTag(selectedSeries, () => {
|
|
||||||
this.bulkSelectionService.deselectAll();
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case Action.MarkAsRead:
|
|
||||||
this.actionService.markMultipleSeriesAsRead(selectedSeries, () => {
|
|
||||||
this.loadPage();
|
|
||||||
this.bulkSelectionService.deselectAll();
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case Action.MarkAsUnread:
|
|
||||||
this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => {
|
|
||||||
this.loadPage();
|
|
||||||
this.bulkSelectionService.deselectAll();
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case Action.Delete:
|
|
||||||
this.actionService.deleteMultipleSeries(selectedSeries, () => {
|
|
||||||
this.loadPage();
|
|
||||||
this.bulkSelectionService.deselectAll();
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
<app-side-nav-companion-bar [showGoBack]="true">
|
<app-side-nav-companion-bar>
|
||||||
<h2 title>
|
<h2 title>
|
||||||
<span *ngIf="actions.length > 0">
|
<span *ngIf="actions.length > 0">
|
||||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="readingList.title"></app-card-actionables>
|
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="readingList.title"></app-card-actionables>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<app-side-nav-companion-bar [showGoBack]="true" pageHeader="Home">
|
<app-side-nav-companion-bar pageHeader="Home">
|
||||||
<h2 title>
|
<h2 title>
|
||||||
<app-card-actionables [actions]="actions"></app-card-actionables>
|
<app-card-actionables [actions]="actions"></app-card-actionables>
|
||||||
Reading Lists
|
Reading Lists
|
||||||
|
@ -498,7 +498,12 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||||||
// If user has progress on the volume, load them where they left off
|
// If user has progress on the volume, load them where they left off
|
||||||
if (volume.pagesRead < volume.pages && volume.pagesRead > 0) {
|
if (volume.pagesRead < volume.pages && volume.pagesRead > 0) {
|
||||||
// Find the continue point chapter and load it
|
// Find the continue point chapter and load it
|
||||||
this.readerService.getCurrentChapter(this.seriesId).subscribe(chapter => this.openChapter(chapter));
|
const unreadChapters = volume.chapters.filter(item => item.pagesRead < item.pages);
|
||||||
|
if (unreadChapters.length > 0) {
|
||||||
|
this.openChapter(unreadChapters[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.openChapter(volume.chapters[0]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -511,7 +516,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openViewInfo(data: Volume | Chapter) {
|
openViewInfo(data: Volume | Chapter) {
|
||||||
const modalRef = this.modalService.open(CardDetailsModalComponent, { size: 'lg' }); // , scrollable: true (these don't work well on mobile)
|
const modalRef = this.modalService.open(CardDetailsModalComponent, { size: 'lg' });
|
||||||
modalRef.componentInstance.data = data;
|
modalRef.componentInstance.data = data;
|
||||||
modalRef.componentInstance.parentName = this.series?.name;
|
modalRef.componentInstance.parentName = this.series?.name;
|
||||||
modalRef.componentInstance.seriesId = this.series?.id;
|
modalRef.componentInstance.seriesId = this.series?.id;
|
||||||
|
@ -6,8 +6,6 @@
|
|||||||
<div class="row g-0 mb-2">
|
<div class="row g-0 mb-2">
|
||||||
<app-tag-badge title="Age Rating" *ngIf="seriesMetadata.ageRating" a11y-click="13,32" class="clickable col-auto" (click)="goTo('ageRating', seriesMetadata.ageRating)" [selectionMode]="TagBadgeCursor.Clickable">{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}</app-tag-badge>
|
<app-tag-badge title="Age Rating" *ngIf="seriesMetadata.ageRating" a11y-click="13,32" class="clickable col-auto" (click)="goTo('ageRating', seriesMetadata.ageRating)" [selectionMode]="TagBadgeCursor.Clickable">{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}</app-tag-badge>
|
||||||
<ng-container *ngIf="series">
|
<ng-container *ngIf="series">
|
||||||
<!-- Maybe we can put the library this resides in to make it easier to get back -->
|
|
||||||
<!-- tooltip here explaining how this is year of first issue -->
|
|
||||||
<app-tag-badge *ngIf="seriesMetadata.releaseYear > 0" title="Release date" class="col-auto">{{seriesMetadata.releaseYear}}</app-tag-badge>
|
<app-tag-badge *ngIf="seriesMetadata.releaseYear > 0" title="Release date" class="col-auto">{{seriesMetadata.releaseYear}}</app-tag-badge>
|
||||||
<app-tag-badge *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''" title="Language" a11y-click="13,32" class="col-auto" (click)="goTo('languages', seriesMetadata.language)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.language}}</app-tag-badge>
|
<app-tag-badge *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''" title="Language" a11y-click="13,32" class="col-auto" (click)="goTo('languages', seriesMetadata.language)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.language}}</app-tag-badge>
|
||||||
<app-tag-badge title="Publication Status" a11y-click="13,32" class="col-auto" (click)="goTo('publicationStatus', seriesMetadata.publicationStatus)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.publicationStatus | publicationStatus}}</app-tag-badge>
|
<app-tag-badge title="Publication Status" a11y-click="13,32" class="col-auto" (click)="goTo('publicationStatus', seriesMetadata.publicationStatus)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.publicationStatus | publicationStatus}}</app-tag-badge>
|
||||||
|
@ -208,6 +208,18 @@ export class UtilityService {
|
|||||||
anyChanged = true;
|
anyChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read status is encoded as true,true,true
|
||||||
|
const readStatus = snapshot.queryParamMap.get('readStatus');
|
||||||
|
if (readStatus !== undefined && readStatus !== null) {
|
||||||
|
const values = readStatus.split(',').map(i => i === "true");
|
||||||
|
if (values.length === 3) {
|
||||||
|
filter.readStatus.inProgress = values[0];
|
||||||
|
filter.readStatus.notRead = values[1];
|
||||||
|
filter.readStatus.read = values[2];
|
||||||
|
anyChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return [filter, anyChanged];
|
return [filter, anyChanged];
|
||||||
}
|
}
|
||||||
|
@ -10,11 +10,6 @@ import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateR
|
|||||||
styleUrls: ['./side-nav-companion-bar.component.scss']
|
styleUrls: ['./side-nav-companion-bar.component.scss']
|
||||||
})
|
})
|
||||||
export class SideNavCompanionBarComponent implements OnInit {
|
export class SideNavCompanionBarComponent implements OnInit {
|
||||||
|
|
||||||
/**
|
|
||||||
* Show a dedicated button to go back one history event.
|
|
||||||
*/
|
|
||||||
@Input() showGoBack: boolean = false;
|
|
||||||
/**
|
/**
|
||||||
* If the page should show a filter
|
* If the page should show a filter
|
||||||
*/
|
*/
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
<ng-container>
|
<ng-container>
|
||||||
<div class="side-nav" [ngClass]="{'closed' : !(navService?.sideNavCollapsed$ | async), 'hidden' :!(navService?.sideNavVisibility$ | async)}" *ngIf="accountService.currentUser$ | async as user">
|
<div class="side-nav" [ngClass]="{'closed' : !(navService?.sideNavCollapsed$ | async), 'hidden' :!(navService?.sideNavVisibility$ | async)}" *ngIf="accountService.currentUser$ | async as user">
|
||||||
<app-side-nav-item icon="fa-user-circle align-self-center phone-hidden" [title]="user.username | sentenceCase" link="/preferences/">
|
<!-- <app-side-nav-item icon="fa-user-circle align-self-center phone-hidden" [title]="user.username | sentenceCase" link="/preferences/">
|
||||||
<ng-container actions>
|
<ng-container actions>
|
||||||
<!-- Todo: This will be customize dashboard/side nav controls-->
|
Todo: This will be customize dashboard/side nav controls
|
||||||
<a href="/preferences/" title="User Settings"><span class="visually-hidden">User Settings</span></a>
|
<a href="/preferences/" title="User Settings"><span class="visually-hidden">User Settings</span></a>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</app-side-nav-item>
|
</app-side-nav-item> -->
|
||||||
|
|
||||||
<div class="mt-3">
|
<app-side-nav-item icon="fa-home" title="Home" link="/library/"></app-side-nav-item>
|
||||||
<app-side-nav-item icon="fa-home" title="Home" link="/library/"></app-side-nav-item>
|
|
||||||
<app-side-nav-item icon="fa-list" title="Collections" link="/collections/"></app-side-nav-item>
|
<app-side-nav-item icon="fa-list" title="Collections" link="/collections/"></app-side-nav-item>
|
||||||
<app-side-nav-item icon="fa-list-ol" title="Reading Lists" link="/lists/"></app-side-nav-item>
|
<app-side-nav-item icon="fa-list-ol" title="Reading Lists" link="/lists/"></app-side-nav-item>
|
||||||
<app-side-nav-item icon="fa-regular fa-rectangle-list" title="All Series" link="/all-series/"></app-side-nav-item>
|
<app-side-nav-item icon="fa-regular fa-rectangle-list" title="All Series" link="/all-series/"></app-side-nav-item>
|
||||||
@ -24,8 +23,7 @@
|
|||||||
<ng-container actions>
|
<ng-container actions>
|
||||||
<app-card-actionables [actions]="actions" [labelBy]="library.name" iconClass="fa-ellipsis-v" (actionHandler)="performAction($event, library)"></app-card-actionables>
|
<app-card-actionables [actions]="actions" [labelBy]="library.name" iconClass="fa-ellipsis-v" (actionHandler)="performAction($event, library)"></app-card-actionables>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</app-side-nav-item>
|
</app-side-nav-item>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="side-nav-overlay" [ngClass]="{'closed' : !(navService?.sideNavCollapsed$ | async)}"></div>
|
<div class="side-nav-overlay" (click)="navService?.toggleSideNav()" [ngClass]="{'closed' : !(navService?.sideNavCollapsed$ | async)}"></div>
|
||||||
</ng-container>
|
</ng-container>
|
@ -1,5 +1,5 @@
|
|||||||
.side-nav {
|
.side-nav {
|
||||||
padding: 10px 0;
|
padding-bottom: 10px;
|
||||||
width: 190px;
|
width: 190px;
|
||||||
background-color: var(--side-nav-bg-color);
|
background-color: var(--side-nav-bg-color);
|
||||||
height: calc(100vh - 85px);
|
height: calc(100vh - 85px);
|
||||||
@ -24,12 +24,17 @@
|
|||||||
background-color: var(--side-nav-closed-bg-color);
|
background-color: var(--side-nav-closed-bg-color);
|
||||||
border: var(--side-nav-border-closed);
|
border: var(--side-nav-border-closed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-nav-item:first() {
|
||||||
|
border-top-left-radius: var(--side-nav-border-radius);
|
||||||
|
border-top-right-radius: var(--side-nav-border-radius);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
.side-nav {
|
.side-nav {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
width: 90vw;
|
width: 55vw;
|
||||||
background-color: var(--side-nav-mobile-bg-color);
|
background-color: var(--side-nav-mobile-bg-color);
|
||||||
height: calc(100vh - 56px);
|
height: calc(100vh - 56px);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -46,6 +51,11 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-nav-item:first() {
|
||||||
|
border-top-left-radius: var(--side-nav-border-radius);
|
||||||
|
border-top-right-radius: var(--side-nav-border-radius);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-nav-overlay {
|
.side-nav-overlay {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { take } from 'rxjs/operators';
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
import { take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||||
|
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
|
||||||
import { UtilityService } from '../../shared/_services/utility.service';
|
import { UtilityService } from '../../shared/_services/utility.service';
|
||||||
import { Library } from '../../_models/library';
|
import { Library } from '../../_models/library';
|
||||||
import { User } from '../../_models/user';
|
import { User } from '../../_models/user';
|
||||||
@ -15,7 +17,7 @@ import { NavService } from '../../_services/nav.service';
|
|||||||
templateUrl: './side-nav.component.html',
|
templateUrl: './side-nav.component.html',
|
||||||
styleUrls: ['./side-nav.component.scss']
|
styleUrls: ['./side-nav.component.scss']
|
||||||
})
|
})
|
||||||
export class SideNavComponent implements OnInit {
|
export class SideNavComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
user: User | undefined;
|
user: User | undefined;
|
||||||
libraries: Library[] = [];
|
libraries: Library[] = [];
|
||||||
@ -27,9 +29,11 @@ export class SideNavComponent implements OnInit {
|
|||||||
return library.name.toLowerCase().indexOf((this.filterQuery || '').toLowerCase()) >= 0;
|
return library.name.toLowerCase().indexOf((this.filterQuery || '').toLowerCase()) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onDestory: Subject<void> = new Subject();
|
||||||
|
|
||||||
|
|
||||||
constructor(public accountService: AccountService, private libraryService: LibraryService,
|
constructor(public accountService: AccountService, private libraryService: LibraryService,
|
||||||
public utilityService: UtilityService, private router: Router,
|
public utilityService: UtilityService, private messageHub: MessageHubService,
|
||||||
private actionFactoryService: ActionFactoryService, private actionService: ActionService, public navService: NavService) { }
|
private actionFactoryService: ActionFactoryService, private actionService: ActionService, public navService: NavService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -44,7 +48,18 @@ export class SideNavComponent implements OnInit {
|
|||||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.messageHub.messages$.pipe(takeUntil(this.onDestory), takeWhile(event => event.event === EVENTS.LibraryModified)).subscribe(event => {
|
||||||
|
this.libraryService.getLibrariesForMember().pipe(take(1)).subscribe((libraries: Library[]) => {
|
||||||
|
this.libraries = libraries;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.onDestory.next();
|
||||||
|
this.onDestory.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAction(action: Action, library: Library) {
|
handleAction(action: Action, library: Library) {
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
<div class="spinner-border spinner-border-sm {{settings.multiple ? 'close-offset' : ''}}" role="status" *ngIf="isLoadingOptions">
|
<div class="spinner-border spinner-border-sm {{settings.multiple ? 'close-offset' : ''}}" role="status" *ngIf="isLoadingOptions">
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="settings.multiple">
|
<ng-container *ngIf="settings.multiple && (selectedData | async) as selected">
|
||||||
<button class="btn btn-close float-end mt-2" style="font-size: 0.8rem;" (click)="clearSelections($event)"></button>
|
<button class="btn btn-close float-end mt-2" *ngIf="selected.length > 0" style="font-size: 0.8rem;" (click)="clearSelections($event)"></button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,3 +107,15 @@ input {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
top: 30%;
|
top: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
.list-group-item {
|
||||||
|
cursor: pointer;
|
||||||
|
border-left-color: var(--list-group-hover-text-color);
|
||||||
|
border-right-color: var(--list-group-hover-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:last-child {
|
||||||
|
border-bottom-color: var(--list-group-hover-text-color);
|
||||||
|
}
|
||||||
|
}
|
@ -5,8 +5,6 @@ import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap
|
|||||||
import { KEY_CODES } from '../shared/_services/utility.service';
|
import { KEY_CODES } from '../shared/_services/utility.service';
|
||||||
import { SelectionCompareFn, TypeaheadSettings } from './typeahead-settings';
|
import { SelectionCompareFn, TypeaheadSettings } from './typeahead-settings';
|
||||||
|
|
||||||
//export type SelectionCompareFn<T> = (a: T, b: T) => boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SelectionModel<T> is used for keeping track of multiple selections. Simple interface with ability to toggle.
|
* SelectionModel<T> is used for keeping track of multiple selections. Simple interface with ability to toggle.
|
||||||
* @param selectedState Optional state to set selectedOptions to. If not passed, defaults to false.
|
* @param selectedState Optional state to set selectedOptions to. If not passed, defaults to false.
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input #apiKey type="text" readonly class="form-control" id="api-key--{{title}}" aria-describedby="button-addon4" [value]="key" (click)="selectAll()">
|
<input #apiKey type="text" readonly class="form-control" id="api-key--{{title}}" aria-describedby="button-addon4" [value]="key" (click)="selectAll()">
|
||||||
<div id="button-addon4">
|
<div id="button-addon4">
|
||||||
<button class="btn btn-outline-secondary" type="button" (click)="copy()"><span class="visually-hidden">Copy</span><i class="fa fa-copy" aria-hidden="true"></i></button>
|
<button class="btn btn-outline-secondary" type="button" (click)="copy()" title="Copy"><span class="visually-hidden">Copy</span><i class="fa fa-copy" aria-hidden="true"></i></button>
|
||||||
<button class="btn btn-danger" type="button" [ngbTooltip]="tipContent" (click)="refresh()" *ngIf="showRefresh"><span class="visually-hidden">Regenerate</span><i class="fa fa-sync-alt" aria-hidden="true"></i></button>
|
<button class="btn btn-danger" type="button" [ngbTooltip]="tipContent" (click)="refresh()" *ngIf="showRefresh"><span class="visually-hidden">Regenerate</span><i class="fa fa-sync-alt" aria-hidden="true"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<ng-template #tipContent>
|
<ng-template #tipContent>
|
||||||
|
@ -36,7 +36,8 @@ import { ColorPickerModule } from 'ngx-color-picker';
|
|||||||
ColorPickerModule, // User prefernces background color
|
ColorPickerModule, // User prefernces background color
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
SiteThemeProviderPipe
|
SiteThemeProviderPipe,
|
||||||
|
ApiKeyComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class UserSettingsModule { }
|
export class UserSettingsModule { }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user