diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 4c8465b21..39ec8bdd9 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -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()) { diff --git a/API/DTOs/Filtering/FilterDto.cs b/API/DTOs/Filtering/FilterDto.cs index 863e66e6e..fba9a7493 100644 --- a/API/DTOs/Filtering/FilterDto.cs +++ b/API/DTOs/Filtering/FilterDto.cs @@ -80,7 +80,7 @@ namespace API.DTOs.Filtering /// /// Sorting Options for a query. Defaults to null, which uses the queries natural sorting order /// - public SortOptions SortOptions { get; init; } = null; + public SortOptions SortOptions { get; set; } = null; /// /// Age Ratings. Empty list will return everything back /// diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 95fb44a9f..67238efb7 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -233,6 +233,14 @@ public class SeriesRepository : ISeriesRepository .SingleOrDefaultAsync(); } + /// + /// Gets all series + /// + /// + /// + /// + /// + /// public async Task> 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; diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 9eb276995..b926abe9c 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -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> GetEmailConfirmedMemberDtosAsync(); Task> GetPendingMemberDtosAsync(); Task> GetAdminUsersAsync(); @@ -53,6 +54,7 @@ public interface IUserRepository Task> GetAllBookmarksByIds(IList bookmarkIds); Task GetUserByEmailAsync(string email); Task> 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); + } + /// /// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags. /// diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index ad37eba12..344fc374f 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -67,11 +67,12 @@

Email Services (SMTP)

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.

  - 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. + Use fully qualified url of the email service. Do not include ending slash.
diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts index ff1123797..c6b1e7452 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts @@ -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; } } diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts index 79e9be68e..a84d1ba2e 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts @@ -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[], 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, b: FilterItem) => { + 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; } diff --git a/UI/Web/src/app/nav-header/nav-header.component.html b/UI/Web/src/app/nav-header/nav-header.component.html index d027e8202..8ae50f034 100644 --- a/UI/Web/src/app/nav-header/nav-header.component.html +++ b/UI/Web/src/app/nav-header/nav-header.component.html @@ -44,7 +44,7 @@
- {{item.title}} + {{item.title}}   (promoted) @@ -56,7 +56,7 @@
- {{item.title}} + {{item.title}}
@@ -103,7 +103,7 @@ -