mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
Polishing for Release (#1039)
* Rewrote the delete bookmarked page logic to be more precise with the way it deletes. * Tell user migration email link is in log * Fixed up the email service tooltip * Tweaked messaging * Removed some dead code from series detail page * Default to SortName sorting when nothing is explicitly asked * Updated typeahead to work with changes and fix enter on new/old items * Cleaned up some extra logic in search result rendering code * On super small screens (300px width or less), move the server settings to user dropdown
This commit is contained in:
parent
c2f3e45a15
commit
4fffe1c404
@ -560,13 +560,16 @@ namespace API.Controllers
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
|
||||
if (user.Bookmarks == null) return Ok();
|
||||
try {
|
||||
user.Bookmarks = user.Bookmarks.Where(x =>
|
||||
x.ChapterId == bookmarkDto.ChapterId
|
||||
&& x.AppUserId == user.Id
|
||||
&& x.Page != bookmarkDto.Page).ToList();
|
||||
try
|
||||
{
|
||||
var bookmarkToDelete = user.Bookmarks.SingleOrDefault(x =>
|
||||
x.ChapterId == bookmarkDto.ChapterId && x.AppUserId == user.Id && x.Page == bookmarkDto.Page &&
|
||||
x.SeriesId == bookmarkDto.SeriesId);
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
if (bookmarkToDelete != null)
|
||||
{
|
||||
_unitOfWork.UserRepository.Delete(bookmarkToDelete);
|
||||
}
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
|
@ -80,7 +80,7 @@ namespace API.DTOs.Filtering
|
||||
/// <summary>
|
||||
/// Sorting Options for a query. Defaults to null, which uses the queries natural sorting order
|
||||
/// </summary>
|
||||
public SortOptions SortOptions { get; init; } = null;
|
||||
public SortOptions SortOptions { get; set; } = null;
|
||||
/// <summary>
|
||||
/// Age Ratings. Empty list will return everything back
|
||||
/// </summary>
|
||||
|
@ -233,6 +233,14 @@ public class SeriesRepository : ISeriesRepository
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all series
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <param name="filter"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter)
|
||||
{
|
||||
var query = await CreateFilteredSearchQueryable(userId, libraryId, filter);
|
||||
@ -637,34 +645,32 @@ public class SeriesRepository : ISeriesRepository
|
||||
)
|
||||
.AsNoTracking();
|
||||
|
||||
if (filter.SortOptions != null)
|
||||
// If no sort options, default to using SortName
|
||||
filter.SortOptions ??= new SortOptions()
|
||||
{
|
||||
if (filter.SortOptions.IsAscending)
|
||||
IsAscending = true,
|
||||
SortField = SortField.SortName
|
||||
};
|
||||
|
||||
if (filter.SortOptions.IsAscending)
|
||||
{
|
||||
query = filter.SortOptions.SortField switch
|
||||
{
|
||||
if (filter.SortOptions.SortField == SortField.SortName)
|
||||
{
|
||||
query = query.OrderBy(s => s.SortName);
|
||||
} else if (filter.SortOptions.SortField == SortField.CreatedDate)
|
||||
{
|
||||
query = query.OrderBy(s => s.Created);
|
||||
} else if (filter.SortOptions.SortField == SortField.LastModifiedDate)
|
||||
{
|
||||
query = query.OrderBy(s => s.LastModified);
|
||||
}
|
||||
}
|
||||
else
|
||||
SortField.SortName => query.OrderBy(s => s.SortName),
|
||||
SortField.CreatedDate => query.OrderBy(s => s.Created),
|
||||
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
|
||||
_ => query
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
query = filter.SortOptions.SortField switch
|
||||
{
|
||||
if (filter.SortOptions.SortField == SortField.SortName)
|
||||
{
|
||||
query = query.OrderByDescending(s => s.SortName);
|
||||
} else if (filter.SortOptions.SortField == SortField.CreatedDate)
|
||||
{
|
||||
query = query.OrderByDescending(s => s.Created);
|
||||
} else if (filter.SortOptions.SortField == SortField.LastModifiedDate)
|
||||
{
|
||||
query = query.OrderByDescending(s => s.LastModified);
|
||||
}
|
||||
}
|
||||
SortField.SortName => query.OrderByDescending(s => s.SortName),
|
||||
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
|
||||
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
|
||||
_ => query
|
||||
};
|
||||
}
|
||||
|
||||
return query;
|
||||
|
@ -31,6 +31,7 @@ public interface IUserRepository
|
||||
void Update(AppUserPreferences preferences);
|
||||
void Update(AppUserBookmark bookmark);
|
||||
public void Delete(AppUser user);
|
||||
void Delete(AppUserBookmark bookmark);
|
||||
Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync();
|
||||
Task<IEnumerable<MemberDto>> GetPendingMemberDtosAsync();
|
||||
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
|
||||
@ -53,6 +54,7 @@ public interface IUserRepository
|
||||
Task<IList<AppUserBookmark>> GetAllBookmarksByIds(IList<int> bookmarkIds);
|
||||
Task<AppUser> GetUserByEmailAsync(string email);
|
||||
Task<IEnumerable<AppUser>> GetAllUsers();
|
||||
|
||||
}
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
@ -88,6 +90,11 @@ public class UserRepository : IUserRepository
|
||||
_context.AppUser.Remove(user);
|
||||
}
|
||||
|
||||
public void Delete(AppUserBookmark bookmark)
|
||||
{
|
||||
_context.AppUserBookmark.Remove(bookmark);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags.
|
||||
/// </summary>
|
||||
|
@ -67,11 +67,12 @@
|
||||
|
||||
<h4>Email Services (SMTP)</h4>
|
||||
<p class="accent">Kavita comes out of the box with an email service to power flows like invite user, forgot password, etc. Emails sent via our service are deleted immediately. You can use your own
|
||||
email service. Set the url of the email service and use the Test button to ensure it works. At any time you can reset to ours. There is no way to disable emails.
|
||||
email service. Set the url of the email service and use the Test button to ensure it works. At any time you can reset to ours. There is no way to disable emails althought confirmation links will always
|
||||
be saved to logs.
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="settings-emailservice">Email Service Url</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="emailServiceTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #emailServiceTooltip>Location where bookmarks will be stored. Bookmarks are source files and can be large. Choose a location with adequate storage. Directory is managed, other files within directory will be deleted.</ng-template>
|
||||
<ng-template #emailServiceTooltip>Use fully qualified url of the email service. Do not include ending slash.</ng-template>
|
||||
<span class="sr-only" id="settings-emailservice-help"><ng-container [ngTemplateOutlet]="emailServiceTooltip"></ng-container></span>
|
||||
<div class="input-group">
|
||||
<input id="settings-emailservice" aria-describedby="settings-emailservice-help" class="form-control" formControlName="emailServiceUrl" type="text" aria-describedby="change-bookmarks-dir">
|
||||
|
@ -2,7 +2,7 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { forkJoin, Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { map, takeUntil } from 'rxjs/operators';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { TypeaheadSettings } from 'src/app/typeahead/typeahead-settings';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
@ -120,13 +120,15 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
this.settings.id = 'collections';
|
||||
this.settings.unique = true;
|
||||
this.settings.addIfNonExisting = true;
|
||||
this.settings.fetchFn = (filter: string) => this.fetchCollectionTags(filter);
|
||||
this.settings.fetchFn = (filter: string) => this.fetchCollectionTags(filter).pipe(map(items => this.settings.compareFn(items, filter)));
|
||||
this.settings.addTransformFn = ((title: string) => {
|
||||
return {id: 0, title: title, promoted: false, coverImage: '', summary: '', coverImageLocked: false };
|
||||
});
|
||||
this.settings.compareFn = (options: CollectionTag[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f);
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.settings.singleCompareFn = (a: CollectionTag, b: CollectionTag) => {
|
||||
return a.id == b.id;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,10 +199,13 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
||||
this.formatSettings.id = 'format';
|
||||
this.formatSettings.unique = true;
|
||||
this.formatSettings.addIfNonExisting = false;
|
||||
this.formatSettings.fetchFn = (filter: string) => of(mangaFormatFilters);
|
||||
this.formatSettings.fetchFn = (filter: string) => of(mangaFormatFilters).pipe(map(items => this.formatSettings.compareFn(items, filter)));
|
||||
this.formatSettings.compareFn = (options: FilterItem<MangaFormat>[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f);
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
|
||||
this.formatSettings.singleCompareFn = (a: FilterItem<MangaFormat>, b: FilterItem<MangaFormat>) => {
|
||||
return a.title == b.title;
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets?.formats && this.filterSettings.presets?.formats.length > 0) {
|
||||
@ -219,11 +222,14 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
||||
this.librarySettings.unique = true;
|
||||
this.librarySettings.addIfNonExisting = false;
|
||||
this.librarySettings.fetchFn = (filter: string) => {
|
||||
return this.libraryService.getLibrariesForMember();
|
||||
return this.libraryService.getLibrariesForMember()
|
||||
.pipe(map(items => this.librarySettings.compareFn(items, filter)));
|
||||
};
|
||||
this.librarySettings.compareFn = (options: Library[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.name.toLowerCase() === f);
|
||||
return options.filter(m => this.utilityService.filter(m.name, filter));
|
||||
}
|
||||
this.librarySettings.singleCompareFn = (a: Library, b: Library) => {
|
||||
return a.name == b.name;
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets?.libraries && this.filterSettings.presets?.libraries.length > 0) {
|
||||
@ -243,11 +249,14 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
||||
this.genreSettings.unique = true;
|
||||
this.genreSettings.addIfNonExisting = false;
|
||||
this.genreSettings.fetchFn = (filter: string) => {
|
||||
return this.metadataService.getAllGenres(this.filter.libraries);
|
||||
return this.metadataService.getAllGenres(this.filter.libraries)
|
||||
.pipe(map(items => this.genreSettings.compareFn(items, filter)));
|
||||
};
|
||||
this.genreSettings.compareFn = (options: Genre[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f);
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.genreSettings.singleCompareFn = (a: Genre, b: Genre) => {
|
||||
return a.title == b.title;
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets?.genres && this.filterSettings.presets?.genres.length > 0) {
|
||||
@ -266,12 +275,15 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
||||
this.ageRatingSettings.id = 'age-rating';
|
||||
this.ageRatingSettings.unique = true;
|
||||
this.ageRatingSettings.addIfNonExisting = false;
|
||||
this.ageRatingSettings.fetchFn = (filter: string) => {
|
||||
return this.metadataService.getAllAgeRatings(this.filter.libraries);
|
||||
};
|
||||
this.ageRatingSettings.fetchFn = (filter: string) => this.metadataService.getAllAgeRatings(this.filter.libraries)
|
||||
.pipe(map(items => this.ageRatingSettings.compareFn(items, filter)));
|
||||
|
||||
this.ageRatingSettings.compareFn = (options: AgeRatingDto[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f && this.utilityService.filter(m.title, filter));
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
|
||||
this.ageRatingSettings.singleCompareFn = (a: AgeRatingDto, b: AgeRatingDto) => {
|
||||
return a.title == b.title;
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets?.ageRating && this.filterSettings.presets?.ageRating.length > 0) {
|
||||
@ -290,12 +302,15 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
||||
this.publicationStatusSettings.id = 'publication-status';
|
||||
this.publicationStatusSettings.unique = true;
|
||||
this.publicationStatusSettings.addIfNonExisting = false;
|
||||
this.publicationStatusSettings.fetchFn = (filter: string) => {
|
||||
return this.metadataService.getAllPublicationStatus(this.filter.libraries);
|
||||
};
|
||||
this.publicationStatusSettings.fetchFn = (filter: string) => this.metadataService.getAllPublicationStatus(this.filter.libraries)
|
||||
.pipe(map(items => this.publicationStatusSettings.compareFn(items, filter)));
|
||||
|
||||
this.publicationStatusSettings.compareFn = (options: PublicationStatusDto[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f && this.utilityService.filter(m.title, filter));
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
|
||||
this.publicationStatusSettings.singleCompareFn = (a: PublicationStatusDto, b: PublicationStatusDto) => {
|
||||
return a.title == b.title;
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets?.publicationStatus && this.filterSettings.presets?.publicationStatus.length > 0) {
|
||||
@ -314,12 +329,14 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
||||
this.tagsSettings.id = 'tags';
|
||||
this.tagsSettings.unique = true;
|
||||
this.tagsSettings.addIfNonExisting = false;
|
||||
this.tagsSettings.fetchFn = (filter: string) => {
|
||||
return this.metadataService.getAllTags(this.filter.libraries);
|
||||
};
|
||||
this.tagsSettings.compareFn = (options: Tag[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f && this.utilityService.filter(m.title, filter));
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.tagsSettings.fetchFn = (filter: string) => this.metadataService.getAllTags(this.filter.libraries)
|
||||
.pipe(map(items => this.tagsSettings.compareFn(items, filter)));
|
||||
|
||||
this.tagsSettings.singleCompareFn = (a: Tag, b: Tag) => {
|
||||
return a.id == b.id;
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets?.tags && this.filterSettings.presets?.tags.length > 0) {
|
||||
@ -338,12 +355,14 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
||||
this.languageSettings.id = 'languages';
|
||||
this.languageSettings.unique = true;
|
||||
this.languageSettings.addIfNonExisting = false;
|
||||
this.languageSettings.fetchFn = (filter: string) => {
|
||||
return this.metadataService.getAllLanguages(this.filter.libraries);
|
||||
};
|
||||
this.languageSettings.compareFn = (options: Language[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f && this.utilityService.filter(m.title, filter));
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.languageSettings.fetchFn = (filter: string) => this.metadataService.getAllLanguages(this.filter.libraries)
|
||||
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
|
||||
|
||||
this.languageSettings.singleCompareFn = (a: Language, b: Language) => {
|
||||
return a.isoCode == b.isoCode;
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets?.languages && this.filterSettings.presets?.languages.length > 0) {
|
||||
@ -362,12 +381,14 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
||||
this.collectionSettings.id = 'collections';
|
||||
this.collectionSettings.unique = true;
|
||||
this.collectionSettings.addIfNonExisting = false;
|
||||
this.collectionSettings.fetchFn = (filter: string) => {
|
||||
return this.collectionTagService.allTags();
|
||||
};
|
||||
this.collectionSettings.compareFn = (options: CollectionTag[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.title.toLowerCase() === f && this.utilityService.filter(m.title, filter));
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.collectionSettings.fetchFn = (filter: string) => this.collectionTagService.allTags()
|
||||
.pipe(map(items => this.collectionSettings.compareFn(items, filter)));
|
||||
|
||||
this.collectionSettings.singleCompareFn = (a: CollectionTag, b: CollectionTag) => {
|
||||
return a.id == b.id;
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets?.collectionTags && this.filterSettings.presets?.collectionTags.length > 0) {
|
||||
@ -432,11 +453,14 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
||||
personSettings.addIfNonExisting = false;
|
||||
personSettings.id = id;
|
||||
personSettings.compareFn = (options: Person[], filter: string) => {
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter(m => m.name.toLowerCase() === f);
|
||||
return options.filter(m => this.utilityService.filter(m.name, filter));
|
||||
}
|
||||
|
||||
personSettings.singleCompareFn = (a: Person, b: Person) => {
|
||||
return a.name == b.name && a.role == b.role;
|
||||
}
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
return this.fetchPeople(role, filter);
|
||||
return this.fetchPeople(role, filter).pipe(map(items => personSettings.compareFn(items, filter)));
|
||||
};
|
||||
return personSettings;
|
||||
}
|
||||
|
@ -44,7 +44,7 @@
|
||||
<app-image class="mr-3 search-result" width="24px" [imageUrl]="imageService.getCollectionCoverImage(item.id)"></app-image>
|
||||
</div>
|
||||
<div class="ml-1">
|
||||
<span *ngIf="item.title.toLowerCase().trim().indexOf(searchTerm) >= 0">{{item.title}}</span>
|
||||
<span>{{item.title}}</span>
|
||||
<span *ngIf="item.promoted">
|
||||
<i class="fa fa-angle-double-up" aria-hidden="true" title="Promoted"></i>
|
||||
<span class="sr-only">(promoted)</span>
|
||||
@ -56,7 +56,7 @@
|
||||
<ng-template #tagTemplate let-item>
|
||||
<div style="display: flex;padding: 5px;" (click)="goTo('tags', item.id)">
|
||||
<div class="ml-1">
|
||||
<span *ngIf="item.title.toLowerCase().trim().indexOf(searchTerm) >= 0">{{item.title}}</span>
|
||||
<span>{{item.title}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -103,7 +103,7 @@
|
||||
<div class="nav-item">
|
||||
<app-nav-events-toggle [user]="user"></app-nav-events-toggle>
|
||||
</div>
|
||||
<div class="nav-item pr-2">
|
||||
<div class="nav-item pr-2 not-xs-only">
|
||||
<a routerLink="/admin/dashboard" *ngIf="user.roles.includes('Admin')" class="dark-exempt btn btn-icon">
|
||||
<i class="fa fa-cogs" aria-hidden="true" style="color: white"></i>
|
||||
<span class="sr-only">Server Settings</span>
|
||||
@ -116,6 +116,7 @@
|
||||
{{user.username | sentenceCase}}
|
||||
</button>
|
||||
<div ngbDropdownMenu>
|
||||
<a class="xs-only" ngbDropdownItem routerLink="/admin/dashboard" *ngIf="user.roles.includes('Admin')">Server Settings</a>
|
||||
<a ngbDropdownItem routerLink="/preferences/">Settings</a>
|
||||
<a ngbDropdownItem (click)="logout()">Logout</a>
|
||||
</div>
|
||||
|
@ -18,6 +18,22 @@ $bg-color: rgb(22, 27, 34);
|
||||
}
|
||||
}
|
||||
|
||||
// On Really small screens, hide the server settings wheel and show it in nav
|
||||
.xs-only {
|
||||
display: none;
|
||||
}
|
||||
.not-xs-only {
|
||||
display: inherit;
|
||||
}
|
||||
@media only screen and (max-width:300px) {
|
||||
.xs-only {
|
||||
display: inherit;
|
||||
}
|
||||
.not-xs-only {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item.dropdown {
|
||||
position: unset;
|
||||
}
|
||||
|
@ -46,10 +46,10 @@ export class AddEmailToAccountMigrationModalComponent implements OnInit {
|
||||
if (!canAccess) {
|
||||
// Display the email to the user
|
||||
this.emailLink = email;
|
||||
await this.confirmService.alert('Please click this link to confirm your email. You must confirm to be able to login. You may need to log out of the current account before clicking. <br/> <a href="' + this.emailLink + '" target="_blank">' + this.emailLink + '</a>');
|
||||
await this.confirmService.alert('Please click this link to confirm your email. You must confirm to be able to login. The link is in your logs. You may need to log out of the current account before clicking. <br/> <a href="' + this.emailLink + '" target="_blank">' + this.emailLink + '</a>');
|
||||
this.modal.close(true);
|
||||
} else {
|
||||
await this.confirmService.alert('Please check your email for the confirmation link. You must confirm to be able to login.');
|
||||
await this.confirmService.alert('Please check your email (or logs) for the confirmation link. You must confirm to be able to login.');
|
||||
this.modal.close(true);
|
||||
}
|
||||
});
|
||||
|
@ -281,10 +281,10 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
||||
handleVolumeActionCallback(action: Action, volume: Volume) {
|
||||
switch(action) {
|
||||
case(Action.MarkAsRead):
|
||||
this.markAsRead(volume);
|
||||
this.markVolumeAsRead(volume);
|
||||
break;
|
||||
case(Action.MarkAsUnread):
|
||||
this.markAsUnread(volume);
|
||||
this.markVolumeAsUnread(volume);
|
||||
break;
|
||||
case(Action.Edit):
|
||||
this.openViewInfo(volume);
|
||||
@ -334,22 +334,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
markSeriesAsUnread(series: Series) {
|
||||
this.seriesService.markUnread(series.id).subscribe(res => {
|
||||
this.toastr.success(series.name + ' is now unread');
|
||||
series.pagesRead = 0;
|
||||
this.loadSeries(series.id);
|
||||
});
|
||||
}
|
||||
|
||||
markSeriesAsRead(series: Series) {
|
||||
this.seriesService.markRead(series.id).subscribe(res => {
|
||||
series.pagesRead = series.pages;
|
||||
this.toastr.success(series.name + ' is now read');
|
||||
this.loadSeries(series.id);
|
||||
});
|
||||
}
|
||||
|
||||
loadSeries(seriesId: number) {
|
||||
this.coverImageOffset = 0;
|
||||
|
||||
@ -444,25 +428,23 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
||||
this.readerService.getCurrentChapter(this.series.id).subscribe(chapter => this.currentlyReadingChapter = chapter);
|
||||
}
|
||||
|
||||
markAsRead(vol: Volume) {
|
||||
markVolumeAsRead(vol: Volume) {
|
||||
if (this.series === undefined) {
|
||||
return;
|
||||
}
|
||||
const seriesId = this.series.id;
|
||||
|
||||
this.actionService.markVolumeAsRead(seriesId, vol, () => {
|
||||
this.actionService.markVolumeAsRead(this.series.id, vol, () => {
|
||||
this.setContinuePoint();
|
||||
this.actionInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
markAsUnread(vol: Volume) {
|
||||
markVolumeAsUnread(vol: Volume) {
|
||||
if (this.series === undefined) {
|
||||
return;
|
||||
}
|
||||
const seriesId = this.series.id;
|
||||
|
||||
this.actionService.markVolumeAsUnread(seriesId, vol, () => {
|
||||
this.actionService.markVolumeAsUnread(this.series.id, vol, () => {
|
||||
this.setContinuePoint();
|
||||
this.actionInProgress = false;
|
||||
});
|
||||
@ -472,9 +454,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
||||
if (this.series === undefined) {
|
||||
return;
|
||||
}
|
||||
const seriesId = this.series.id;
|
||||
|
||||
this.actionService.markChapterAsRead(seriesId, chapter, () => {
|
||||
this.actionService.markChapterAsRead(this.series.id, chapter, () => {
|
||||
this.setContinuePoint();
|
||||
this.actionInProgress = false;
|
||||
});
|
||||
@ -484,9 +465,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
||||
if (this.series === undefined) {
|
||||
return;
|
||||
}
|
||||
const seriesId = this.series.id;
|
||||
|
||||
this.actionService.markChapterAsUnread(seriesId, chapter, () => {
|
||||
this.actionService.markChapterAsUnread(this.series.id, chapter, () => {
|
||||
this.setContinuePoint();
|
||||
this.actionInProgress = false;
|
||||
});
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
export type SelectionCompareFn<T> = (a: T, b: T) => boolean;
|
||||
|
||||
export class TypeaheadSettings<T> {
|
||||
/**
|
||||
* How many ms between typing actions before pipeline to load data is triggered
|
||||
@ -24,7 +26,11 @@ export class TypeaheadSettings<T> {
|
||||
/**
|
||||
* Function to compare the elements. Should return all elements that fit the matching criteria. This is only used with non-Observable based fetchFn, but must be defined for all uses of typeahead (TODO)
|
||||
*/
|
||||
compareFn!: ((optionList: T[], filter: string) => T[]);
|
||||
compareFn!: ((optionList: T[], filter: string) => T[]);
|
||||
/**
|
||||
* Function which is used for comparing objects when keeping track of state. Useful over shallow equal when you have image urls that have random numbers on them.
|
||||
*/
|
||||
singleCompareFn?: SelectionCompareFn<T>;
|
||||
/**
|
||||
* Function to fetch the data from the server. If data is mainatined in memory, wrap in an observable.
|
||||
*/
|
||||
|
@ -3,9 +3,9 @@ import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
|
||||
import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
|
||||
import { KEY_CODES } from '../shared/_services/utility.service';
|
||||
import { TypeaheadSettings } from './typeahead-settings';
|
||||
import { SelectionCompareFn, TypeaheadSettings } from './typeahead-settings';
|
||||
|
||||
export type SelectionCompareFn<T> = (a: T, b: T) => boolean;
|
||||
//export type SelectionCompareFn<T> = (a: T, b: T) => boolean;
|
||||
|
||||
/**
|
||||
* SelectionModel<T> is used for keeping track of multiple selections. Simple interface with ability to toggle.
|
||||
@ -60,14 +60,16 @@ export class SelectionModel<T> {
|
||||
* @param compareFn optional method to use to perform comparisons
|
||||
* @returns boolean
|
||||
*/
|
||||
isSelected(data: T, compareFn?: ((d: T) => boolean)): boolean {
|
||||
isSelected(data: T, compareFn?: SelectionCompareFn<T>): boolean {
|
||||
let dataItem: Array<any>;
|
||||
|
||||
let lookupMethod = this.shallowEqual;
|
||||
if (compareFn != undefined || compareFn != null) {
|
||||
dataItem = this._data.filter(d => compareFn(d.value));
|
||||
} else {
|
||||
dataItem = this._data.filter(d => this.shallowEqual(d.value, data));
|
||||
lookupMethod = compareFn;
|
||||
}
|
||||
|
||||
dataItem = this._data.filter(d => lookupMethod(d.value, data));
|
||||
|
||||
if (dataItem.length > 0) {
|
||||
return dataItem[0].selected;
|
||||
}
|
||||
@ -283,13 +285,17 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
if (item.classList.contains('active')) {
|
||||
this.filteredOptions.pipe(take(1)).subscribe((res: any[]) => {
|
||||
// This isn't giving back the filtered array, but everything
|
||||
const result = this.settings.compareFn(res, (item.textContent || '').trim());
|
||||
|
||||
if (this.settings.addIfNonExisting && item.classList.contains('add-item')) {
|
||||
this.addNewItem(this.typeaheadControl.value);
|
||||
this.resetField();
|
||||
this.focusedIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = this.settings.compareFn(res, (this.typeaheadControl.value || '').trim());
|
||||
if (result.length === 1) {
|
||||
if (item.classList.contains('add-item')) {
|
||||
this.addNewItem(this.typeaheadControl.value);
|
||||
} else {
|
||||
this.toggleSelection(result[0]);
|
||||
}
|
||||
this.toggleSelection(result[0]);
|
||||
this.resetField();
|
||||
this.focusedIndex = 0;
|
||||
}
|
||||
@ -320,12 +326,12 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
toggleSelection(opt: any): void {
|
||||
this.optionSelection.toggle(opt);
|
||||
this.optionSelection.toggle(opt, undefined, this.settings.singleCompareFn);
|
||||
this.selectedData.emit(this.optionSelection.selected());
|
||||
}
|
||||
|
||||
removeSelectedOption(opt: any) {
|
||||
this.optionSelection.toggle(opt);
|
||||
this.optionSelection.toggle(opt, undefined, this.settings.singleCompareFn);
|
||||
this.selectedData.emit(this.optionSelection.selected());
|
||||
this.resetField();
|
||||
}
|
||||
@ -351,7 +357,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
|
||||
filterSelected(item: any) {
|
||||
if (this.settings.unique && this.settings.multiple) {
|
||||
return !this.optionSelection.isSelected(item);
|
||||
return !this.optionSelection.isSelected(item, this.settings.singleCompareFn);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
Loading…
x
Reference in New Issue
Block a user