mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
Release Shakeout (#1186)
* Cleaned up some styles on the progress bar in book reader * Fixed up some phone-hidden classes and added titles around the codebase. Stat reporting on first run now takes into account that admin user wont exist. * Fixed manage library page not updating last scan time when a notification event comes in. * Integrated SeriesSort ComicInfo tag (somehow it got missed) * Some minor style changes and no results found for bookmarks on chapter detail modal * Fixed the labels in action bar on book reader so Prev/Next are in same place * Cleaned up some responsive styles around images and reduced custom classes in light of new display classes on collection detail and series detail pages * Fixed an issue with webkit browsers and book reader where the scroll to would fail as the document wasn't fully rendered. A 10ms delay seems to fix the issue. * Cleaned up some code and filtering for collections. Collection detail is missing filtering functionality somehow, disabled the button and will add in future release * Correctly validate and show a message when a user is not an admin or has change password role when going through forget password flow. * Fixed a bug on manage libraries where library last scan didn't work on first scan of a library, due to there being no updated series. * Fixed a rendering issue with text being focused on confirm email page textboxes. Fixed a bug where when deleting a theme that was default, Kavita didn't reset Dark as the default theme. * Cleaned up the naming and styles for side nav active item hover * Fixed event widget to have correct styling on eink and light * Tried to fix a rendering issue on side nav for light themes, but can't figure it out * On light more, ensure switches are green * Fixed a bug where opening a page with a preselected filter, the filter toggle button would require 2 clicks to collapse * Reverted the revert of On Deck. * Improved the upload by url experience by sending a custom fail error to UI when a url returns 401. * When deleting a library, emit a series removed event for each series removed so user's dashboards/screens update. * Fixed an api throwing an error due to text being sent back instead of json. * Fixed a refresh bug with refreshing pending invites after deleting an invite. Ensure we always refresh pending invites even if user cancel's from invite, as they might invite, then hit cancel, where invite is still active. * Fixed a bug where invited users with + in the email would fail due to validation, but UI wouldn't properly inform user.
This commit is contained in:
parent
606ce2295e
commit
78ffb8a8a2
@ -525,6 +525,12 @@ namespace API.Controllers
|
||||
return Ok("An email will be sent to the email if it exists in our database");
|
||||
}
|
||||
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
|
||||
|
||||
if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole))
|
||||
return Unauthorized("You are not permitted to this operation.");
|
||||
|
||||
var emailLink = GenerateEmailLink(await _userManager.GeneratePasswordResetTokenAsync(user), "confirm-reset-password", user.Email);
|
||||
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||
|
@ -197,6 +197,12 @@ namespace API.Controllers
|
||||
_taskScheduler.CleanupChapters(chapterIds);
|
||||
}
|
||||
|
||||
foreach (var seriesId in seriesIds)
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved,
|
||||
MessageFactory.SeriesRemovedEvent(seriesId, string.Empty, libraryId), false);
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
|
||||
MessageFactory.LibraryModifiedEvent(libraryId, "delete"), false);
|
||||
return Ok(true);
|
||||
|
@ -618,6 +618,12 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey));
|
||||
if (!await _downloadService.HasDownloadPermission(user))
|
||||
{
|
||||
return BadRequest("User does not have download permissions");
|
||||
}
|
||||
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
var (bytes, contentType, fileDownloadName) = await _downloadService.GetFirstFileDownload(files);
|
||||
return File(bytes, contentType, fileDownloadName);
|
||||
@ -776,6 +782,7 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"),
|
||||
accLink,
|
||||
CreatePageStreamLink(seriesId, volumeId, chapterId, mangaFile, apiKey)
|
||||
},
|
||||
Content = new FeedEntryContent()
|
||||
@ -785,11 +792,12 @@ public class OpdsController : BaseApiController
|
||||
}
|
||||
};
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey));
|
||||
if (await _downloadService.HasDownloadPermission(user))
|
||||
{
|
||||
entry.Links.Add(accLink);
|
||||
}
|
||||
// We can't not show acc link in the feed, panels wont work like that. We have to block download directly
|
||||
// var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey));
|
||||
// if (await _downloadService.HasDownloadPermission(user))
|
||||
// {
|
||||
// entry.Links.Add(accLink);
|
||||
// }
|
||||
|
||||
|
||||
return entry;
|
||||
|
@ -47,12 +47,24 @@ namespace API.Controllers
|
||||
{
|
||||
var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace("/", "_").Replace(":", "_");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", "");
|
||||
var path = await dto.Url
|
||||
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");
|
||||
try
|
||||
{
|
||||
var path = await dto.Url
|
||||
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");
|
||||
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"Could not download file");
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
|
||||
return BadRequest($"Could not download file");
|
||||
|
||||
return $"coverupload_{dateString}.{format}";
|
||||
return $"coverupload_{dateString}.{format}";
|
||||
}
|
||||
catch (FlurlHttpException ex)
|
||||
{
|
||||
// Unauthorized
|
||||
if (ex.StatusCode == 401)
|
||||
return BadRequest("The server requires authentication to load the url externally");
|
||||
}
|
||||
|
||||
return BadRequest("Unable to download image, please use another url or upload by file");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -14,6 +14,7 @@ namespace API.Data.Metadata
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Series { get; set; } = string.Empty;
|
||||
public string SeriesSort { get; set; } = string.Empty;
|
||||
public string Number { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// The total number of items in the series.
|
||||
|
@ -89,6 +89,7 @@ public class LibraryRepository : ILibraryRepository
|
||||
{
|
||||
var library = await GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders | LibraryIncludes.Series);
|
||||
_context.Library.Remove(library);
|
||||
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
|
@ -624,7 +624,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
LastReadingProgress = _context.AppUserProgresses
|
||||
.Where(p => p.Id == progress.Id && p.AppUserId == userId)
|
||||
.Max(p => p.LastModified),
|
||||
// This is only taking into account chapters that have progress on them, not all chapters in said series
|
||||
// BUG: 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 => allChapters.Contains(c.Id)).Max(c => c.Created)
|
||||
});
|
||||
|
@ -15,6 +15,12 @@ namespace API.Extensions
|
||||
{
|
||||
public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
options.User.AllowedUserNameCharacters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+/";
|
||||
});
|
||||
|
||||
services.AddIdentityCore<AppUser>(opt =>
|
||||
{
|
||||
opt.Password.RequireNonAlphanumeric = false;
|
||||
|
@ -72,9 +72,8 @@ namespace API
|
||||
}
|
||||
|
||||
await context.Database.MigrateAsync();
|
||||
var roleManager = services.GetRequiredService<RoleManager<AppRole>>();
|
||||
|
||||
await Seed.SeedRoles(roleManager);
|
||||
await Seed.SeedRoles(services.GetRequiredService<RoleManager<AppRole>>());
|
||||
await Seed.SeedSettings(context, directoryService);
|
||||
await Seed.SeedThemes(context);
|
||||
await Seed.SeedUserApiKeys(context);
|
||||
@ -110,7 +109,7 @@ namespace API
|
||||
(await context.ServerSetting.SingleOrDefaultAsync(s =>
|
||||
s.Key == ServerSettingKey.InstallVersion))?.Value;
|
||||
}
|
||||
catch
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
@ -164,7 +164,7 @@ public class SeriesService : ISeriesService
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.ScanSeries,
|
||||
MessageFactory.ScanSeriesEvent(series.Id, series.Name), false);
|
||||
MessageFactory.ScanSeriesEvent(series.LibraryId, series.Id, series.Name), false);
|
||||
|
||||
await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
|
||||
|
||||
|
@ -15,7 +15,7 @@ public interface ITaskScheduler
|
||||
Task ScheduleTasks();
|
||||
Task ScheduleStatsTasks();
|
||||
void ScheduleUpdaterTasks();
|
||||
void ScanLibrary(int libraryId, bool forceUpdate = false);
|
||||
void ScanLibrary(int libraryId);
|
||||
void CleanupChapters(int[] chapterIds);
|
||||
void RefreshMetadata(int libraryId, bool forceUpdate = true);
|
||||
void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false);
|
||||
@ -146,7 +146,7 @@ public class TaskScheduler : ITaskScheduler
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void ScanLibrary(int libraryId, bool forceUpdate = false)
|
||||
public void ScanLibrary(int libraryId)
|
||||
{
|
||||
_logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId);
|
||||
BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId));
|
||||
|
@ -117,10 +117,15 @@ namespace API.Services.Tasks.Scanner
|
||||
}
|
||||
|
||||
// Patch is SeriesSort from ComicInfo
|
||||
if (info.ComicInfo != null && !string.IsNullOrEmpty(info.ComicInfo.TitleSort))
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.TitleSort))
|
||||
{
|
||||
info.SeriesSort = info.ComicInfo.TitleSort;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.SeriesSort))
|
||||
{
|
||||
info.SeriesSort = info.ComicInfo.SeriesSort;
|
||||
}
|
||||
}
|
||||
|
||||
TrackSeries(info);
|
||||
|
@ -162,7 +162,7 @@ public class ScannerService : IScannerService
|
||||
}
|
||||
// Tell UI that this series is done
|
||||
await _eventHub.SendMessageAsync(MessageFactory.ScanSeries,
|
||||
MessageFactory.ScanSeriesEvent(seriesId, series.Name));
|
||||
MessageFactory.ScanSeriesEvent(libraryId, seriesId, series.Name));
|
||||
await CleanupDbEntities();
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, series.Id, false));
|
||||
@ -428,7 +428,7 @@ public class ScannerService : IScannerService
|
||||
foreach (var series in librarySeries)
|
||||
{
|
||||
// This is something more like, the series has finished updating in the backend. It may or may not have been modified.
|
||||
await _eventHub.SendMessageAsync(MessageFactory.ScanSeries, MessageFactory.ScanSeriesEvent(series.Id, series.Name));
|
||||
await _eventHub.SendMessageAsync(MessageFactory.ScanSeries, MessageFactory.ScanSeriesEvent(library.Id, series.Id, series.Name));
|
||||
}
|
||||
}
|
||||
|
||||
@ -523,7 +523,7 @@ public class ScannerService : IScannerService
|
||||
series.Format = parsedInfos[0].Format;
|
||||
}
|
||||
series.OriginalName ??= parsedInfos[0].Series;
|
||||
if (!series.SortNameLocked) series.SortName ??= parsedInfos[0].SeriesSort;
|
||||
if (!series.SortNameLocked) series.SortName = parsedInfos[0].SeriesSort;
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name));
|
||||
|
||||
|
@ -100,6 +100,21 @@ public class SiteThemeService : ISiteThemeService
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
// if there are no default themes, reselect Dark as default
|
||||
var postSaveThemes = (await _unitOfWork.SiteThemeRepository.GetThemes()).ToList();
|
||||
if (!postSaveThemes.Any(t => t.IsDefault))
|
||||
{
|
||||
var defaultThemeName = Seed.DefaultThemes.Single(t => t.IsDefault).NormalizedName;
|
||||
var theme = postSaveThemes.SingleOrDefault(t => t.NormalizedName == defaultThemeName);
|
||||
if (theme != null)
|
||||
{
|
||||
theme.IsDefault = true;
|
||||
_unitOfWork.SiteThemeRepository.Update(theme);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.SiteThemeProgressEvent("", "", ProgressEventType.Ended));
|
||||
|
||||
|
@ -102,11 +102,6 @@ public class StatsService : IStatsService
|
||||
var installId = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId);
|
||||
var installVersion = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||
|
||||
var firstAdminUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First();
|
||||
var firstAdminUserPref = (await _unitOfWork.UserRepository.GetPreferencesAsync(firstAdminUser.UserName));
|
||||
|
||||
var activeTheme = firstAdminUserPref.Theme ?? Seed.DefaultThemes.First(t => t.IsDefault);
|
||||
|
||||
var serverInfo = new ServerInfoDto
|
||||
{
|
||||
InstallId = installId.Value,
|
||||
@ -117,15 +112,24 @@ public class StatsService : IStatsService
|
||||
NumOfCores = Math.Max(Environment.ProcessorCount, 1),
|
||||
HasBookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync()).Any(),
|
||||
NumberOfLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Count(),
|
||||
ActiveSiteTheme = activeTheme.Name,
|
||||
NumberOfCollections = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count(),
|
||||
NumberOfReadingLists = await _unitOfWork.ReadingListRepository.Count(),
|
||||
OPDSEnabled = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds,
|
||||
NumberOfUsers = (await _unitOfWork.UserRepository.GetAllUsers()).Count(),
|
||||
TotalFiles = await _unitOfWork.LibraryRepository.GetTotalFiles(),
|
||||
MangaReaderMode = firstAdminUserPref.ReaderMode
|
||||
};
|
||||
|
||||
var firstAdminUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).FirstOrDefault();
|
||||
|
||||
if (firstAdminUser != null)
|
||||
{
|
||||
var firstAdminUserPref = (await _unitOfWork.UserRepository.GetPreferencesAsync(firstAdminUser.UserName));
|
||||
var activeTheme = firstAdminUserPref.Theme ?? Seed.DefaultThemes.First(t => t.IsDefault);
|
||||
|
||||
serverInfo.ActiveSiteTheme = activeTheme.Name;
|
||||
serverInfo.MangaReaderMode = firstAdminUserPref.ReaderMode;
|
||||
}
|
||||
|
||||
return serverInfo;
|
||||
}
|
||||
}
|
||||
|
@ -80,13 +80,15 @@ namespace API.SignalR
|
||||
public const string LibraryModified = "LibraryModified";
|
||||
|
||||
|
||||
public static SignalRMessage ScanSeriesEvent(int seriesId, string seriesName)
|
||||
public static SignalRMessage ScanSeriesEvent(int libraryId, int seriesId, string seriesName)
|
||||
{
|
||||
return new SignalRMessage()
|
||||
{
|
||||
Name = ScanSeries,
|
||||
EventType = ProgressEventType.Single,
|
||||
Body = new
|
||||
{
|
||||
LibraryId = libraryId,
|
||||
SeriesId = seriesId,
|
||||
SeriesName = seriesName
|
||||
}
|
||||
|
@ -120,6 +120,7 @@ namespace API
|
||||
ForwardedHeaders.All;
|
||||
});
|
||||
|
||||
|
||||
services.AddHangfire(configuration => configuration
|
||||
.UseSimpleAssemblyNameTypeSerializer()
|
||||
.UseRecommendedSerializerSettings()
|
||||
@ -149,7 +150,6 @@ namespace API
|
||||
// Apply all migrations on startup
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
|
||||
var userManager = serviceProvider.GetRequiredService<UserManager<AppUser>>();
|
||||
var context = serviceProvider.GetRequiredService<DataContext>();
|
||||
|
||||
await MigrateBookmarks.Migrate(directoryService, unitOfWork,
|
||||
logger, cacheService);
|
||||
@ -161,6 +161,7 @@ namespace API
|
||||
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||
installVersion.Value = BuildInfo.Version.ToString();
|
||||
unitOfWork.SettingsRepository.Update(installVersion);
|
||||
|
||||
await unitOfWork.CommitAsync();
|
||||
}).GetAwaiter()
|
||||
.GetResult();
|
||||
|
@ -12,7 +12,7 @@
|
||||
<label for="rating" class="form-label">Rating</label>
|
||||
<div>
|
||||
<ngb-rating style="margin-top: 2px; font-size: 1.5rem;" formControlName="rating"></ngb-rating>
|
||||
<button class="btn btn-icon ms-2" (click)="clearRating()"><i aria-hidden="true" class="fa fa-ban"></i><span class="phone-hidden"> Clear</span></button>
|
||||
<button class="btn btn-icon ms-2" (click)="clearRating()" title="clear"><i aria-hidden="true" class="fa fa-ban"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
export interface ScanSeriesEvent {
|
||||
libraryId: number;
|
||||
seriesId: number;
|
||||
seriesName: string;
|
||||
}
|
@ -47,7 +47,7 @@ export class ReadingListService {
|
||||
}
|
||||
|
||||
updateByMultiple(readingListId: number, seriesId: number, volumeIds: Array<number>, chapterIds?: Array<number>) {
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-multiple', {readingListId, seriesId, volumeIds, chapterIds});
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-multiple', {readingListId, seriesId, volumeIds, chapterIds}, { responseType: 'text' as 'json' });
|
||||
}
|
||||
|
||||
updateByMultipleSeries(readingListId: number, seriesIds: Array<number>) {
|
||||
|
@ -155,7 +155,6 @@ export class SeriesService {
|
||||
}
|
||||
|
||||
scan(libraryId: number, seriesId: number) {
|
||||
// TODO: Pipe and put a toaster up: this.toastr.info('Scan queued for ' + series.name);
|
||||
return this.httpClient.post(this.baseUrl + 'series/scan', {libraryId: libraryId, seriesId: seriesId});
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-8"><h3>Libraries</h3></div>
|
||||
<div class="col-4"><button class="btn btn-primary float-end" (click)="addLibrary()"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> Add Library</span></button></div>
|
||||
<div class="col-4"><button class="btn btn-primary float-end" (click)="addLibrary()" title="Add Library"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> Add Library</span></button></div>
|
||||
</div>
|
||||
<ul class="list-group" *ngIf="!createLibraryToggle; else createLibrary">
|
||||
<li *ngFor="let library of libraries; let idx = index; trackby: trackbyLibrary" class="list-group-item no-hover">
|
||||
<div>
|
||||
<h4>
|
||||
<span id="library-name--{{idx}}"><a [routerLink]="'/library/' + library.id">{{library.name}}</a></span>
|
||||
<div class="spinner-border text-primary" style="width: 1.5rem; height: 1.5rem;" role="status" *ngIf="scanInProgress.hasOwnProperty(library.id) && scanInProgress[library.id].progress" title="Scan in progress. Started at {{scanInProgress[library.id].timestamp | date: 'short'}}">
|
||||
<!-- <div class="spinner-border text-primary" style="width: 1.5rem; height: 1.5rem;" role="status" *ngIf="scanInProgress.hasOwnProperty(library.id) && scanInProgress[library.id].progress" title="Scan in progress. Started at {{scanInProgress[library.id].timestamp | date: 'short'}}">
|
||||
<span class="visually-hidden">Scan for {{library.name}} in progress</span>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="float-end">
|
||||
<button class="btn btn-secondary me-2 btn-sm" (click)="scanLibrary(library)" placement="top" ngbTooltip="Scan Library" attr.aria-label="Scan Library"><i class="fa fa-sync-alt" title="Scan"></i></button>
|
||||
<button class="btn btn-danger me-2 btn-sm" [disabled]="deletionInProgress" (click)="deleteLibrary(library)"><i class="fa fa-trash" placement="top" ngbTooltip="Delete Library" attr.aria-label="Delete {{library.name | sentenceCase}}"></i></button>
|
||||
|
@ -2,10 +2,10 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Subject } from 'rxjs';
|
||||
import { take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, filter, take, takeLast, takeUntil } from 'rxjs/operators';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { NotificationProgressEvent } from 'src/app/_models/events/notification-progress-event';
|
||||
import { ProgressEvent } from 'src/app/_models/events/progress-event';
|
||||
import { ScanSeriesEvent } from 'src/app/_models/events/scan-series-event';
|
||||
import { Library, LibraryType } from 'src/app/_models/library';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service';
|
||||
@ -25,7 +25,6 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
* If a deletion is in progress for a library
|
||||
*/
|
||||
deletionInProgress: boolean = false;
|
||||
scanInProgress: {[key: number]: {progress: boolean, timestamp?: string}} = {};
|
||||
libraryTrackBy = (index: number, item: Library) => `${item.name}_${item.lastScanned}_${item.type}_${item.folders.length}`;
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
@ -38,29 +37,29 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
this.getLibraries();
|
||||
|
||||
// when a progress event comes in, show it on the UI next to library
|
||||
this.hubService.messages$.pipe(takeUntil(this.onDestroy), takeWhile(event => event.event === EVENTS.NotificationProgress))
|
||||
.subscribe((event: Message<NotificationProgressEvent>) => {
|
||||
if (event.event !== EVENTS.NotificationProgress && (event.payload as NotificationProgressEvent).name === EVENTS.ScanSeries) return;
|
||||
this.hubService.messages$.pipe(takeUntil(this.onDestroy),
|
||||
filter(event => event.event === EVENTS.ScanSeries || event.event === EVENTS.NotificationProgress),
|
||||
distinctUntilChanged((prev: Message<ScanSeriesEvent | NotificationProgressEvent>, curr: Message<ScanSeriesEvent | NotificationProgressEvent>) =>
|
||||
this.hasMessageChanged(prev, curr)))
|
||||
.subscribe((event: Message<ScanSeriesEvent | NotificationProgressEvent>) => {
|
||||
console.log('scan event: ', event);
|
||||
|
||||
let libId = 0;
|
||||
if (event.event === EVENTS.ScanSeries) {
|
||||
libId = (event.payload as ScanSeriesEvent).libraryId;
|
||||
} else {
|
||||
if ((event.payload as NotificationProgressEvent).body.hasOwnProperty('libraryId')) {
|
||||
libId = (event.payload as NotificationProgressEvent).body.libraryId;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('scan event: ', event.payload);
|
||||
// TODO: Refactor this to use EventyType on NotificationProgress interface rather than float comparison
|
||||
|
||||
const scanEvent = event.payload.body as ProgressEvent;
|
||||
this.scanInProgress[scanEvent.libraryId] = {progress: scanEvent.progress !== 1};
|
||||
if (scanEvent.progress === 0) {
|
||||
this.scanInProgress[scanEvent.libraryId].timestamp = scanEvent.eventTime;
|
||||
}
|
||||
|
||||
if (this.scanInProgress[scanEvent.libraryId].progress === false && (scanEvent.progress === 1 || event.payload.eventType === 'ended')) {
|
||||
this.libraryService.getLibraries().pipe(take(1)).subscribe(libraries => {
|
||||
const newLibrary = libraries.find(lib => lib.id === scanEvent.libraryId);
|
||||
const existingLibrary = this.libraries.find(lib => lib.id === scanEvent.libraryId);
|
||||
const newLibrary = libraries.find(lib => lib.id === libId);
|
||||
const existingLibrary = this.libraries.find(lib => lib.id === libId);
|
||||
if (existingLibrary !== undefined) {
|
||||
existingLibrary.lastScanned = newLibrary?.lastScanned || existingLibrary.lastScanned;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@ -69,6 +68,17 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
hasMessageChanged(prev: Message<ScanSeriesEvent | NotificationProgressEvent>, curr: Message<ScanSeriesEvent | NotificationProgressEvent>) {
|
||||
if (curr.event !== prev.event) return true;
|
||||
if (curr.event === EVENTS.ScanSeries) {
|
||||
return (prev.payload as ScanSeriesEvent).libraryId === (curr.payload as ScanSeriesEvent).libraryId;
|
||||
}
|
||||
if (curr.event === EVENTS.NotificationProgress) {
|
||||
return (prev.payload as NotificationProgressEvent).eventType != (curr.payload as NotificationProgressEvent).eventType;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getLibraries() {
|
||||
this.loading = true;
|
||||
this.libraryService.getLibraries().pipe(take(1)).subscribe(libraries => {
|
||||
|
@ -74,6 +74,7 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
loadPendingInvites() {
|
||||
this.pendingInvites = [];
|
||||
this.memberService.getPendingInvites().subscribe(members => {
|
||||
this.pendingInvites = members;
|
||||
// Show logged in user at the top of the list
|
||||
@ -116,9 +117,7 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
||||
inviteUser() {
|
||||
const modalRef = this.modalService.open(InviteUserComponent, {size: 'lg'});
|
||||
modalRef.closed.subscribe((successful: boolean) => {
|
||||
if (successful) {
|
||||
this.loadPendingInvites();
|
||||
}
|
||||
this.loadPendingInvites();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)">
|
||||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)">
|
||||
<h2 title>
|
||||
<app-card-actionables [actions]="actions"></app-card-actionables>
|
||||
All Series
|
||||
|
@ -41,7 +41,7 @@
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label id="readingdirection" class="form-label">Reading Direction</label>
|
||||
<button (click)="toggleReadingDirection()" class="btn btn-icon" aria-labelledby="readingdirection" title="{{readingDirection === 0 ? 'Left to Right' : 'Right to Left'}}"><i class="fa {{readingDirection === 0 ? 'fa-arrow-right' : 'fa-arrow-left'}} " aria-hidden="true"></i><span class="phone-hidden"> {{readingDirection === 0 ? 'Left to Right' : 'Right to Left'}}</span></button>
|
||||
<button (click)="toggleReadingDirection()" class="btn btn-icon" aria-labelledby="readingdirection" title="{{readingDirection === 0 ? 'Left to Right' : 'Right to Left'}}"><i class="fa {{readingDirection === 0 ? 'fa-arrow-right' : 'fa-arrow-left'}} " aria-hidden="true"></i><span class="d-none d-sm-block"> {{readingDirection === 0 ? 'Left to Right' : 'Right to Left'}}</span></button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label id="darkmode" class="form-label">Dark Mode</label>
|
||||
@ -52,7 +52,7 @@
|
||||
<label id="tap-pagination" class="form-label">Tap Pagination <i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="tapPaginationTooltip" role="button" tabindex="0" aria-describedby="tap-pagination-help"></i></label>
|
||||
<ng-template #tapPaginationTooltip>The ability to click the sides of the page to page left and right</ng-template>
|
||||
<span class="visually-hidden" id="tap-pagination-help">The ability to click the sides of the page to page left and right</span>
|
||||
<button (click)="toggleClickToPaginate()" class="btn btn-icon" aria-labelledby="tap-pagination"><i class="fa fa-arrows-alt-h {{clickToPaginate ? 'icon-primary-color' : ''}}" aria-hidden="true"></i><span *ngIf="darkMode"> {{clickToPaginate ? 'On' : 'Off'}}</span></button>
|
||||
<button (click)="toggleClickToPaginate()" class="btn btn-icon" aria-labelledby="tap-pagination"><i class="fa fa-arrows-alt-h {{clickToPaginate ? 'icon-primary-color' : ''}}" aria-hidden="true"></i> {{clickToPaginate ? 'On' : 'Off'}}</button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label id="fullscreen" class="form-label">Fullscreen <i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="fullscreenTooltip" role="button" tabindex="0" aria-describedby="fullscreen-help"></i></label>
|
||||
@ -70,13 +70,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
||||
<div class="col-1 page-stub">{{pageNum}}</div>
|
||||
<div class="col-8" style="margin-top: 15px;padding-right:10px">
|
||||
<button class="btn btn-small btn-icon col-1" style="padding-left: 0px" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
||||
<div class="col-1 page-stub ps-1">{{pageNum}}</div>
|
||||
<div class="col-8 pe-1" style="margin-top: 15px">
|
||||
<ngb-progressbar style="cursor: pointer" title="Go to page" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
|
||||
</div>
|
||||
<div class="col-1 btn-icon page-stub" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div>
|
||||
<button class="btn btn-small btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
||||
<div class="col-1 btn-icon page-stub pe-1" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div>
|
||||
<button class="btn btn-small btn-icon col-1" style="padding-right: 0px; padding-left: 0px" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="table-of-contents">
|
||||
<h3>Table of Contents</h3>
|
||||
@ -127,11 +127,11 @@
|
||||
[disabled]="IsPrevDisabled"
|
||||
title="{{readingDirection === ReadingDirection.LeftToRight ? 'Previous' : 'Next'}} Page">
|
||||
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? IsPrevChapter : IsNextChapter) ? 'fa-angle-double-left' : 'fa-angle-left'}}" aria-hidden="true"></i>
|
||||
<span class="phone-hidden"> {{readingDirection === ReadingDirection.LeftToRight ? 'Previous' : 'Next'}}</span>
|
||||
<span class="d-none d-sm-block"> {{readingDirection === ReadingDirection.LeftToRight ? 'Previous' : 'Next'}}</span>
|
||||
</button>
|
||||
<button *ngIf="!this.adhocPageHistory.isEmpty()" class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="goBack()" title="Go Back"><i class="fa fa-reply" aria-hidden="true"></i><span class="phone-hidden"> Go Back</span></button>
|
||||
<button class="btn btn-secondary col-2 col-xs-1" (click)="toggleDrawer()"><i class="fa fa-bars" aria-hidden="true"></i><span class="phone-hidden"> Settings</span></button>
|
||||
<div class="book-title col-2 phone-hidden">
|
||||
<button *ngIf="!this.adhocPageHistory.isEmpty()" class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="goBack()" title="Go Back"><i class="fa fa-reply" aria-hidden="true"></i><span class="d-none d-sm-block"> Go Back</span></button>
|
||||
<button class="btn btn-secondary col-2 col-xs-1" (click)="toggleDrawer()"><i class="fa fa-bars" aria-hidden="true"></i><span class="d-none d-sm-block">Settings</span></button>
|
||||
<div class="book-title col-2 d-none d-sm-block">
|
||||
<ng-container *ngIf="isLoading; else showTitle">
|
||||
<div class="spinner-border spinner-border-sm text-primary" style="border-radius: 50%;" role="status">
|
||||
<span class="visually-hidden">Loading book...</span>
|
||||
@ -142,12 +142,12 @@
|
||||
<span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" aria-label="Incognito mode is on. Toggle to turn off.">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">Incognito Mode</span>)</span>
|
||||
</ng-template>
|
||||
</div>
|
||||
<button class="btn btn-secondary col-2 col-xs-1" (click)="closeReader()"><i class="fa fa-times-circle" aria-hidden="true"></i><span class="phone-hidden"> Close</span></button>
|
||||
<button class="btn btn-secondary col-2 col-xs-1" (click)="closeReader()"><i class="fa fa-times-circle" aria-hidden="true"></i><span class="d-none d-sm-block"> Close</span></button>
|
||||
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1"
|
||||
[disabled]="IsNextDisabled"
|
||||
(click)="nextPage()" title="{{readingDirection === ReadingDirection.LeftToRight ? 'Next' : 'Previous'}} Page">
|
||||
<span class="phone-hidden">{{readingDirection === ReadingDirection.LeftToRight ? 'Next' : 'Previous'}} </span>
|
||||
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? IsNextChapter : IsPrevChapter) ? 'fa-angle-double-right' : 'fa-angle-right'}}" aria-hidden="true"></i>
|
||||
<span class="d-none d-sm-block">{{readingDirection === ReadingDirection.LeftToRight ? 'Next' : 'Previous'}} </span>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -1013,8 +1013,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (element === null) return;
|
||||
|
||||
this.scrollService.scrollTo(element.getBoundingClientRect().top + window.pageYOffset + TOP_OFFSET, this.reader.nativeElement);
|
||||
const fromTopOffset = element.getBoundingClientRect().top + window.pageYOffset + TOP_OFFSET;
|
||||
// We need to use a delay as webkit browsers (aka apple devices) don't always have the document rendered by this point
|
||||
setTimeout(() => this.scrollService.scrollTo(fromTopOffset, this.reader.nativeElement), 10);
|
||||
}
|
||||
|
||||
toggleClickToPaginate() {
|
||||
|
@ -96,6 +96,9 @@
|
||||
<ng-container *ngFor="let bookmark of bookmarks; let idx = index">
|
||||
<app-bookmark [bookmark]="bookmark" class="col-auto" (bookmarkRemoved)="removeBookmark(bookmark, idx)"></app-bookmark>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="bookmarks.length === 0">
|
||||
No bookmarks yet
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
@ -8,7 +8,7 @@
|
||||
No metadata available
|
||||
</span>
|
||||
<div class="row g-0">
|
||||
<div class="col-auto" *ngIf="chapter.writers && chapter.writers.length > 0">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.writers && chapter.writers.length > 0">
|
||||
<h6>Writers</h6>
|
||||
<app-badge-expander [items]="chapter.writers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
@ -17,7 +17,7 @@
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto" *ngIf="chapter.coverArtists && chapter.coverArtists.length > 0">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.coverArtists && chapter.coverArtists.length > 0">
|
||||
<h6>Cover Artists</h6>
|
||||
<app-badge-expander [items]="chapter.coverArtists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
@ -26,7 +26,7 @@
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto" *ngIf="chapter.pencillers && chapter.pencillers.length > 0">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.pencillers && chapter.pencillers.length > 0">
|
||||
<h6>Pencillers</h6>
|
||||
<app-badge-expander [items]="chapter.pencillers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
@ -35,7 +35,7 @@
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto" *ngIf="chapter.inkers && chapter.inkers.length > 0">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.inkers && chapter.inkers.length > 0">
|
||||
<h6>Inkers</h6>
|
||||
<app-badge-expander [items]="chapter.inkers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
@ -44,7 +44,7 @@
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto" *ngIf="chapter.colorists && chapter.colorists.length > 0">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.colorists && chapter.colorists.length > 0">
|
||||
<h6>Colorists</h6>
|
||||
<app-badge-expander [items]="chapter.colorists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
@ -54,7 +54,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-auto" *ngIf="chapter.letterers && chapter.letterers.length > 0">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.letterers && chapter.letterers.length > 0">
|
||||
<h6>Letterers</h6>
|
||||
<app-badge-expander [items]="chapter.letterers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
@ -63,7 +63,7 @@
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto" *ngIf="chapter.editors && chapter.editors.length > 0">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.editors && chapter.editors.length > 0">
|
||||
<h6>Editors</h6>
|
||||
<app-badge-expander [items]="chapter.editors">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
@ -72,7 +72,7 @@
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto" *ngIf="chapter.publishers && chapter.publishers.length > 0">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.publishers && chapter.publishers.length > 0">
|
||||
<h6>Publishers</h6>
|
||||
<app-badge-expander [items]="chapter.publishers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
@ -81,7 +81,7 @@
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto" *ngIf="chapter.characters && chapter.characters.length > 0">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.characters && chapter.characters.length > 0">
|
||||
<h6>Characters</h6>
|
||||
<app-badge-expander [items]="chapter.characters">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
@ -89,7 +89,7 @@
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
<div class="col-auto" *ngIf="chapter.translators && chapter.translators.length > 0">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.translators && chapter.translators.length > 0">
|
||||
<h6>Translators</h6>
|
||||
<app-badge-expander [items]="chapter.translators">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
|
@ -1,17 +1,17 @@
|
||||
<app-side-nav-companion-bar *ngIf="series !== undefined">
|
||||
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasFilter]="false" (filterOpen)="filterOpen.emit($event)">
|
||||
<ng-container title>
|
||||
<h2 style="margin-bottom: 0px">
|
||||
<h2 style="margin-bottom: 0px" *ngIf="collectionTag !== undefined">
|
||||
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="collectionTagActions" [labelBy]="collectionTag.title" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
{{collectionTag.title}}
|
||||
{{collectionTag.title}}<span *ngIf="collectionTag.promoted"> (<i aria-hidden="true" class="fa fa-angle-double-up"></i>)</span>
|
||||
</h2>
|
||||
</ng-container>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid" *ngIf="collectionTag !== undefined" style="padding-top: 10px">
|
||||
<div class="container-fluid pt-2" *ngIf="collectionTag !== undefined">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6 poster">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
|
||||
<app-image maxWidth="481px" [imageUrl]="tagImage"></app-image>
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6">
|
||||
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
|
||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</div>
|
||||
@ -24,6 +24,7 @@
|
||||
[items]="series"
|
||||
[pagination]="seriesPagination"
|
||||
[filterSettings]="filterSettings"
|
||||
[filterOpen]="filterOpen"
|
||||
(pageChange)="onPageChange($event)"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, EventEmitter, HostListener, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
@ -43,6 +43,8 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
summary: string = '';
|
||||
|
||||
actionInProgress: boolean = false;
|
||||
|
||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
|
||||
private onDestory: Subject<void> = new Subject<void>();
|
||||
@ -102,6 +104,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
const tagId = parseInt(routeId, 10);
|
||||
this.seriesPagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
|
||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.utilityService.filterPresetsFromUrl(this.route.snapshot, this.seriesService.createSeriesFilter());
|
||||
this.filterSettings.presets.collectionTags = [tagId];
|
||||
@ -161,16 +164,22 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
onPageChange(pagination: Pagination) {
|
||||
this.router.navigate(['collections', this.collectionTag.id], {replaceUrl: true, queryParamsHandling: 'merge', queryParams: {page: this.seriesPagination.currentPage} });
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
loadPage() {
|
||||
const page = this.route.snapshot.queryParamMap.get('page');
|
||||
const page = this.getPage();
|
||||
if (page != null) {
|
||||
if (this.seriesPagination === undefined || this.seriesPagination === null) {
|
||||
this.seriesPagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
}
|
||||
this.seriesPagination.currentPage = parseInt(page, 10);
|
||||
}
|
||||
|
||||
// The filter is out of sync with the presets from typeaheads on first load but syncs afterwards
|
||||
if (this.filter == undefined) {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
this.filter.collectionTags.push(this.collectionTag.id);
|
||||
}
|
||||
|
||||
// TODO: Add ability to filter series for a collection
|
||||
// Reload page after a series is updated or first load
|
||||
this.seriesService.getSeriesForTag(this.collectionTag.id, this.seriesPagination?.currentPage, this.seriesPagination?.itemsPerPage).subscribe(tags => {
|
||||
this.series = tags.result;
|
||||
@ -180,9 +189,10 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
updateFilter(data: FilterEvent) {
|
||||
this.filter = data.filter;
|
||||
if (this.seriesPagination !== undefined && this.seriesPagination !== null && !data.isFirst) {
|
||||
updateFilter(event: FilterEvent) {
|
||||
this.filter = event.filter;
|
||||
const page = this.getPage();
|
||||
if (page === undefined || page === null || !event.isFirst) {
|
||||
this.seriesPagination.currentPage = 1;
|
||||
this.onPageChange(this.seriesPagination);
|
||||
} else {
|
||||
@ -190,6 +200,11 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
getPage() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('page');
|
||||
}
|
||||
|
||||
handleCollectionActionCallback(action: Action, collectionTag: CollectionTag) {
|
||||
switch (action) {
|
||||
case(Action.Edit):
|
||||
|
@ -1,14 +1,12 @@
|
||||
// NOTE: I'm leaving this not fully customized because I'm planning to rewrite the whole design in v0.5.2/3
|
||||
// These are customizations for events nav
|
||||
.dark-menu {
|
||||
background-color: var(--navbar-bg-color);
|
||||
border-color: rgba(1, 4, 9, 0.5);
|
||||
border-color: var(--event-widget-border-color); // rgba(1, 4, 9, 0.5);
|
||||
}
|
||||
|
||||
.dark-menu-item {
|
||||
color: var(--body-text-color);
|
||||
background-color: rgb(1, 4, 9);
|
||||
border-color: rgba(53, 53, 53, 0.5);
|
||||
color: var(--event-widget-text-color);
|
||||
background-color: var(--event-widget-item-bg-color); // rgb(1, 4, 9)
|
||||
border-color: var(--event-widget-item-border-color); // rgba(53, 53, 53, 0.5)
|
||||
}
|
||||
|
||||
// Popovers need to be their own component
|
||||
|
@ -1,4 +1,4 @@
|
||||
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)">
|
||||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)">
|
||||
<h2 title>
|
||||
<app-card-actionables [actions]="actions"></app-card-actionables>
|
||||
{{libraryName}}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)">
|
||||
<app-side-nav-companion-bar [hasFilter]="true" [filterOpenByDefault]="filterSettings.openByDefault" (filterOpen)="filterOpen.emit($event)">
|
||||
<h2 title>
|
||||
Recently Added
|
||||
</h2>
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
<div class="mb-3" style="width:100%">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input class="form-control" type="email" id="email" formControlName="email" required>
|
||||
<input class="form-control" type="email" id="email" formControlName="email" required readonly>
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||
<div *ngIf="registerForm.get('email')?.errors?.required">
|
||||
This field is required
|
||||
|
@ -1,4 +1,4 @@
|
||||
input {
|
||||
background-color: #fff !important;
|
||||
color: black;
|
||||
color: black !important;
|
||||
}
|
@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ThemeService } from 'src/app/theme.service';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { NavService } from 'src/app/_services/nav.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirm-email',
|
||||
@ -29,8 +30,9 @@ export class ConfirmEmailComponent implements OnInit {
|
||||
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
||||
private toastr: ToastrService, private themeService: ThemeService) {
|
||||
this.themeService.setTheme(this.themeService.defaultTheme);
|
||||
private toastr: ToastrService, private themeService: ThemeService, private navService: NavService) {
|
||||
this.navService.hideSideNav();
|
||||
this.themeService.setTheme(this.themeService.defaultTheme);
|
||||
const token = this.route.snapshot.queryParamMap.get('token');
|
||||
const email = this.route.snapshot.queryParamMap.get('email');
|
||||
if (token == undefined || token === '' || token === null) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
input {
|
||||
background-color: #fff !important;
|
||||
color: black;
|
||||
}
|
||||
color: black !important;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { MemberService } from 'src/app/_services/member.service';
|
||||
import { NavService } from 'src/app/_services/nav.service';
|
||||
|
||||
/**
|
||||
* This is exclusivly used to register the first user on the server and nothing else
|
||||
@ -22,10 +23,12 @@ export class RegisterComponent implements OnInit {
|
||||
password: new FormControl('', [Validators.required, Validators.maxLength(32), Validators.minLength(6)]),
|
||||
});
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, private toastr: ToastrService, private memberService: MemberService) {
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
||||
private toastr: ToastrService, private memberService: MemberService) {
|
||||
this.memberService.adminExists().pipe(take(1)).subscribe(adminExists => {
|
||||
if (adminExists) {
|
||||
this.router.navigateByUrl('login');
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ export class ResetPasswordComponent implements OnInit {
|
||||
this.accountService.requestResetPasswordEmail(model).subscribe((resp: string) => {
|
||||
this.toastr.info(resp);
|
||||
this.router.navigateByUrl('login');
|
||||
}, err => {
|
||||
this.toastr.error(err.error);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -9,10 +9,10 @@
|
||||
<h6 style="margin-left:40px;" title="Localized Name">{{series?.localizedName}}</h6>
|
||||
</ng-container>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid" *ngIf="series !== undefined" style="padding-top: 10px">
|
||||
<div class="container-fluid pt-2" *ngIf="series !== undefined">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6">
|
||||
<app-image class="poster" maxWidth="300px" [imageUrl]="seriesImage"></app-image>
|
||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
|
||||
<app-image maxWidth="300px" [imageUrl]="seriesImage"></app-image>
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
|
||||
<div class="row g-0">
|
||||
@ -21,7 +21,7 @@
|
||||
<span>
|
||||
<i class="fa {{showBook ? 'fa-book-open' : 'fa-book'}}"></i>
|
||||
</span>
|
||||
<span class="read-btn--text"> {{(hasReadingProgress) ? 'Continue' : 'Read'}}</span>
|
||||
<span class="d-none d-sm-inline-block"> {{(hasReadingProgress) ? 'Continue' : 'Read'}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto ms-2" *ngIf="isAdmin">
|
||||
@ -31,7 +31,7 @@
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto ms-2">
|
||||
<div class="col-auto ms-2 d-none d-sm-block">
|
||||
<div class="card-actions">
|
||||
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-h" btnClass="btn-secondary"></app-card-actionables>
|
||||
</div>
|
||||
|
@ -5,21 +5,4 @@
|
||||
.rating-star {
|
||||
margin-top: 2px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.poster {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
@media(max-width: var(--grid-breakpoints-sm)) {
|
||||
.poster {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: var(--grid-breakpoints-sm)) {
|
||||
.read-btn--text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,11 @@ export class SideNavCompanionBarComponent implements OnInit {
|
||||
*/
|
||||
@Input() hasFilter: boolean = false;
|
||||
|
||||
/**
|
||||
* Is the input open by default
|
||||
*/
|
||||
@Input() filterOpenByDefault: boolean = false;
|
||||
|
||||
/**
|
||||
* Should be passed through from Filter component.
|
||||
*/
|
||||
@ -27,14 +32,10 @@ export class SideNavCompanionBarComponent implements OnInit {
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
goBack() {
|
||||
|
||||
this.isFilterOpen = this.filterOpenByDefault;
|
||||
}
|
||||
|
||||
toggleFilter() {
|
||||
//collapse.toggle()
|
||||
this.isFilterOpen = !this.isFilterOpen;
|
||||
this.filterOpen.emit(this.isFilterOpen);
|
||||
}
|
||||
|
@ -59,8 +59,13 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--side-nav-hover-color);
|
||||
color: var(--side-nav-hover-text-color);
|
||||
background-color: var(--side-nav-hover-bg-color);
|
||||
|
||||
.card-actions i.fa {
|
||||
// TODO: The override to white does not work, please fix for light themes
|
||||
color: var(--side-nav-hover-text-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
@ -76,13 +81,20 @@
|
||||
}
|
||||
|
||||
.side-nav-text, i {
|
||||
color: var(--side-nav-item-active-text-color);
|
||||
|
||||
color: var(--side-nav-item-active-text-color) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--side-nav-hover-color);
|
||||
color: var(--side-nav-hover-text-color);
|
||||
background-color: var(--side-nav-hover-bg-color);
|
||||
|
||||
.card-actions i.fa {
|
||||
// TODO: The override to white does not work, please fix for light themes
|
||||
color: var(--side-nav-hover-text-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||
import { filter, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
|
||||
import { UtilityService } from '../../shared/_services/utility.service';
|
||||
import { Library } from '../../_models/library';
|
||||
@ -48,7 +48,8 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
||||
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.messageHub.messages$.pipe(takeUntil(this.onDestory), filter(event => event.event === EVENTS.LibraryModified)).subscribe(event => {
|
||||
console.log('Received library modfied event');
|
||||
this.libraryService.getLibrariesForMember().pipe(take(1)).subscribe((libraries: Library[]) => {
|
||||
this.libraries = libraries;
|
||||
});
|
||||
|
@ -223,7 +223,6 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
switchMap(val => {
|
||||
this.isLoadingOptions = true;
|
||||
let results: Observable<any[]>;
|
||||
console.log('val: ', val);
|
||||
if (Array.isArray(this.settings.fetchFn)) {
|
||||
const filteredArray = this.settings.compareFn(this.settings.fetchFn, val.trim());
|
||||
results = of(filteredArray).pipe(takeUntil(this.onDestroy), map((items: any[]) => items.filter(item => this.filterSelected(item))));
|
||||
@ -457,9 +456,6 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
if (this.showAddItem) {
|
||||
this.hasFocus = true;
|
||||
}
|
||||
console.log('show Add item: ', this.showAddItem);
|
||||
console.log('compare func: ', this.settings.compareFn(options, this.typeaheadControl.value.trim()));
|
||||
|
||||
}
|
||||
|
||||
unlock(event: any) {
|
||||
|
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<a href="/registration/reset-password" style="color: white">Forgot Password?</a>
|
||||
<a routerLink="/registration/reset-password" style="color: white">Forgot Password?</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
@ -94,7 +94,7 @@
|
||||
--side-nav-openclose-transition: 0.15s ease-in-out;
|
||||
--side-nav-box-shadow: rgba(0,0,0,0.5);
|
||||
--side-nav-mobile-box-shadow: 3px 0em 5px 10em rgb(0 0 0 / 50%);
|
||||
--side-nav-hover-color: white;
|
||||
--side-nav-hover-text-color: white;
|
||||
--side-nav-hover-bg-color: black;
|
||||
--side-nav-color: white;
|
||||
--side-nav-border-radius: 5px;
|
||||
@ -210,5 +210,12 @@
|
||||
--carousel-hover-header-text-decoration: none;
|
||||
|
||||
/** Drawer */
|
||||
--drawer-background-color: black;
|
||||
--drawer-background-color: black;
|
||||
|
||||
/** Event Widget */
|
||||
--event-widget-bg-color: rgb(1, 4, 9);
|
||||
--event-widget-item-bg-color: rgb(1, 4, 9);
|
||||
--event-widget-text-color: var(--body-text-color);
|
||||
--event-widget-item-border-color: rgba(53, 53, 53, 0.5);
|
||||
--event-widget-border-color: rgba(1, 4, 9, 0.5);
|
||||
}
|
||||
|
@ -37,6 +37,7 @@
|
||||
--btn-disabled-bg-color: #020202;
|
||||
--btn-disabled-text-color: white;
|
||||
--btn-disabled-border-color: #6c757d;
|
||||
--btn-fa-icon-color: black;
|
||||
|
||||
|
||||
/* Nav */
|
||||
@ -65,7 +66,7 @@
|
||||
--side-nav-openclose-transition: 1ms;
|
||||
--side-nav-box-shadow: none;
|
||||
--side-nav-mobile-box-shadow: 3px 0em 5px 10em rgb(0 0 0 / 50%);
|
||||
--side-nav-hover-color: white;
|
||||
--side-nav-hover-text-color: white;
|
||||
--side-nav-hover-bg-color: black;
|
||||
--side-nav-color: black;
|
||||
--side-nav-border-radius: 5px;
|
||||
@ -78,7 +79,7 @@
|
||||
--side-nav-item-active-color: var(--primary-color);
|
||||
--side-nav-active-bg-color: rgba(0,0,0,0.5);
|
||||
--side-nav-overlay-color: rgba(0,0,0,1);
|
||||
--side-nav-item-active-text-color: black;
|
||||
--side-nav-item-active-text-color: white;
|
||||
|
||||
/* Toasts */
|
||||
--toast-success-bg-color: rgba(74, 198, 148, 0.9);
|
||||
@ -156,5 +157,19 @@
|
||||
--pagination-focus-border-color: var(--primary-color);
|
||||
--pagination-link-hover-color: var(--primary-color);
|
||||
|
||||
/** Event Widget */
|
||||
--event-widget-bg-color: white;
|
||||
--event-widget-item-bg-color: lightgrey;
|
||||
--event-widget-text-color: black;
|
||||
--event-widget-item-border-color: lightgrey;
|
||||
--event-widget-border-color: lightgrey;
|
||||
|
||||
/* Popover */
|
||||
--popover-body-bg-color: var(--navbar-bg-color);
|
||||
--popover-body-text-color: var(--navbar-text-color);
|
||||
--popover-outerarrow-color: lightgrey;
|
||||
--popover-arrow-color: lightgrey;
|
||||
--popover-bg-color: lightgrey;
|
||||
--popover-border-color: lightgrey;
|
||||
|
||||
}
|
@ -23,11 +23,20 @@
|
||||
|
||||
/* Buttons */
|
||||
--btn-primary-text-color: black;
|
||||
--btn-primary-bg-color: white;
|
||||
--btn-primary-border-color: black;
|
||||
--btn-primary-hover-text-color: white;
|
||||
--btn-primary-hover-bg-color: black;
|
||||
--btn-primary-hover-border-color: black;
|
||||
--btn-alt-bg-color: #424c72;
|
||||
--btn-alt-border-color: #444f75;
|
||||
--btn-alt-hover-bg-color: #3b4466;
|
||||
--btn-alt-focus-bg-color: #343c59;
|
||||
--btn-alt-focus-boxshadow-color: rgb(68 79 117 / 50%);
|
||||
--btn-fa-icon-color: black;
|
||||
--btn-disabled-bg-color: #020202;
|
||||
--btn-disabled-text-color: white;
|
||||
--btn-disabled-border-color: #6c757d;
|
||||
|
||||
/* Nav */
|
||||
--nav-link-active-text-color: white;
|
||||
@ -46,7 +55,7 @@
|
||||
--side-nav-openclose-transition: 0.15s ease-in-out;
|
||||
--side-nav-box-shadow: none;
|
||||
--side-nav-mobile-box-shadow: 3px 0em 5px 10em rgb(0 0 0 / 50%);
|
||||
--side-nav-hover-color: white;
|
||||
--side-nav-hover-text-color: white;
|
||||
--side-nav-hover-bg-color: black;
|
||||
--side-nav-color: black;
|
||||
--side-nav-border-radius: 5px;
|
||||
@ -58,12 +67,12 @@
|
||||
--side-nav-closed-bg-color: transparent;
|
||||
--side-nav-item-active-color: var(--primary-color);
|
||||
--side-nav-active-bg-color: rgba(0,0,0,0.5);
|
||||
--side-nav-item-active-text-color: black;
|
||||
--side-nav-item-active-text-color: white;
|
||||
--side-nav-overlay-color: rgba(0,0,0,0.5);
|
||||
|
||||
|
||||
/* Checkboxes */
|
||||
--checkbox-checked-bg-color: black;
|
||||
--checkbox-checked-bg-color: var(--primary-color);
|
||||
--checkbox-bg-color: white;
|
||||
--checkbox-border-color: var(--primary-color);
|
||||
--checkbox-focus-border-color: var(--input-border-color);
|
||||
@ -148,4 +157,19 @@
|
||||
--pagination-link-bg-color: white;
|
||||
--pagination-focus-border-color: var(--primary-color);
|
||||
--pagination-link-hover-color: var(--primary-color);
|
||||
|
||||
/** Event Widget */
|
||||
--event-widget-bg-color: white;
|
||||
--event-widget-item-bg-color: lightgrey;
|
||||
--event-widget-text-color: black;
|
||||
--event-widget-item-border-color: lightgrey;
|
||||
--event-widget-border-color: lightgrey;
|
||||
|
||||
/* Popover */
|
||||
--popover-body-bg-color: var(--navbar-bg-color);
|
||||
--popover-body-text-color: var(--navbar-text-color);
|
||||
--popover-outerarrow-color: lightgrey;
|
||||
--popover-arrow-color: lightgrey;
|
||||
--popover-bg-color: lightgrey;
|
||||
--popover-border-color: lightgrey;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user