faster smaller

This commit is contained in:
Andrey Kondratev
2025-11-09 18:48:57 +05:00
parent ca27a2b3f0
commit 6db48b16a7
11 changed files with 470 additions and 76 deletions

View File

@@ -46,6 +46,8 @@ class QuixoticApp {
private welcomePlaceholder!: HTMLElement;
private searchTimeout?: NodeJS.Timeout;
private currentVideos: VideoResult[] = [];
private loadingStartTime: number = 0;
private minLoadingDuration: number = 400; // Минимальное время показа спиннера (ms)
constructor() {
this.tg = (window as WindowWithTelegram).Telegram?.WebApp;
@@ -91,10 +93,13 @@ class QuixoticApp {
return;
}
// Set new timeout for search (500ms delay)
// Show loading spinner immediately for better UX feedback
this.showLoading();
// Set new timeout for search (300ms delay - best practice for instant search)
this.searchTimeout = setTimeout(() => {
this.search();
}, 500);
}, 300);
});
// Still handle Enter key for immediate search
@@ -148,14 +153,35 @@ class QuixoticApp {
}
const data: SearchResponse = await response.json();
// Ensure minimum loading time for better UX (prevents flashing)
const elapsedTime = Date.now() - this.loadingStartTime;
const remainingTime = Math.max(0, this.minLoadingDuration - elapsedTime);
if (remainingTime > 0) {
await new Promise(resolve => setTimeout(resolve, remainingTime));
}
this.displayResults(data.videos);
} catch (error) {
console.error('Search error:', error);
// Ensure minimum loading time even for errors
const elapsedTime = Date.now() - this.loadingStartTime;
const remainingTime = Math.max(0, this.minLoadingDuration - elapsedTime);
if (remainingTime > 0) {
await new Promise(resolve => setTimeout(resolve, remainingTime));
}
this.showNoResults();
}
}
private showLoading(): void {
// Record loading start time
this.loadingStartTime = Date.now();
// Clear any existing status messages
const existingMessage = document.querySelector('.tg-status-message');
if (existingMessage) {
@@ -201,8 +227,15 @@ class QuixoticApp {
this.noResults.classList.add('tg-hidden');
this.noResults.style.display = 'none';
this.results.innerHTML = videos.map(video => `
<div class='tg-list-item' onclick='app.convertVideo("${video.id}", "${this.escapeHtml(video.title)}", "${this.escapeHtml(video.url)}")'>
// Use DocumentFragment for better performance
const fragment = document.createDocumentFragment();
videos.forEach(video => {
const item = document.createElement('div');
item.className = 'tg-list-item';
item.dataset.videoId = video.id;
item.innerHTML = `
<div class='tg-list-item__content'>
<div class='tg-list-item__media'>
<img class='tg-list-item__thumbnail'
@@ -217,29 +250,35 @@ class QuixoticApp {
<div class='tg-list-item__subtitle'>${this.escapeHtml(video.channel)}</div>
</div>
</div>
</div>
`).join('');
// Add touch event listeners to prevent sticky states
this.results.querySelectorAll('.tg-list-item').forEach(item => {
const element = item as HTMLElement;
`;
// Use event listener instead of inline onclick
item.addEventListener('click', () => {
this.convertVideo(video.id, video.title, video.url);
});
// Reset visual state on touch end
element.addEventListener('touchend', () => {
item.addEventListener('touchend', () => {
setTimeout(() => {
element.blur();
element.style.background = '';
element.style.transform = '';
item.blur();
item.style.background = '';
item.style.transform = '';
}, 100);
});
}, { passive: true });
// Also handle mouse leave for desktop
element.addEventListener('mouseleave', () => {
element.style.background = '';
element.style.transform = '';
item.addEventListener('mouseleave', () => {
item.style.background = '';
item.style.transform = '';
});
fragment.appendChild(item);
});
// Clear and append once
this.results.innerHTML = '';
this.results.appendChild(fragment);
// Lazy load images with IntersectionObserver
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
@@ -281,8 +320,8 @@ class QuixoticApp {
const performer = video?.channel || 'Unknown Artist';
const thumbnail = video?.thumbnail || '';
// Find the clicked element by looking for the one that contains this videoId
const videoElement = document.querySelector(`[onclick*="${videoId}"]`) as HTMLElement;
// Find the clicked element using data attribute
const videoElement = document.querySelector(`[data-video-id="${videoId}"]`) as HTMLElement;
if (videoElement) {
// Force blur to reset any active/focus states
videoElement.blur();