diff --git a/src/app/channel-tile/channel-tile.component.html b/src/app/channel-tile/channel-tile.component.html index 0742023b..ae4b9b2e 100644 --- a/src/app/channel-tile/channel-tile.component.html +++ b/src/app/channel-tile/channel-tile.component.html @@ -4,12 +4,12 @@ }" [ngbTooltip]="channel?.name" triggers="hover" (click)="click()" class="channel d-inline-flex p-2 align-items-center" (contextmenu)="onRightClick($event)">
- +
{{ channel?.name }}
-
{{ getSourceName() }}
+
{{ sourceName }}
-
diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index b8fdb432..974e89c4 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -3,6 +3,7 @@ import { Component, ElementRef, HostListener, + NgZone, OnDestroy, ViewChild, } from "@angular/core"; @@ -104,12 +105,19 @@ export class HomeComponent implements AfterViewInit, OnDestroy { window.scrollTo({ top: 0, behavior: "smooth" }); } + // Stable identity for the channel grid so re-search/re-sort/load-more reuses + // existing tile DOM instead of destroying and rebuilding every tile. + trackByChannel(_index: number, channel: Channel): string | number { + return channel.id != null ? `${channel.media_type}-${channel.id}` : _index; + } + constructor( private router: Router, public memory: MemoryService, public toast: ToastrService, private error: ErrorService, private modal: NgbModal, + private zone: NgZone, ) { this.getSources(); } @@ -285,29 +293,40 @@ export class HomeComponent implements AfterViewInit, OnDestroy { this.loading = false; } - checkScrollTop() { - const scrollPosition = - window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; - this.showScrollTop = scrollPosition > 300; - } - - async checkScrollEnd() { + // Scroll runs OUTSIDE the Angular zone and is throttled with rAF so that the + // dozens of scroll events per second don't each trigger an app-wide change + // detection pass (the main cause of scroll/mouse stutter while browsing). + // We only re-enter the zone when something the UI actually binds to changes. + private scrollTicking = false; + private onScroll = () => { + if (this.scrollTicking) return; + this.scrollTicking = true; + requestAnimationFrame(() => { + this.handleScroll(); + this.scrollTicking = false; + }); + }; + + private handleScroll() { + const scrollTop = window.scrollY || document.documentElement.scrollTop || 0; + const showScrollTop = scrollTop > 300; + if (showScrollTop !== this.showScrollTop) { + // Re-enter Angular only when the scroll-to-top button visibility flips. + this.zone.run(() => (this.showScrollTop = showScrollTop)); + } if (this.reachedMax === true || this.loading === true) return; const scrollHeight = document.documentElement.scrollHeight; - const scrollTop = window.scrollY || document.documentElement.scrollTop; const clientHeight = window.innerHeight || document.documentElement.clientHeight; if (scrollTop + clientHeight >= scrollHeight * 0.75) { - await this.loadMore(); + // Re-enter Angular so the new page of channels renders. + this.zone.run(() => this.loadMore()); } } - @HostListener("window:scroll", ["$event"]) - async scroll(event: any) { - this.checkScrollTop(); - await this.checkScrollEnd(); - } - ngAfterViewInit(): void { + this.zone.runOutsideAngular(() => { + window.addEventListener("scroll", this.onScroll, { passive: true }); + }); this.addEvents().then((_) => _); this.subscriptions.push( fromEvent(this.search.nativeElement, "keyup") @@ -630,6 +649,7 @@ export class HomeComponent implements AfterViewInit, OnDestroy { } ngOnDestroy() { + window.removeEventListener("scroll", this.onScroll); this.subscriptions.forEach((x) => x.unsubscribe()); }