diff --git a/.gitignore b/.gitignore index 3f0844a..1da77d7 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,5 @@ Thumbs.db # Build dist/ -build/ \ No newline at end of file +build/ +!public diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..aef8223 --- /dev/null +++ b/public/index.html @@ -0,0 +1,40 @@ + + + + + + Quixotic Music + + + + +
+
+

<µ Quixotic

+

0948 8 A:0G09 +

+ +
+
+ + +
+
+ + + +
+ +
+ + +
+ + + + \ No newline at end of file diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000..fd4e686 --- /dev/null +++ b/public/script.js @@ -0,0 +1,161 @@ +class QuixoticApp { + constructor() { + this.tg = window.Telegram?.WebApp; + this.init(); + this.bindEvents(); + } + + init() { + if (this.tg) { + this.tg.ready(); + this.tg.expand(); + this.tg.MainButton.hide(); + } + + this.searchInput = document.getElementById('searchInput'); + this.searchBtn = document.getElementById('searchBtn'); + this.loading = document.getElementById('loading'); + this.results = document.getElementById('results'); + this.noResults = document.getElementById('noResults'); + } + + bindEvents() { + this.searchBtn.addEventListener('click', () => this.search()); + this.searchInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + this.search(); + } + }); + } + + async search() { + const query = this.searchInput.value.trim(); + if (!query) return; + + this.showLoading(); + + try { + const response = await fetch('/api/search', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + userId: this.tg?.initDataUnsafe?.user?.id || 'demo' + }) + }); + + if (!response.ok) { + throw new Error('Search failed'); + } + + const data = await response.json(); + this.displayResults(data.videos); + } catch (error) { + console.error('Search error:', error); + this.showNoResults(); + } + } + + showLoading() { + this.loading.classList.remove('hidden'); + this.results.classList.add('hidden'); + this.noResults.classList.add('hidden'); + this.searchBtn.disabled = true; + } + + hideLoading() { + this.loading.classList.add('hidden'); + this.searchBtn.disabled = false; + } + + displayResults(videos) { + this.hideLoading(); + + if (!videos || videos.length === 0) { + this.showNoResults(); + return; + } + + this.results.innerHTML = videos.map(video => ` +
+ ${this.escapeHtml(video.title)} +
+
${this.escapeHtml(video.title)}
+
${this.escapeHtml(video.channel)}
+
${this.formatDuration(video.duration)}
+
+
+ `).join(''); + + this.results.classList.remove('hidden'); + this.noResults.classList.add('hidden'); + } + + showNoResults() { + this.hideLoading(); + this.results.classList.add('hidden'); + this.noResults.classList.remove('hidden'); + } + + async convertVideo(videoId, title) { + const videoElement = event.currentTarget; + videoElement.classList.add('converting'); + + try { + const response = await fetch('/api/convert', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + videoId, + title, + userId: this.tg?.initDataUnsafe?.user?.id || 'demo' + }) + }); + + if (!response.ok) { + throw new Error('Conversion failed'); + } + + const data = await response.json(); + + if (this.tg) { + this.tg.sendData(JSON.stringify({ + action: 'send_audio', + audioUrl: data.audioUrl, + title: title + })); + } else { + // Fallback for testing without Telegram + window.open(data.audioUrl, '_blank'); + } + } catch (error) { + console.error('Conversion error:', error); + if (this.tg) { + this.tg.showAlert('H81:0 ?@8 :>=25@B0F88. >?@>1C9B5 5I5 @07.'); + } else { + alert('H81:0 ?@8 :>=25@B0F88. >?@>1C9B5 5I5 @07.'); + } + } finally { + videoElement.classList.remove('converting'); + } + } + + formatDuration(seconds) { + if (!seconds) return ''; + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + } + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +} + +const app = new QuixoticApp(); \ No newline at end of file diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..a3d4bdc --- /dev/null +++ b/public/style.css @@ -0,0 +1,206 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + background: var(--tg-theme-bg-color, #f0f0f0); + color: var(--tg-theme-text-color, #000); + line-height: 1.6; +} + +.container { + max-width: 100vw; + margin: 0 auto; + padding: 20px 16px; +} + +header { + text-align: center; + margin-bottom: 30px; +} + +header h1 { + font-size: 2rem; + margin-bottom: 8px; + color: var(--tg-theme-button-color, #007AFF); +} + +header p { + color: var(--tg-theme-hint-color, #666); + font-size: 0.9rem; +} + +.search-section { + margin-bottom: 30px; +} + +.search-container { + display: flex; + gap: 10px; + background: var(--tg-theme-secondary-bg-color, #fff); + border-radius: 12px; + padding: 4px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +#searchInput { + flex: 1; + padding: 12px 16px; + border: none; + background: transparent; + font-size: 1rem; + color: var(--tg-theme-text-color, #000); + outline: none; +} + +#searchInput::placeholder { + color: var(--tg-theme-hint-color, #999); +} + +#searchBtn { + padding: 12px 16px; + background: var(--tg-theme-button-color, #007AFF); + color: var(--tg-theme-button-text-color, #fff); + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; + transition: opacity 0.2s; +} + +#searchBtn:hover { + opacity: 0.8; +} + +#searchBtn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.loading { + text-align: center; + padding: 40px 20px; +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid var(--tg-theme-hint-color, #ddd); + border-top: 3px solid var(--tg-theme-button-color, #007AFF); + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 16px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.results-container { + display: grid; + gap: 16px; +} + +.video-item { + display: flex; + background: var(--tg-theme-secondary-bg-color, #fff); + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; + cursor: pointer; +} + +.video-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); +} + +.video-thumbnail { + width: 120px; + height: 90px; + object-fit: cover; + flex-shrink: 0; +} + +.video-info { + padding: 16px; + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.video-title { + font-weight: 600; + font-size: 0.95rem; + line-height: 1.4; + margin-bottom: 8px; + color: var(--tg-theme-text-color, #000); + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.video-channel { + font-size: 0.85rem; + color: var(--tg-theme-hint-color, #666); + margin-bottom: 4px; +} + +.video-duration { + font-size: 0.8rem; + color: var(--tg-theme-hint-color, #888); + background: rgba(0, 0, 0, 0.1); + padding: 2px 6px; + border-radius: 4px; + align-self: flex-start; +} + +.no-results { + text-align: center; + padding: 60px 20px; + color: var(--tg-theme-hint-color, #666); +} + +.hidden { + display: none !important; +} + +.converting { + opacity: 0.6; + pointer-events: none; +} + +.converting::after { + content: ">=25@B0F8O..."; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--tg-theme-button-color, #007AFF); + color: var(--tg-theme-button-text-color, #fff); + padding: 8px 12px; + border-radius: 6px; + font-size: 0.8rem; +} + +@media (max-width: 480px) { + .video-item { + flex-direction: column; + } + + .video-thumbnail { + width: 100%; + height: 180px; + } + + .container { + padding: 16px 12px; + } +} \ No newline at end of file