mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-31 20:24:27 -04:00
UI Updates + New Events (#806)
* Implemented ability to see downloads users are performing on the events widget. * Fixed a bug where version update task was calling wrong code * Fixed a bug where when checking for updates, the event wouldn't be pushed to server with correct name. Added update check to the event widget rather than opening a modal on the user. * Relaxed password requirements to only be 6-32 characters and inform user on register form about the requirements * Removed a ton of duplicate logic for series cards where the logic was already defined in action service * Fixed OPDS total items giving a rounded number rather than total items. * Fixed off by one issue on OPDS pagination
This commit is contained in:
parent
69bd087697
commit
e248cf7579
@ -11,9 +11,11 @@ using API.Extensions;
|
|||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
using API.SignalR;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
@ -25,16 +27,19 @@ namespace API.Controllers
|
|||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
private readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
private readonly IDownloadService _downloadService;
|
private readonly IDownloadService _downloadService;
|
||||||
|
private readonly IHubContext<MessageHub> _messageHub;
|
||||||
private readonly NumericComparer _numericComparer;
|
private readonly NumericComparer _numericComparer;
|
||||||
private const string DefaultContentType = "application/octet-stream";
|
private const string DefaultContentType = "application/octet-stream";
|
||||||
|
|
||||||
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService, ICacheService cacheService, IDownloadService downloadService)
|
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService,
|
||||||
|
ICacheService cacheService, IDownloadService downloadService, IHubContext<MessageHub> messageHub)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_archiveService = archiveService;
|
_archiveService = archiveService;
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_downloadService = downloadService;
|
_downloadService = downloadService;
|
||||||
|
_messageHub = messageHub;
|
||||||
_numericComparer = new NumericComparer();
|
_numericComparer = new NumericComparer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,13 +72,7 @@ namespace API.Controllers
|
|||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (files.Count == 1)
|
return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series.Name} - Volume {volume.Number}.zip");
|
||||||
{
|
|
||||||
return await GetFirstFileDownload(files);
|
|
||||||
}
|
|
||||||
var (fileBytes, _) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
|
|
||||||
$"download_{User.GetUsername()}_v{volumeId}");
|
|
||||||
return File(fileBytes, DefaultContentType, $"{series.Name} - Volume {volume.Number}.zip");
|
|
||||||
}
|
}
|
||||||
catch (KavitaException ex)
|
catch (KavitaException ex)
|
||||||
{
|
{
|
||||||
@ -96,13 +95,7 @@ namespace API.Controllers
|
|||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (files.Count == 1)
|
return await DownloadFiles(files, $"download_{User.GetUsername()}_c{chapterId}", $"{series.Name} - Chapter {chapter.Number}.zip");
|
||||||
{
|
|
||||||
return await GetFirstFileDownload(files);
|
|
||||||
}
|
|
||||||
var (fileBytes, _) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
|
|
||||||
$"download_{User.GetUsername()}_c{chapterId}");
|
|
||||||
return File(fileBytes, DefaultContentType, $"{series.Name} - Chapter {chapter.Number}.zip");
|
|
||||||
}
|
}
|
||||||
catch (KavitaException ex)
|
catch (KavitaException ex)
|
||||||
{
|
{
|
||||||
@ -110,6 +103,21 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<ActionResult> DownloadFiles(ICollection<MangaFile> files, string tempFolder, string downloadName)
|
||||||
|
{
|
||||||
|
await _messageHub.Clients.All.SendAsync(SignalREvents.DownloadProgress,
|
||||||
|
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(downloadName), 0F));
|
||||||
|
if (files.Count == 1)
|
||||||
|
{
|
||||||
|
return await GetFirstFileDownload(files);
|
||||||
|
}
|
||||||
|
var (fileBytes, _) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
|
||||||
|
tempFolder);
|
||||||
|
await _messageHub.Clients.All.SendAsync(SignalREvents.DownloadProgress,
|
||||||
|
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(downloadName), 1F));
|
||||||
|
return File(fileBytes, DefaultContentType, downloadName);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("series")]
|
[HttpGet("series")]
|
||||||
public async Task<ActionResult> DownloadSeries(int seriesId)
|
public async Task<ActionResult> DownloadSeries(int seriesId)
|
||||||
{
|
{
|
||||||
@ -117,13 +125,7 @@ namespace API.Controllers
|
|||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (files.Count == 1)
|
return await DownloadFiles(files, $"download_{User.GetUsername()}_s{seriesId}", $"{series.Name}.zip");
|
||||||
{
|
|
||||||
return await GetFirstFileDownload(files);
|
|
||||||
}
|
|
||||||
var (fileBytes, _) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
|
|
||||||
$"download_{User.GetUsername()}_s{seriesId}");
|
|
||||||
return File(fileBytes, DefaultContentType, $"{series.Name}.zip");
|
|
||||||
}
|
}
|
||||||
catch (KavitaException ex)
|
catch (KavitaException ex)
|
||||||
{
|
{
|
||||||
@ -187,5 +189,6 @@ namespace API.Controllers
|
|||||||
DirectoryService.ClearAndDeleteDirectory(fullExtractPath);
|
DirectoryService.ClearAndDeleteDirectory(fullExtractPath);
|
||||||
return File(fileBytes, DefaultContentType, $"{series.Name} - Bookmarks.zip");
|
return File(fileBytes, DefaultContentType, $"{series.Name} - Bookmarks.zip");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -583,7 +583,7 @@ namespace API.Controllers
|
|||||||
feed.Links.Add(CreateLink(FeedLinkRelation.Prev, FeedLinkType.AtomNavigation, url + "pageNumber=" + (pageNumber - 1)));
|
feed.Links.Add(CreateLink(FeedLinkRelation.Prev, FeedLinkType.AtomNavigation, url + "pageNumber=" + (pageNumber - 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageNumber + 1 < list.TotalPages)
|
if (pageNumber + 1 <= list.TotalPages)
|
||||||
{
|
{
|
||||||
feed.Links.Add(CreateLink(FeedLinkRelation.Next, FeedLinkType.AtomNavigation, url + "pageNumber=" + (pageNumber + 1)));
|
feed.Links.Add(CreateLink(FeedLinkRelation.Next, FeedLinkType.AtomNavigation, url + "pageNumber=" + (pageNumber + 1)));
|
||||||
}
|
}
|
||||||
@ -596,7 +596,7 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
feed.Total = list.TotalPages * list.PageSize;
|
feed.Total = list.TotalCount;
|
||||||
feed.ItemsPerPage = list.PageSize;
|
feed.ItemsPerPage = list.PageSize;
|
||||||
feed.StartIndex = (Math.Max(list.CurrentPage - 1, 0) * list.PageSize) + 1;
|
feed.StartIndex = (Math.Max(list.CurrentPage - 1, 0) * list.PageSize) + 1;
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,13 @@ namespace API.Extensions
|
|||||||
{
|
{
|
||||||
services.AddIdentityCore<AppUser>(opt =>
|
services.AddIdentityCore<AppUser>(opt =>
|
||||||
{
|
{
|
||||||
// Change password / signin requirements here
|
|
||||||
opt.Password.RequireNonAlphanumeric = false;
|
opt.Password.RequireNonAlphanumeric = false;
|
||||||
|
opt.Password.RequireDigit = false;
|
||||||
|
opt.Password.RequireDigit = false;
|
||||||
|
opt.Password.RequireLowercase = false;
|
||||||
|
opt.Password.RequireUppercase = false;
|
||||||
|
opt.Password.RequireNonAlphanumeric = false;
|
||||||
|
opt.Password.RequiredLength = 6;
|
||||||
})
|
})
|
||||||
.AddRoles<AppRole>()
|
.AddRoles<AppRole>()
|
||||||
.AddRoleManager<RoleManager<AppRole>>()
|
.AddRoleManager<RoleManager<AppRole>>()
|
||||||
|
@ -120,7 +120,7 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Scheduling Auto-Update tasks");
|
_logger.LogInformation("Scheduling Auto-Update tasks");
|
||||||
// Schedule update check between noon and 6pm local time
|
// Schedule update check between noon and 6pm local time
|
||||||
RecurringJob.AddOrUpdate("check-updates", () => _versionUpdaterService.CheckForUpdate(), Cron.Daily(Rnd.Next(12, 18)), TimeZoneInfo.Local);
|
RecurringJob.AddOrUpdate("check-updates", () => CheckForUpdate(), Cron.Daily(Rnd.Next(12, 18)), TimeZoneInfo.Local);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -86,10 +86,6 @@ namespace API.Services.Tasks
|
|||||||
return CreateDto(update);
|
return CreateDto(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<IEnumerable<UpdateNotificationDto>> GetAllReleases()
|
public async Task<IEnumerable<UpdateNotificationDto>> GetAllReleases()
|
||||||
{
|
{
|
||||||
var updates = await GetGithubReleases();
|
var updates = await GetGithubReleases();
|
||||||
@ -140,13 +136,7 @@ namespace API.Services.Tasks
|
|||||||
|
|
||||||
private async Task SendEvent(UpdateNotificationDto update, IReadOnlyList<string> admins)
|
private async Task SendEvent(UpdateNotificationDto update, IReadOnlyList<string> admins)
|
||||||
{
|
{
|
||||||
var connections = new List<string>();
|
await _messageHub.Clients.Users(admins).SendAsync(SignalREvents.UpdateAvailable, MessageFactory.UpdateVersionEvent(update));
|
||||||
foreach (var admin in admins)
|
|
||||||
{
|
|
||||||
connections.AddRange(await _tracker.GetConnectionsForUser(admin));
|
|
||||||
}
|
|
||||||
|
|
||||||
await _messageHub.Clients.Users(admins).SendAsync(SignalREvents.UpdateVersion, MessageFactory.UpdateVersionEvent(update));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ namespace API.SignalR
|
|||||||
{
|
{
|
||||||
return new SignalRMessage
|
return new SignalRMessage
|
||||||
{
|
{
|
||||||
Name = SignalREvents.UpdateVersion,
|
Name = SignalREvents.UpdateAvailable,
|
||||||
Body = update
|
Body = update
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ namespace API.SignalR
|
|||||||
{
|
{
|
||||||
return new SignalRMessage
|
return new SignalRMessage
|
||||||
{
|
{
|
||||||
Name = SignalREvents.UpdateVersion,
|
Name = SignalREvents.UpdateAvailable,
|
||||||
Body = new
|
Body = new
|
||||||
{
|
{
|
||||||
TagId = tagId,
|
TagId = tagId,
|
||||||
@ -147,5 +147,19 @@ namespace API.SignalR
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SignalRMessage DownloadProgressEvent(string username, string downloadName, float progress)
|
||||||
|
{
|
||||||
|
return new SignalRMessage()
|
||||||
|
{
|
||||||
|
Name = SignalREvents.DownloadProgress,
|
||||||
|
Body = new
|
||||||
|
{
|
||||||
|
UserName = username,
|
||||||
|
DownloadName = downloadName,
|
||||||
|
Progress = progress
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
public static class SignalREvents
|
public static class SignalREvents
|
||||||
{
|
{
|
||||||
public const string UpdateVersion = "UpdateVersion";
|
public const string UpdateAvailable = "UpdateAvailable";
|
||||||
public const string ScanSeries = "ScanSeries";
|
public const string ScanSeries = "ScanSeries";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event during Refresh Metadata for cover image change
|
/// Event during Refresh Metadata for cover image change
|
||||||
@ -27,5 +27,10 @@
|
|||||||
/// Event sent out during cleaning up temp and cache folders
|
/// Event sent out during cleaning up temp and cache folders
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string CleanupProgress = "CleanupProgress";
|
public const string CleanupProgress = "CleanupProgress";
|
||||||
|
/// <summary>
|
||||||
|
/// Event sent out during downloading of files
|
||||||
|
/// </summary>
|
||||||
|
public const string DownloadProgress = "DownloadProgress";
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ export type VolumeActionCallback = (volume: Volume) => void;
|
|||||||
export type ChapterActionCallback = (chapter: Chapter) => void;
|
export type ChapterActionCallback = (chapter: Chapter) => void;
|
||||||
export type ReadingListActionCallback = (readingList: ReadingList) => void;
|
export type ReadingListActionCallback = (readingList: ReadingList) => void;
|
||||||
export type VoidActionCallback = () => void;
|
export type VoidActionCallback = () => void;
|
||||||
|
export type BooleanActionCallback = (result: boolean) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible for executing actions
|
* Responsible for executing actions
|
||||||
@ -138,6 +139,9 @@ export class ActionService implements OnDestroy {
|
|||||||
*/
|
*/
|
||||||
async refreshMetdata(series: Series, callback?: SeriesActionCallback) {
|
async refreshMetdata(series: Series, callback?: SeriesActionCallback) {
|
||||||
if (!await this.confirmService.confirm('Refresh metadata will force all cover images and metadata to be recalculated. This is a heavy operation. Are you sure you don\'t want to perform a Scan instead?')) {
|
if (!await this.confirmService.confirm('Refresh metadata will force all cover images and metadata to be recalculated. This is a heavy operation. Are you sure you don\'t want to perform a Scan instead?')) {
|
||||||
|
if (callback) {
|
||||||
|
callback(series);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -484,4 +488,20 @@ export class ActionService implements OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteSeries(series: Series, callback?: BooleanActionCallback) {
|
||||||
|
if (!await this.confirmService.confirm('Are you sure you want to delete this series? It will not modify files on disk.')) {
|
||||||
|
if (callback) {
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.seriesService.delete(series.id).subscribe((res: boolean) => {
|
||||||
|
if (callback) {
|
||||||
|
this.toastr.success('Series deleted');
|
||||||
|
callback(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,8 @@ export enum EVENTS {
|
|||||||
SeriesAddedToCollection = 'SeriesAddedToCollection',
|
SeriesAddedToCollection = 'SeriesAddedToCollection',
|
||||||
ScanLibraryError = 'ScanLibraryError',
|
ScanLibraryError = 'ScanLibraryError',
|
||||||
BackupDatabaseProgress = 'BackupDatabaseProgress',
|
BackupDatabaseProgress = 'BackupDatabaseProgress',
|
||||||
CleanupProgress = 'CleanupProgress'
|
CleanupProgress = 'CleanupProgress',
|
||||||
|
DownloadProgress = 'DownloadProgress'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Message<T> {
|
export interface Message<T> {
|
||||||
@ -38,7 +39,6 @@ export interface Message<T> {
|
|||||||
export class MessageHubService {
|
export class MessageHubService {
|
||||||
hubUrl = environment.hubUrl;
|
hubUrl = environment.hubUrl;
|
||||||
private hubConnection!: HubConnection;
|
private hubConnection!: HubConnection;
|
||||||
private updateNotificationModalRef: NgbModalRef | null = null;
|
|
||||||
|
|
||||||
private messagesSource = new ReplaySubject<Message<any>>(1);
|
private messagesSource = new ReplaySubject<Message<any>>(1);
|
||||||
public messages$ = this.messagesSource.asObservable();
|
public messages$ = this.messagesSource.asObservable();
|
||||||
@ -53,7 +53,7 @@ export class MessageHubService {
|
|||||||
|
|
||||||
isAdmin: boolean = false;
|
isAdmin: boolean = false;
|
||||||
|
|
||||||
constructor(private modalService: NgbModal, private toastr: ToastrService, private router: Router) {
|
constructor(private toastr: ToastrService, private router: Router) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +106,13 @@ export class MessageHubService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.hubConnection.on(EVENTS.DownloadProgress, resp => {
|
||||||
|
this.messagesSource.next({
|
||||||
|
event: EVENTS.DownloadProgress,
|
||||||
|
payload: resp.body
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.hubConnection.on(EVENTS.RefreshMetadataProgress, resp => {
|
this.hubConnection.on(EVENTS.RefreshMetadataProgress, resp => {
|
||||||
this.messagesSource.next({
|
this.messagesSource.next({
|
||||||
event: EVENTS.RefreshMetadataProgress,
|
event: EVENTS.RefreshMetadataProgress,
|
||||||
@ -162,16 +169,6 @@ export class MessageHubService {
|
|||||||
event: EVENTS.UpdateAvailable,
|
event: EVENTS.UpdateAvailable,
|
||||||
payload: resp.body
|
payload: resp.body
|
||||||
});
|
});
|
||||||
// Ensure only 1 instance of UpdateNotificationModal can be open at once
|
|
||||||
if (this.updateNotificationModalRef != null) { return; }
|
|
||||||
this.updateNotificationModalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' });
|
|
||||||
this.updateNotificationModalRef.componentInstance.updateData = resp.body;
|
|
||||||
this.updateNotificationModalRef.closed.subscribe(() => {
|
|
||||||
this.updateNotificationModalRef = null;
|
|
||||||
});
|
|
||||||
this.updateNotificationModalRef.dismissed.subscribe(() => {
|
|
||||||
this.updateNotificationModalRef = null;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,35 +132,26 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshMetdata(series: Series) {
|
async refreshMetdata(series: Series) {
|
||||||
this.seriesService.refreshMetadata(series).subscribe((res: any) => {
|
this.actionService.refreshMetdata(series);
|
||||||
this.toastr.success('Refresh started for ' + series.name);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scanLibrary(series: Series) {
|
async scanLibrary(series: Series) {
|
||||||
this.seriesService.scan(series.libraryId, series.id).subscribe((res: any) => {
|
this.seriesService.scan(series.libraryId, series.id).subscribe((res: any) => {
|
||||||
this.toastr.success('Scan started for ' + series.name);
|
this.toastr.success('Scan started for ' + series.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSeries(series: Series) {
|
async deleteSeries(series: Series) {
|
||||||
if (!await this.confirmService.confirm('Are you sure you want to delete this series? It will not modify files on disk.')) {
|
this.actionService.deleteSeries(series, (result: boolean) => {
|
||||||
return;
|
if (result) {
|
||||||
}
|
|
||||||
|
|
||||||
this.seriesService.delete(series.id).subscribe((res: boolean) => {
|
|
||||||
if (res) {
|
|
||||||
this.toastr.success('Series deleted');
|
|
||||||
this.reload.emit(true);
|
this.reload.emit(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
markAsUnread(series: Series) {
|
markAsUnread(series: Series) {
|
||||||
this.seriesService.markUnread(series.id).subscribe(res => {
|
this.actionService.markSeriesAsUnread(series, () => {
|
||||||
this.toastr.success(series.name + ' is now unread');
|
|
||||||
series.pagesRead = 0;
|
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
this.data.pagesRead = 0;
|
this.data.pagesRead = 0;
|
||||||
}
|
}
|
||||||
@ -170,9 +161,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
markAsRead(series: Series) {
|
markAsRead(series: Series) {
|
||||||
this.seriesService.markRead(series.id).subscribe(res => {
|
this.actionService.markSeriesAsRead(series, () => {
|
||||||
this.toastr.success(series.name + ' is now read');
|
|
||||||
series.pagesRead = series.pages;
|
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
this.data.pagesRead = series.pages;
|
this.data.pagesRead = series.pages;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<ng-container>
|
<ng-container>
|
||||||
|
|
||||||
<button type="button" class="btn btn-icon {{progressEventsSource.getValue().length > 0 ? 'colored' : ''}}"
|
<button type="button" class="btn btn-icon {{(progressEventsSource.getValue().length > 0 || updateAvailable) ? 'colored' : ''}}"
|
||||||
[ngbPopover]="popContent" title="Activity" placement="bottom" [popoverClass]="'nav-events'">
|
[ngbPopover]="popContent" title="Activity" placement="bottom" [popoverClass]="'nav-events'">
|
||||||
<i aria-hidden="true" class="fa fa-wave-square"></i>
|
<i aria-hidden="true" class="fa fa-wave-square"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -14,9 +14,12 @@
|
|||||||
<span class="sr-only">Scan for {{event.libraryName}} in progress</span>
|
<span class="sr-only">Scan for {{event.libraryName}} in progress</span>
|
||||||
</div>
|
</div>
|
||||||
{{prettyPrintProgress(event.progress)}}%
|
{{prettyPrintProgress(event.progress)}}%
|
||||||
{{prettyPrintEvent(event.eventType)}} {{event.libraryName}}
|
{{prettyPrintEvent(event.eventType, event)}} {{event.libraryName}}
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item dark-menu-item" *ngIf="progressEventsSource.getValue().length === 0 && !updateAvailable">Not much going on here</li>
|
||||||
|
<li class="list-group-item dark-menu-item update-available" *ngIf="updateAvailable" (click)="handleUpdateAvailableClick()">
|
||||||
|
<i class="fa fa-chevron-circle-up" aria-hidden="true"></i> Update available
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item dark-menu-item" *ngIf="progressEventsSource.getValue().length === 0">Not much going on here</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
@ -20,4 +20,13 @@
|
|||||||
.colored {
|
.colored {
|
||||||
background-color: colors.$primary-color;
|
background-color: colors.$primary-color;
|
||||||
border-radius: 60px;
|
border-radius: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-available {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
i.fa {
|
||||||
|
color: colors.$primary-color !important;
|
||||||
|
}
|
||||||
|
color: colors.$primary-color;
|
||||||
}
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { BehaviorSubject, Subject } from 'rxjs';
|
import { BehaviorSubject, Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
import { UpdateNotificationModalComponent } from '../shared/update-notification/update-notification-modal.component';
|
||||||
import { ProgressEvent } from '../_models/events/scan-library-progress-event';
|
import { ProgressEvent } from '../_models/events/scan-library-progress-event';
|
||||||
import { User } from '../_models/user';
|
import { User } from '../_models/user';
|
||||||
import { LibraryService } from '../_services/library.service';
|
import { LibraryService } from '../_services/library.service';
|
||||||
@ -16,6 +18,8 @@ interface ProcessedEvent {
|
|||||||
|
|
||||||
type ProgressType = EVENTS.ScanLibraryProgress | EVENTS.RefreshMetadataProgress | EVENTS.BackupDatabaseProgress | EVENTS.CleanupProgress;
|
type ProgressType = EVENTS.ScanLibraryProgress | EVENTS.RefreshMetadataProgress | EVENTS.BackupDatabaseProgress | EVENTS.CleanupProgress;
|
||||||
|
|
||||||
|
const acceptedEvents = [EVENTS.ScanLibraryProgress, EVENTS.RefreshMetadataProgress, EVENTS.BackupDatabaseProgress, EVENTS.CleanupProgress, EVENTS.DownloadProgress];
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-nav-events-toggle',
|
selector: 'app-nav-events-toggle',
|
||||||
templateUrl: './nav-events-toggle.component.html',
|
templateUrl: './nav-events-toggle.component.html',
|
||||||
@ -33,7 +37,11 @@ export class NavEventsToggleComponent implements OnInit, OnDestroy {
|
|||||||
progressEventsSource = new BehaviorSubject<ProcessedEvent[]>([]);
|
progressEventsSource = new BehaviorSubject<ProcessedEvent[]>([]);
|
||||||
progressEvents$ = this.progressEventsSource.asObservable();
|
progressEvents$ = this.progressEventsSource.asObservable();
|
||||||
|
|
||||||
constructor(private messageHub: MessageHubService, private libraryService: LibraryService) { }
|
updateAvailable: boolean = false;
|
||||||
|
updateBody: any;
|
||||||
|
private updateNotificationModalRef: NgbModalRef | null = null;
|
||||||
|
|
||||||
|
constructor(private messageHub: MessageHubService, private libraryService: LibraryService, private modalService: NgbModal) { }
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.onDestroy.next();
|
this.onDestroy.next();
|
||||||
@ -43,8 +51,11 @@ export class NavEventsToggleComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => {
|
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => {
|
||||||
if (event.event === EVENTS.ScanLibraryProgress || event.event === EVENTS.RefreshMetadataProgress || event.event === EVENTS.BackupDatabaseProgress || event.event === EVENTS.CleanupProgress) {
|
if (acceptedEvents.includes(event.event)) {
|
||||||
this.processProgressEvent(event, event.event);
|
this.processProgressEvent(event, event.event);
|
||||||
|
} else if (event.event === EVENTS.UpdateAvailable) {
|
||||||
|
this.updateAvailable = true;
|
||||||
|
this.updateBody = event.payload;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -64,7 +75,7 @@ export class NavEventsToggleComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (scanEvent.progress !== 1) {
|
if (scanEvent.progress !== 1) {
|
||||||
const libraryName = names[scanEvent.libraryId] || '';
|
const libraryName = names[scanEvent.libraryId] || '';
|
||||||
const newEvent = {eventType: eventType, timestamp: scanEvent.eventTime, progress: scanEvent.progress, libraryId: scanEvent.libraryId, libraryName};
|
const newEvent = {eventType: eventType, timestamp: scanEvent.eventTime, progress: scanEvent.progress, libraryId: scanEvent.libraryId, libraryName, rawBody: event.payload};
|
||||||
data.push(newEvent);
|
data.push(newEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,16 +84,29 @@ export class NavEventsToggleComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleUpdateAvailableClick() {
|
||||||
|
if (this.updateNotificationModalRef != null) { return; }
|
||||||
|
this.updateNotificationModalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' });
|
||||||
|
this.updateNotificationModalRef.componentInstance.updateData = this.updateBody;
|
||||||
|
this.updateNotificationModalRef.closed.subscribe(() => {
|
||||||
|
this.updateNotificationModalRef = null;
|
||||||
|
});
|
||||||
|
this.updateNotificationModalRef.dismissed.subscribe(() => {
|
||||||
|
this.updateNotificationModalRef = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
prettyPrintProgress(progress: number) {
|
prettyPrintProgress(progress: number) {
|
||||||
return Math.trunc(progress * 100);
|
return Math.trunc(progress * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
prettyPrintEvent(eventType: string) {
|
prettyPrintEvent(eventType: string, event: any) {
|
||||||
switch(eventType) {
|
switch(eventType) {
|
||||||
case (EVENTS.ScanLibraryProgress): return 'Scanning ';
|
case (EVENTS.ScanLibraryProgress): return 'Scanning ';
|
||||||
case (EVENTS.RefreshMetadataProgress): return 'Refreshing ';
|
case (EVENTS.RefreshMetadataProgress): return 'Refreshing ';
|
||||||
case (EVENTS.CleanupProgress): return 'Clearing Cache';
|
case (EVENTS.CleanupProgress): return 'Clearing Cache';
|
||||||
case (EVENTS.BackupDatabaseProgress): return 'Backing up Database';
|
case (EVENTS.BackupDatabaseProgress): return 'Backing up Database';
|
||||||
|
case (EVENTS.DownloadProgress): return event.rawBody.userName.charAt(0).toUpperCase() + event.rawBody.userName.substr(1) + ' is downloading ' + event.rawBody.downloadName;
|
||||||
default: return eventType;
|
default: return eventType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" *ngIf="registerForm.get('isAdmin')?.value || !authDisabled">
|
<div class="form-group" *ngIf="registerForm.get('isAdmin')?.value || !authDisabled">
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="passwordTooltip" role="button" tabindex="0"></i>
|
||||||
<input id="password" class="form-control" formControlName="password" type="password">
|
<ng-template #passwordTooltip>
|
||||||
|
Password must be between 6 and 32 characters in length
|
||||||
|
</ng-template>
|
||||||
|
<span class="sr-only" id="password-help"><ng-container [ngTemplateOutlet]="passwordTooltip"></ng-container></span>
|
||||||
|
<input id="password" class="form-control" formControlName="password" type="password" aria-describedby="password-help">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check" *ngIf="!firstTimeFlow">
|
<div class="form-check" *ngIf="!firstTimeFlow">
|
||||||
|
@ -302,17 +302,11 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
|
|
||||||
async deleteSeries(series: Series) {
|
async deleteSeries(series: Series) {
|
||||||
if (!await this.confirmService.confirm('Are you sure you want to delete this series? It will not modify files on disk.')) {
|
this.actionService.deleteSeries(series, (result: boolean) => {
|
||||||
this.actionInProgress = false;
|
this.actionInProgress = false;
|
||||||
return;
|
if (result) {
|
||||||
}
|
|
||||||
|
|
||||||
this.seriesService.delete(series.id).subscribe((res: boolean) => {
|
|
||||||
if (res) {
|
|
||||||
this.toastr.success('Series deleted');
|
|
||||||
this.router.navigate(['library', this.libraryId]);
|
this.router.navigate(['library', this.libraryId]);
|
||||||
}
|
}
|
||||||
this.actionInProgress = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbCollapseModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
|
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
|
||||||
import { SafeHtmlPipe } from './safe-html.pipe';
|
import { SafeHtmlPipe } from './safe-html.pipe';
|
||||||
import { RegisterMemberComponent } from '../register-member/register-member.component';
|
import { RegisterMemberComponent } from '../register-member/register-member.component';
|
||||||
@ -37,6 +37,7 @@ import { SentenceCasePipe } from './sentence-case.pipe';
|
|||||||
RouterModule,
|
RouterModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbCollapseModule,
|
NgbCollapseModule,
|
||||||
|
NgbTooltipModule, // RegisterMemberComponent
|
||||||
NgCircleProgressModule.forRoot(),
|
NgCircleProgressModule.forRoot(),
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user