Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/app/channel-tile/channel-tile.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
}" [ngbTooltip]="channel?.name" triggers="hover" (click)="click()" class="channel d-inline-flex p-2 align-items-center"
(contextmenu)="onRightClick($event)">
<div class="image-container me-2">
<img (error)="showImage = false" class="channel-image" *ngIf="channel?.image && showImage"
src="{{ channel?.image }}" />
<img (error)="onError($event)" class="channel-image" *ngIf="channel?.image && showImage"
loading="lazy" decoding="async" src="{{ channel?.image }}" />
</div>
<div class="channel-title-container d-flex flex-column justify-content-center">
<div class="channel-title">{{ channel?.name }}</div>
<div class="channel-source">{{ getSourceName() }}</div>
<div class="channel-source">{{ sourceName }}</div>
</div>
<div class="ms-auto d-flex flex-column justify-content-center align-items-center h-100">
<svg *ngIf="channel?.tv_archive === true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
Expand Down
29 changes: 23 additions & 6 deletions src/app/channel-tile/channel-tile.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Input,
Expand Down Expand Up @@ -36,6 +38,7 @@ import { writeText } from "@tauri-apps/plugin-clipboard-manager";
selector: "app-channel-tile",
templateUrl: "./channel-tile.component.html",
styleUrl: "./channel-tile.component.css",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChannelTileComponent implements OnDestroy, AfterViewInit {
constructor(
Expand All @@ -46,8 +49,21 @@ export class ChannelTileComponent implements OnDestroy, AfterViewInit {
private el: ElementRef,
private renderer: Renderer2,
private download: DownloadService,
private cdr: ChangeDetectorRef,
) { }
@Input() channel?: Channel;
private _channel?: Channel;
// Precomputed once when the channel input is set, so the template doesn't
// call a method binding on every change-detection cycle.
sourceName = "";
@Input() set channel(value: Channel | undefined) {
this._channel = value;
this.sourceName = value?.source_id
? (this.memory.Sources.get(value.source_id)?.name ?? "")
: "";
}
get channel(): Channel | undefined {
return this._channel;
}
@Input() id!: number;
@Input() viewMode: number = 0;
@ViewChild(MatMenuTrigger, { static: true }) matMenuTrigger!: MatMenuTrigger;
Expand Down Expand Up @@ -125,6 +141,7 @@ export class ChannelTileComponent implements OnDestroy, AfterViewInit {
if (!file) return;
}
this.starting = true;
this.cdr.markForCheck();
this.memory.SetFocus.next(this.id);
try {
await invoke("play", { channel: this.channel, record: record, recordPath: file });
Expand All @@ -136,6 +153,7 @@ export class ChannelTileComponent implements OnDestroy, AfterViewInit {
this.error.handleError(e);
});
this.starting = false;
this.cdr.markForCheck();
}

onRightClick(event: MouseEvent) {
Expand All @@ -148,11 +166,13 @@ export class ChannelTileComponent implements OnDestroy, AfterViewInit {
this.menuTopLeftPosition.y = event.clientY;
if (this.memory.currentContextMenu?.menuOpen) this.memory.currentContextMenu.closeMenu();
this.memory.currentContextMenu = this.matMenuTrigger;
this.cdr.markForCheck();
this.matMenuTrigger.openMenu();
}

onError(event: Event) {
this.showImage = false;
this.cdr.markForCheck();
}

async favorite() {
Expand All @@ -175,6 +195,7 @@ export class ChannelTileComponent implements OnDestroy, AfterViewInit {
this.fade = false;
this.toastr.success(msg);
}
this.cdr.markForCheck();
} catch (e) {
this.error.handleError(e, `Failed to add/remove "${this.channel?.name}" to/from favorites`);
}
Expand Down Expand Up @@ -206,6 +227,7 @@ export class ChannelTileComponent implements OnDestroy, AfterViewInit {
this.channel!.hidden = hide;
this.fade = this.viewMode == ViewMode.Hidden ? !hide : hide;
this.toastr.success(`${msg} (updates on reload)`);
this.cdr.markForCheck();
} catch (e) {
this.error.handleError(e, `Failed to hide/unhide "${this.channel?.name}"`);
}
Expand Down Expand Up @@ -235,11 +257,6 @@ export class ChannelTileComponent implements OnDestroy, AfterViewInit {
);
}

getSourceName(): string {
if (!this.channel?.source_id) return "";
return this.memory.Sources.get(this.channel.source_id)?.name || "";
}

async showEPGModal() {
try {
let data: EPG[] = await invoke("get_epg", { channel: this.channel });
Expand Down
3 changes: 2 additions & 1 deletion src/app/home/home.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ <h4 *ngIf="this.nodeStack.hasNodes()" class="ms-2 mb-0">
</h4>
</div>
<div class="row gy-3" [@fade]="channelsVisible ? 'visible' : 'hidden'">
<app-channel-tile [attr.id]="i == 0 ? 'first' : null" *ngFor="let channel of channels; let i = index"
<app-channel-tile [attr.id]="i == 0 ? 'first' : null"
*ngFor="let channel of channels; let i = index; trackBy: trackByChannel"
class="col-lg-4 col-md-4" [id]="i" [channel]="channel" [viewMode]="viewType"></app-channel-tile>
</div>
</div>
Expand Down
50 changes: 35 additions & 15 deletions src/app/home/home.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Component,
ElementRef,
HostListener,
NgZone,
OnDestroy,
ViewChild,
} from "@angular/core";
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -630,6 +649,7 @@ export class HomeComponent implements AfterViewInit, OnDestroy {
}

ngOnDestroy() {
window.removeEventListener("scroll", this.onScroll);
this.subscriptions.forEach((x) => x.unsubscribe());
}

Expand Down