Added a minor loading animation when searching.

This commit is contained in:
Tom
2025-06-25 14:49:05 +00:00
parent 3326b7c589
commit 7875c5407c
3 changed files with 63 additions and 25 deletions

View File

@ -30,7 +30,8 @@
padding: 20px;
}
.results-error img, .results-end img {
.results-error img,
.results-end img {
vertical-align: middle;
}
@ -40,4 +41,32 @@
.filter-warning {
filter: brightness(0) saturate(100%) invert(52%) sepia(95%) saturate(2039%) hue-rotate(3deg) brightness(106%) contrast(102%);
}
.loading {
width: fit-content;
font-weight: bold;
font-family: monospace;
font-size: 25px;
align-self: center;
padding: 5px;
margin: 20px;
background: radial-gradient(circle closest-side, #000 94%, #0000) right/calc(200% - 1em) 100%;
animation: l24 1s infinite alternate linear;
}
.loading::before {
content: "Loading...";
line-height: 1em;
color: #0000;
background: inherit;
background-image: radial-gradient(circle closest-side, #fff 94%, #000);
-webkit-background-clip: text;
background-clip: text;
}
@keyframes l24 {
100% {
background-position: left
}
}

View File

@ -1,21 +1,24 @@
<div class="search-content">
<search-box
(searchOutput)="search.next($event)"
(filtersOutput)="filters.next($event)" />
<div #scrollbar
class="search-content">
<search-box (searchOutput)="search.next($event)"
(filtersOutput)="filters.next($event)" />
<div class="results-box"
(scroll)="onResultsScroll($event)">
@for (result of results; track $index) {
<media-search-item class="result-item"
[media]="result" />
}
@if (busy()) {
<div class="loading"></div>
}
@if (searchError() != null) {
<p class="results-error">
<img src="/icons/error_icon.svg"
alt="error icon"
class="filter-error" />
{{searchError()}}
</p>
}
<p class="results-error">
<img src="/icons/error_icon.svg"
alt="error icon"
class="filter-error" />
{{searchError()}}
</p>
}
@if (endOfResults()) {
<div class="results-end">
<img src="/icons/warning_icon.svg"

View File

@ -1,9 +1,7 @@
import { Component, inject, NgZone, OnDestroy, signal } from '@angular/core';
import { AuthService } from '../../services/auth/auth.service';
import { Component, ElementRef, inject, NgZone, OnDestroy, signal, ViewChild } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { BehaviorSubject, combineLatest, distinctUntilChanged, filter, scan, Subscription, tap, throttleTime } from 'rxjs';
import { BehaviorSubject, combineLatest, debounceTime, distinctUntilChanged, filter, scan, Subscription, throttleTime } from 'rxjs';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { RedirectionService } from '../../services/redirection.service';
import { BookSearchResultDto } from '../../shared/dto/book-search-result.dto';
import { MediaSearchItemComponent } from '../media-search-item/media-search-item.component';
import { SearchBoxComponent } from "../search-box/search-box.component";
@ -21,17 +19,18 @@ import { SearchContextDto } from '../../shared/dto/search-context.dto';
styleUrl: './add-new-page.component.css'
})
export class AddNewPageComponent implements OnDestroy {
private readonly _auth = inject(AuthService);
private readonly _http = inject(HttpClient);
private readonly _redirect = inject(RedirectionService);
private readonly _subscriptions: Subscription[] = [];
private readonly _zone = inject(NgZone);
@ViewChild('scrollbar') private readonly searchContentRef: ElementRef<Element> = {} as ElementRef;
search = new BehaviorSubject<string>('');
filters = new BehaviorSubject<SearchContextDto>(new SearchContextDto());
page = new BehaviorSubject<number>(0);
results: BookSearchResultDto[] = [];
resultsPerPage = signal<number>(10);
busy = signal<boolean>(false);
endOfResults = signal<boolean>(false);
searchError = signal<string | null>(null);
@ -45,13 +44,14 @@ export class AddNewPageComponent implements OnDestroy {
),
filters: this.filters,
page: this.page.pipe(
throttleTime(1500, undefined, { leading: true, trailing: true }),
throttleTime(1500, undefined, { leading: false, trailing: true }),
),
}).pipe(
filter(entry => entry.search!.length > 1),
throttleTime(1000, undefined, { leading: false, trailing: true }),
debounceTime(1000),
scan((acc, next) => {
// New searches means resetting to page 0.
// Ignore if user is busy loading new pages.
if (acc.search != next.search) {
this.results = [];
return {
@ -65,8 +65,10 @@ export class AddNewPageComponent implements OnDestroy {
return acc;
}
// Ignore further searching on the same search term.
if (this.endOfResults()) {
// Ignore further page searching if:
// - there are no more results;
// - user is still busy loading new pages.
if (this.endOfResults() || this.busy()) {
return {
...next,
page: -1,
@ -79,10 +81,12 @@ export class AddNewPageComponent implements OnDestroy {
page: Math.min(acc.page + 1, next.page),
};
}),
distinctUntilChanged(),
filter(entry => entry.page >= 0),
tap(_ => this.endOfResults.set(false)),
distinctUntilChanged(),
).subscribe((entry) => {
this.busy.set(true);
this.endOfResults.set(false);
this.searchContentRef.nativeElement.scrollTop = 0;
this._http.get('/api/providers/search',
{
params: {
@ -99,8 +103,10 @@ export class AddNewPageComponent implements OnDestroy {
if (results.length < this.resultsPerPage()) {
this.endOfResults.set(true);
}
this.busy.set(false);
},
error: (err) => {
this.busy.set(false);
if (err instanceof HttpErrorResponse) {
if (err.status == 400) {
this.searchError.set('Something went wrong when Google received the request.');
@ -128,7 +134,7 @@ export class AddNewPageComponent implements OnDestroy {
const limit = scroll.scrollHeight - scroll.clientHeight;
// Prevent page changes when:
// - new search is happening (emptying results);
// - new search is happening (emptied results);
// - still scrolling through current content.
if (scroll.scrollTop == 0 || scroll.scrollTop < limit - 25) {
return;