Compare commits
6 commits
develop
...
feature/de
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cba7bc1190 | ||
![]() |
50273db00c | ||
![]() |
5abc0fefd6 | ||
![]() |
a9ca770307 | ||
![]() |
7263a561d9 | ||
![]() |
99424acb48 |
16 changed files with 284 additions and 80 deletions
|
@ -2,6 +2,7 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Search;
|
||||
using API.Extensions;
|
||||
|
@ -47,20 +48,25 @@ public class SearchController : BaseApiController
|
|||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search for different entities against the query string
|
||||
/// </summary>
|
||||
/// <param name="queryString"></param>
|
||||
/// <param name="maxRecords">Defaults to 15, if 0 will not apply any limit to search results and may result in longer response times</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("search")]
|
||||
public async Task<ActionResult<SearchResultGroupDto>> Search(string queryString)
|
||||
public async Task<ActionResult<SearchResultGroupDto>> Search(string queryString, int maxRecords = 15)
|
||||
{
|
||||
queryString = Uri.UnescapeDataString(queryString).Trim().Replace(@"%", string.Empty).Replace(":", string.Empty);
|
||||
queryString = Uri.UnescapeDataString(queryString)
|
||||
.Trim()
|
||||
.Replace(@"%", string.Empty)
|
||||
.Replace(":", string.Empty);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Library);
|
||||
if (!user.Libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
// Get libraries user has access to
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
|
||||
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.SearchSeries(user.Id, isAdmin, libraries.Select(l => l.Id).ToArray(), queryString);
|
||||
var series = await _unitOfWork.SeriesRepository.SearchSeries(user.Id, isAdmin,
|
||||
user.Libraries.Select(l => l.Id).ToArray(), queryString, maxRecords);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ public interface ISeriesRepository
|
|||
/// <param name="libraryIds"></param>
|
||||
/// <param name="searchQuery"></param>
|
||||
/// <returns></returns>
|
||||
Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, int[] libraryIds, string searchQuery);
|
||||
Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, int[] libraryIds, string searchQuery, int maxRecords = 15);
|
||||
Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None);
|
||||
Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId);
|
||||
Task<Series> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata);
|
||||
|
@ -287,27 +287,35 @@ public class SeriesRepository : ISeriesRepository
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, int[] libraryIds, string searchQuery)
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="isAdmin"></param>
|
||||
/// <param name="libraryIds"></param>
|
||||
/// <param name="searchQuery"></param>
|
||||
/// <param name="maxRecords">If 0 or less, will not apply any LIMIT</param>
|
||||
/// <returns></returns>
|
||||
public async Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, int[] libraryIds, string searchQuery, int maxRecords = 15)
|
||||
{
|
||||
const int maxRecords = 15;
|
||||
var result = new SearchResultGroupDto();
|
||||
var searchQueryNormalized = Services.Tasks.Scanner.Parser.Parser.Normalize(searchQuery);
|
||||
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
var seriesIds = _context.Series
|
||||
var seriesIds = await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.Select(s => s.Id)
|
||||
.ToList();
|
||||
.ToListAsync();
|
||||
|
||||
result.Libraries = await _context.Library
|
||||
result.Libraries = _context.Library
|
||||
.Where(l => libraryIds.Contains(l.Id))
|
||||
.Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%"))
|
||||
.OrderBy(l => l.Name)
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.TakeIfGreaterThan0(maxRecords)
|
||||
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
.AsEnumerable();
|
||||
|
||||
var justYear = Regex.Match(searchQuery, @"\d{4}").Value;
|
||||
var hasYearInQuery = !string.IsNullOrEmpty(justYear);
|
||||
|
@ -325,60 +333,61 @@ public class SeriesRepository : ISeriesRepository
|
|||
.OrderBy(s => s.SortName)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.TakeIfGreaterThan0(maxRecords)
|
||||
.ProjectTo<SearchResultDto>(_mapper.ConfigurationProvider)
|
||||
.AsEnumerable();
|
||||
|
||||
result.ReadingLists = await _context.ReadingList
|
||||
result.ReadingLists = _context.ReadingList
|
||||
.Where(rl => rl.AppUserId == userId || rl.Promoted)
|
||||
.Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%"))
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.TakeIfGreaterThan0(maxRecords)
|
||||
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
.AsEnumerable();
|
||||
|
||||
result.Collections = await _context.CollectionTag
|
||||
result.Collections = _context.CollectionTag
|
||||
.Where(c => EF.Functions.Like(c.Title, $"%{searchQuery}%")
|
||||
|| EF.Functions.Like(c.NormalizedTitle, $"%{searchQueryNormalized}%"))
|
||||
.Where(c => c.Promoted || isAdmin)
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.OrderBy(s => s.Title)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.OrderBy(s => s.Title)
|
||||
.TakeIfGreaterThan0(maxRecords)
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
.AsEnumerable();
|
||||
|
||||
result.Persons = await _context.SeriesMetadata
|
||||
result.Persons = _context.SeriesMetadata
|
||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||
.SelectMany(sm => sm.People.Where(t => EF.Functions.Like(t.Name, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.Distinct()
|
||||
.OrderBy(p => p.NormalizedName)
|
||||
.TakeIfGreaterThan0(maxRecords)
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
.AsEnumerable();
|
||||
|
||||
result.Genres = await _context.SeriesMetadata
|
||||
result.Genres = _context.SeriesMetadata
|
||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||
.SelectMany(sm => sm.Genres.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.OrderBy(t => t.Title)
|
||||
.Distinct()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(t => t.Title)
|
||||
.TakeIfGreaterThan0(maxRecords)
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
.AsEnumerable();
|
||||
|
||||
result.Tags = await _context.SeriesMetadata
|
||||
result.Tags = _context.SeriesMetadata
|
||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||
.SelectMany(sm => sm.Tags.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.OrderBy(t => t.Title)
|
||||
.Distinct()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(t => t.Title)
|
||||
.TakeIfGreaterThan0(maxRecords)
|
||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
.AsEnumerable();
|
||||
|
||||
var fileIds = _context.Series
|
||||
.Where(s => seriesIds.Contains(s.Id))
|
||||
|
@ -387,22 +396,24 @@ public class SeriesRepository : ISeriesRepository
|
|||
.SelectMany(v => v.Chapters)
|
||||
.SelectMany(c => c.Files.Select(f => f.Id));
|
||||
|
||||
result.Files = await _context.MangaFile
|
||||
result.Files = _context.MangaFile
|
||||
.Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id))
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(f => f.Id)
|
||||
.TakeIfGreaterThan0(maxRecords)
|
||||
.ProjectTo<MangaFileDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
.AsEnumerable();
|
||||
|
||||
|
||||
result.Chapters = await _context.Chapter
|
||||
result.Chapters = _context.Chapter
|
||||
.Include(c => c.Files)
|
||||
.Where(c => EF.Functions.Like(c.TitleName, $"%{searchQuery}%"))
|
||||
.Where(c => c.Files.All(f => fileIds.Contains(f.Id)))
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(c => c.Id)
|
||||
.TakeIfGreaterThan0(maxRecords)
|
||||
.ProjectTo<ChapterDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
.AsEnumerable();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ public enum AppUserIncludes
|
|||
WantToRead = 64,
|
||||
ReadingListsWithItems = 128,
|
||||
Devices = 256,
|
||||
|
||||
Library = 512,
|
||||
}
|
||||
|
||||
public interface IUserRepository
|
||||
|
@ -202,9 +202,12 @@ public class UserRepository : IUserRepository
|
|||
query = query.Include(u => u.Devices);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.Library))
|
||||
{
|
||||
query = query.Include(u => u.Libraries);
|
||||
}
|
||||
|
||||
|
||||
return query;
|
||||
return query.AsSplitQuery();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -110,4 +110,21 @@ public static class QueryableExtensions
|
|||
})
|
||||
.SingleAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only applies the Take if it's greater than 0
|
||||
/// </summary>
|
||||
/// <param name="queryable"></param>
|
||||
/// <param name="takeAmt"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static IQueryable<T> TakeIfGreaterThan0<T>(this IQueryable<T> queryable, int takeAmt)
|
||||
{
|
||||
if (takeAmt > 0)
|
||||
{
|
||||
return queryable.Take(takeAmt);
|
||||
}
|
||||
|
||||
return queryable;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { of } from 'rxjs';
|
||||
import { distinctUntilChanged, map, Observable, of, ReplaySubject, startWith, switchMap } from 'rxjs';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { SearchResultGroup } from '../_models/search/search-result-group';
|
||||
import { Series } from '../_models/series';
|
||||
|
@ -12,13 +12,24 @@ export class SearchService {
|
|||
|
||||
baseUrl = environment.apiUrl;
|
||||
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
private searchSubject: ReplaySubject<string> = new ReplaySubject(1);
|
||||
searchResults$: Observable<SearchResultGroup>;
|
||||
searchTerm$: Observable<string> = this.searchSubject.asObservable();
|
||||
|
||||
constructor(private httpClient: HttpClient) {
|
||||
this.searchResults$ = this.searchSubject.pipe(
|
||||
startWith(''),
|
||||
map(val => val.trim()),
|
||||
distinctUntilChanged(),
|
||||
switchMap(term => {
|
||||
if (term === '' || term === null || term === undefined) return of(new SearchResultGroup());
|
||||
return this.httpClient.get<SearchResultGroup>(this.baseUrl + 'search/search?queryString=' + encodeURIComponent(term))
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
search(term: string) {
|
||||
if (term === '') {
|
||||
return of(new SearchResultGroup());
|
||||
}
|
||||
return this.httpClient.get<SearchResultGroup>(this.baseUrl + 'search/search?queryString=' + encodeURIComponent(term));
|
||||
this.searchSubject.next(term);
|
||||
}
|
||||
|
||||
getSeriesForMangaFile(mangaFileId: number) {
|
||||
|
|
|
@ -49,6 +49,10 @@ const routes: Routes = [
|
|||
path: 'want-to-read',
|
||||
loadChildren: () => import('../app/want-to-read/want-to-read.module').then(m => m.WantToReadModule)
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
loadChildren: () => import('../app/search/search.module').then(m => m.SearchModule)
|
||||
},
|
||||
{
|
||||
path: 'library',
|
||||
runGuardsAndResolvers: 'always',
|
||||
|
|
|
@ -129,11 +129,14 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
|
|||
seriesSettings.id = 'relation--' + index;
|
||||
seriesSettings.unique = true;
|
||||
seriesSettings.addIfNonExisting = false;
|
||||
seriesSettings.fetchFn = (searchFilter: string) => this.searchService.search(searchFilter).pipe(
|
||||
map(group => group.series),
|
||||
map(items => seriesSettings.compareFn(items, searchFilter)),
|
||||
map(series => series.filter(s => s.seriesId !== this.series.id)),
|
||||
);
|
||||
seriesSettings.fetchFn = (searchFilter: string) => {
|
||||
this.searchService.search(searchFilter);
|
||||
return this.searchService.searchResults$.pipe(
|
||||
map(group => group.series),
|
||||
map(items => seriesSettings.compareFn(items, searchFilter)),
|
||||
map(series => series.filter(s => s.seriesId !== this.series.id)),
|
||||
);
|
||||
}
|
||||
|
||||
seriesSettings.compareFn = (options: SearchResult[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.name, filter));
|
||||
|
@ -144,7 +147,8 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
if (series !== undefined) {
|
||||
return this.searchService.search(series.name).pipe(
|
||||
this.searchService.search(series.name)
|
||||
return this.searchService.searchResults$.pipe(
|
||||
map(group => group.series), map(results => {
|
||||
seriesSettings.savedData = results.filter(s => s.seriesId === series.id);
|
||||
return seriesSettings;
|
||||
|
|
|
@ -13,6 +13,14 @@
|
|||
</div>
|
||||
<div class="dropdown" *ngIf="hasFocus">
|
||||
<ul class="list-group" role="listbox" id="dropdown">
|
||||
<ng-container *ngIf="seeMoreTemplate !== undefined && hasData && searchTerm.length > 0">
|
||||
<li class="list-group-item section-header"><h5 id="seemore-group">See More</h5></li>
|
||||
<ul class="list-group results">
|
||||
<li class="list-group-item">
|
||||
<ng-container [ngTemplateOutlet]="seeMoreTemplate" [ngTemplateOutletContext]="{ $implicit: searchTerm }"></ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="seriesTemplate !== undefined && grouppedData.series.length > 0">
|
||||
<li class="list-group-item section-header"><h5 id="series-group">Series</h5></li>
|
||||
<ul class="list-group results" role="group" aria-describedby="series-group">
|
||||
|
@ -109,7 +117,6 @@
|
|||
<ng-container [ngTemplateOutlet]="noResultsTemplate"></ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -58,6 +58,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
|||
@ContentChild('personTemplate') personTemplate: TemplateRef<any> | undefined;
|
||||
@ContentChild('genreTemplate') genreTemplate!: TemplateRef<any>;
|
||||
@ContentChild('noResultsTemplate') noResultsTemplate!: TemplateRef<any>;
|
||||
@ContentChild('seeMoreTemplate') seeMoreTemplate!: TemplateRef<any>;
|
||||
@ContentChild('libraryTemplate') libraryTemplate!: TemplateRef<any>;
|
||||
@ContentChild('readingListTemplate') readingListTemplate!: TemplateRef<any>;
|
||||
@ContentChild('fileTemplate') fileTemplate!: TemplateRef<any>;
|
||||
|
@ -77,7 +78,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
get hasData() {
|
||||
return !(this.noResultsTemplate != undefined && !this.grouppedData.persons.length && !this.grouppedData.collections.length
|
||||
return !(!this.grouppedData.persons.length && !this.grouppedData.collections.length
|
||||
&& !this.grouppedData.series.length && !this.grouppedData.persons.length && !this.grouppedData.tags.length && !this.grouppedData.genres.length && !this.grouppedData.libraries.length
|
||||
&& !this.grouppedData.files.length && !this.grouppedData.chapters.length);
|
||||
}
|
||||
|
@ -179,6 +180,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
open(event?: FocusEvent) {
|
||||
// TODO: Supress showing the groupped results when on the search page directly
|
||||
this.hasFocus = true;
|
||||
this.focusChanged.emit(this.hasFocus);
|
||||
this.cdRef.markForCheck();
|
||||
|
|
|
@ -192,7 +192,7 @@ NZ0ZV4zm4/L1dfnYNCrjTFq9G03rmj5D+Y4i0OHuL3GFPJytaM54AAAAAElFTkSuQmCC
|
|||
[minQueryLength]="2"
|
||||
initialValue=""
|
||||
placeholder="Search…"
|
||||
[grouppedData]="searchResults"
|
||||
[grouppedData]="(search$ | async) || emptyResult"
|
||||
(inputChanged)="onChangeSearch($event)"
|
||||
(clearField)="clearSearch()"
|
||||
(focusChanged)="focusUpdate($event)"
|
||||
|
@ -301,6 +301,9 @@ NZ0ZV4zm4/L1dfnYNCrjTFq9G03rmj5D+Y4i0OHuL3GFPJytaM54AAAAAElFTkSuQmCC
|
|||
No results found
|
||||
</ng-template>
|
||||
|
||||
<ng-template #seeMoreTemplate let-query>
|
||||
<a routerLink="/search" [queryParams]="{'query': query}">See more</a>
|
||||
</ng-template>
|
||||
</app-grouped-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { DOCUMENT } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { fromEvent, Subject } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, filter, takeUntil, tap } from 'rxjs/operators';
|
||||
import { fromEvent, Observable, of, Subject } from 'rxjs';
|
||||
import { catchError, debounceTime, distinctUntilChanged, filter, startWith, takeUntil, tap } from 'rxjs/operators';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { MangaFile } from 'src/app/_models/manga-file';
|
||||
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||
|
@ -16,7 +16,6 @@ import { SearchResult } from '../../_models/search-result';
|
|||
import { SearchResultGroup } from '../../_models/search/search-result-group';
|
||||
import { AccountService } from '../../_services/account.service';
|
||||
import { ImageService } from '../../_services/image.service';
|
||||
import { LibraryService } from '../../_services/library.service';
|
||||
import { NavService } from '../../_services/nav.service';
|
||||
|
||||
@Component({
|
||||
|
@ -32,7 +31,8 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
|||
isLoading = false;
|
||||
debounceTime = 300;
|
||||
imageStyles = {width: '24px', 'margin-top': '5px'};
|
||||
searchResults: SearchResultGroup = new SearchResultGroup();
|
||||
emptyResult: SearchResultGroup = new SearchResultGroup();
|
||||
search$!: Observable<SearchResultGroup>;
|
||||
searchTerm = '';
|
||||
customFilter: (items: SearchResult[], query: string) => SearchResult[] = (items: SearchResult[], query: string) => {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
|
@ -53,8 +53,23 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
|||
|
||||
constructor(public accountService: AccountService, private router: Router, public navService: NavService,
|
||||
public imageService: ImageService, @Inject(DOCUMENT) private document: Document,
|
||||
private scrollService: ScrollService, private searchService: SearchService, private readonly cdRef: ChangeDetectorRef) {
|
||||
private scrollService: ScrollService, public searchService: SearchService, private readonly cdRef: ChangeDetectorRef) {
|
||||
this.scrollElem = this.document.body;
|
||||
|
||||
this.search$ = this.searchService.searchResults$.pipe(
|
||||
startWith(new SearchResultGroup()),
|
||||
takeUntil(this.onDestroy),
|
||||
tap((_) => {
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
}),
|
||||
catchError(() => {
|
||||
this.isLoading = false;
|
||||
this.searchTerm = '';
|
||||
this.cdRef.markForCheck();
|
||||
return of();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -108,18 +123,8 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
|||
onChangeSearch(val: string) {
|
||||
this.isLoading = true;
|
||||
this.searchTerm = val.trim();
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.searchService.search(val.trim()).pipe(takeUntil(this.onDestroy)).subscribe(results => {
|
||||
this.searchResults = results;
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
}, err => {
|
||||
this.searchResults.reset();
|
||||
this.isLoading = false;
|
||||
this.searchTerm = '';
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
this.searchService.search(val);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
goTo(queryParamName: string, filter: any) {
|
||||
|
@ -172,7 +177,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
|||
clearSearch() {
|
||||
this.searchViewRef.clear();
|
||||
this.searchTerm = '';
|
||||
this.searchResults = new SearchResultGroup();
|
||||
this.searchService.search('');
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
|
19
UI/Web/src/app/search/search.module.ts
Normal file
19
UI/Web/src/app/search/search.module.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SearchComponent } from './search/search.component';
|
||||
import { SearchRoutingModule } from './search.router.module';
|
||||
import { CardsModule } from '../cards/cards.module';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
SearchComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SearchRoutingModule,
|
||||
CardsModule
|
||||
]
|
||||
})
|
||||
export class SearchModule { }
|
17
UI/Web/src/app/search/search.router.module.ts
Normal file
17
UI/Web/src/app/search/search.router.module.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { SearchComponent } from './search/search.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: SearchComponent
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes), ],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class SearchRoutingModule { }
|
13
UI/Web/src/app/search/search/search.component.html
Normal file
13
UI/Web/src/app/search/search/search.component.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<h3>Search Results: {{searchService.searchTerm$ | async}}</h3>
|
||||
<app-card-detail-layout
|
||||
[isLoading]="isLoading"
|
||||
[items]="(series$ | async) || []"
|
||||
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [] [suppressLibraryLink]="false" (reload)="loadPage()"[allowSelection]="false"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
<!-- [pagination]="pagination"
|
||||
[trackByIdentity]="trackByIdentity"
|
||||
[refresh]="refresh"-->
|
0
UI/Web/src/app/search/search/search.component.scss
Normal file
0
UI/Web/src/app/search/search/search.component.scss
Normal file
82
UI/Web/src/app/search/search/search.component.ts
Normal file
82
UI/Web/src/app/search/search/search.component.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { map, Observable, Subject, switchMap, takeUntil } from 'rxjs';
|
||||
import { SearchResult } from 'src/app/_models/search-result';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { SearchService } from 'src/app/_services/search.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search',
|
||||
templateUrl: './search.component.html',
|
||||
styleUrls: ['./search.component.scss']
|
||||
})
|
||||
export class SearchComponent implements OnInit, OnDestroy {
|
||||
|
||||
isLoading: boolean = false;
|
||||
originalQueryString: string = '';
|
||||
series$!: Observable<Series[]>;
|
||||
|
||||
private onDestroy: Subject<void> = new Subject();
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, public searchService: SearchService,) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const queryString = this.route.snapshot.queryParamMap.get('query');
|
||||
console.log('query: ', queryString)
|
||||
if (queryString === undefined || queryString === null || queryString === '') {
|
||||
this.router.navigateByUrl('/libraries');
|
||||
return;
|
||||
}
|
||||
this.originalQueryString = queryString;
|
||||
|
||||
// const searchResults$ = this.searchService.searchTerm$.pipe(
|
||||
// takeUntil(this.onDestroy),
|
||||
// switchMap(() =>)
|
||||
|
||||
// );
|
||||
|
||||
this.series$ = this.searchService.searchResults$.pipe(
|
||||
takeUntil(this.onDestroy),
|
||||
map(g => g.series.map(s => {
|
||||
return {
|
||||
id: s.seriesId,
|
||||
sortName: s.sortName,
|
||||
libraryName: s.libraryName,
|
||||
libraryId: s.libraryId,
|
||||
localizedName: s.localizedName,
|
||||
name: s.name,
|
||||
originalName: s.originalName,
|
||||
format: s.format,
|
||||
volumes: [],
|
||||
pages: 0,
|
||||
pagesRead: 0,
|
||||
userRating: 0,
|
||||
userReview: '',
|
||||
coverImageLocked: false,
|
||||
sortNameLocked: false,
|
||||
localizedNameLocked: false,
|
||||
nameLocked: false,
|
||||
created: '',
|
||||
latestReadDate: '',
|
||||
lastChapterAdded: '',
|
||||
lastFolderScanned: '',
|
||||
wordCount: 0,
|
||||
minHoursToRead: 0,
|
||||
maxHoursToRead: 0,
|
||||
avgHoursToRead: 0,
|
||||
folderPath: '',
|
||||
};
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
loadPage() {}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue