/* eslint-disable @typescript-eslint/member-ordering */ import { AfterViewInit, Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild, inject, input, output, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { firstValueFrom } from 'rxjs'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { lucideChevronDown, lucideImage, lucideSearch, lucideX } from '@ng-icons/lucide'; import { KlipyGif, KlipyService } from '../../application/services/klipy.service'; import type { RoomSignalSourceInput } from '../../../server-directory'; import { ChatImageProxyFallbackDirective } from '../chat-image-proxy-fallback.directive'; import { ViewportService } from '../../../../core/platform'; const KLIPY_CARD_MIN_WIDTH = 140; const KLIPY_CARD_MAX_WIDTH = 248; const KLIPY_CARD_MIN_HEIGHT = 104; const KLIPY_CARD_MAX_HEIGHT = 220; const KLIPY_CARD_FALLBACK_SIZE = 160; @Component({ selector: 'app-klipy-gif-picker', standalone: true, imports: [ CommonModule, FormsModule, NgIcon, ChatImageProxyFallbackDirective ], viewProviders: [ provideIcons({ lucideChevronDown, lucideImage, lucideSearch, lucideX }) ], templateUrl: './klipy-gif-picker.component.html' }) export class KlipyGifPickerComponent implements OnInit, AfterViewInit, OnDestroy { readonly signalSource = input(null); readonly gifSelected = output(); readonly closed = output(); @ViewChild('searchInput') searchInput?: ElementRef; private readonly klipy = inject(KlipyService); private readonly viewport = inject(ViewportService); readonly isMobile = this.viewport.isMobile; private currentPage = 1; private searchTimer: ReturnType | null = null; private requestId = 0; searchQuery = ''; results = signal([]); loading = signal(false); errorMessage = signal(''); hasNext = signal(false); ngOnInit(): void { void this.loadResults(true); } ngAfterViewInit(): void { requestAnimationFrame(() => { this.searchInput?.nativeElement.focus(); this.searchInput?.nativeElement.select(); }); } ngOnDestroy(): void { this.clearSearchTimer(); } @HostListener('document:keydown.escape') onEscape(): void { this.close(); } onSearchQueryChanged(query: string): void { this.searchQuery = query; this.clearSearchTimer(); this.searchTimer = setTimeout(() => { void this.loadResults(true); }, 250); } retry(): void { void this.loadResults(true); } async loadMore(): Promise { if (this.loading() || !this.hasNext()) return; this.currentPage += 1; await this.loadResults(false); } selectGif(gif: KlipyGif): void { this.gifSelected.emit(gif); } close(): void { this.closed.emit(undefined); } gifCardHeight(gif: KlipyGif): number { return this.getGifCardSize(gif).height; } private async loadResults(reset: boolean): Promise { if (reset) { this.currentPage = 1; } const requestId = ++this.requestId; this.loading.set(true); this.errorMessage.set(''); try { const response = await firstValueFrom( this.klipy.searchGifs(this.searchQuery, this.currentPage, undefined, this.signalSource()) ); if (requestId !== this.requestId) return; this.results.set( reset ? response.results : this.mergeResults(this.results(), response.results) ); this.hasNext.set(response.hasNext); } catch (error) { if (requestId !== this.requestId) return; this.errorMessage.set( error instanceof Error ? error.message : 'Failed to load GIFs from KLIPY.' ); if (reset) { this.results.set([]); } this.hasNext.set(false); } finally { if (requestId === this.requestId) { this.loading.set(false); } } } private mergeResults(existing: KlipyGif[], incoming: KlipyGif[]): KlipyGif[] { const seen = new Set(existing.map((gif) => gif.id)); const merged = [...existing]; for (const gif of incoming) { if (seen.has(gif.id)) continue; merged.push(gif); seen.add(gif.id); } return merged; } private clearSearchTimer(): void { if (this.searchTimer) { clearTimeout(this.searchTimer); this.searchTimer = null; } } private getGifCardSize(gif: KlipyGif): { width: number; height: number } { if (gif.width <= 0 || gif.height <= 0) { return { width: KLIPY_CARD_FALLBACK_SIZE, height: KLIPY_CARD_FALLBACK_SIZE }; } const maxScale = Math.min( KLIPY_CARD_MAX_WIDTH / gif.width, KLIPY_CARD_MAX_HEIGHT / gif.height ); const minScale = Math.max( KLIPY_CARD_MIN_WIDTH / gif.width, KLIPY_CARD_MIN_HEIGHT / gif.height ); const scale = minScale <= maxScale ? Math.min(maxScale, Math.max(minScale, 1)) : maxScale; const scaledWidth = Math.round(gif.width * scale); const scaledHeight = Math.round(gif.height * scale); return { width: Math.min(KLIPY_CARD_MAX_WIDTH, Math.max(KLIPY_CARD_MIN_WIDTH, scaledWidth)), height: Math.min(KLIPY_CARD_MAX_HEIGHT, Math.max(KLIPY_CARD_MIN_HEIGHT, scaledHeight)) }; } }