From 3950d91070e8fa0a971f756f9bca3c3227d5377b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enes=20Yak=C4=B1=C5=9Ft=C4=B1r?= Date: Sat, 19 Sep 2026 05:14:11 +0300 Subject: [PATCH] feat(v1.5.0): unified observer, sleep timer, double-tap seek, voice assistant commands & qr handoff --- client.js | 710 ++++++++++++++++++++++++++++++++++++++++++++++++--- main.js | 98 ++++++- package.json | 8 +- style.css | 243 ++++++++++++++++++ 4 files changed, 1022 insertions(+), 37 deletions(-) diff --git a/client.js b/client.js index ee3cbc9..3762a33 100644 --- a/client.js +++ b/client.js @@ -31,7 +31,10 @@ if (typeof window !== 'undefined') { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', neuterIncompatible); } - setInterval(neuterIncompatible, 1000); + var neuterInterval = setInterval(neuterIncompatible, 1000); + setTimeout(function () { + clearInterval(neuterInterval); + }, 5000); } catch (e) {} } @@ -951,14 +954,12 @@ try { } else if (type === 'checkbox') { inp.setAttribute('aria-label', 'Seçim Kutusu'); } else if (type === 'password') { - inp.setAttribute('aria-label', 'Şifre'); + inp.setAttribute('aria-label', 'Şifre'); } } }); } - setInterval(autoFixDOM, 1000); - // 3. Screen-Reader & Turkish Q Optimized Shortcuts & Player Bar (v1.4.1) function toggleShortcutsModal() { var modal = document.getElementById('yaktube-shortcuts-dialog'); @@ -997,6 +998,8 @@ try { 'Alt + T veya TSinema / Tiyatro Modu' + 'Alt + I veya IHızlı Video Bilgisi & İstatistikler' + 'Alt + C veya CAltyazıları Aç / Kapat' + + 'Alt + U⏱️ Uyku Zamanlayıcısı (Sleep Timer)' + + 'Alt + Q📱 Telefonda Devam Et (QR Kod)' + 'Alt + . / Alt + ,Oynatma hızını artır / azalt' + 'Alt + S veya / (Shift+7)Arama penceresini aç' + 'Alt + V veya Shift + VSesli aramayı başlat' + @@ -1045,6 +1048,7 @@ try { } return null; } + window.getVideoElement = getVideoElement; function formatTime(seconds) { if (isNaN(seconds) || seconds < 0) return '0 saniye'; @@ -1143,6 +1147,493 @@ try { } } + // ========================================================================== + // YAKTUBE QUICK TOAST HELPER (v1.5.0) + // ========================================================================== + function showQuickToast(message, icon) { + var existing = document.getElementById('yaktube-quick-toast'); + if (existing) existing.remove(); + + var toast = document.createElement('div'); + toast.id = 'yaktube-quick-toast'; + toast.className = 'yaktube-quick-toast'; + toast.style.cssText = + 'position:fixed!important;top:80px!important;left:50%!important;transform:translateX(-50%)!important;' + + 'background:rgba(24,24,31,0.92)!important;border:1px solid #ff8f37!important;color:#fff!important;' + + 'padding:10px 22px!important;border-radius:30px!important;font-size:14px!important;font-weight:700!important;' + + 'box-shadow:0 10px 30px rgba(0,0,0,0.8)!important;z-index:999999!important;pointer-events:none!important;' + + 'backdrop-filter:blur(8px)!important;display:flex!important;align-items:center!important;gap:8px!important;'; + toast.innerHTML = (icon ? '' + icon + ' ' : '') + '' + message + ''; + document.body.appendChild(toast); + + setTimeout(function () { + if (toast && toast.parentNode) { + toast.style.opacity = '0'; + toast.style.transition = 'opacity 0.3s ease-out'; + setTimeout(function () { + if (toast.parentNode) toast.parentNode.removeChild(toast); + }, 300); + } + }, 2200); + } + + // ========================================================================== + // YAKTUBE SLEEP TIMER ENGINE (v1.5.0) + // ========================================================================== + var sleepTimer = { + countdownInterval: null, + targetTime: 0, + mode: null, + originalVolume: 1 + }; + + function clearSleepTimer() { + if (sleepTimer.countdownInterval) clearInterval(sleepTimer.countdownInterval); + sleepTimer.countdownInterval = null; + sleepTimer.targetTime = 0; + sleepTimer.mode = null; + + var badge = document.getElementById('yaktube-sleep-timer-badge'); + if (badge) badge.remove(); + var endListenerVideo = getVideoElement(); + if (endListenerVideo && endListenerVideo._sleepEndListener) { + endListenerVideo.removeEventListener('ended', endListenerVideo._sleepEndListener); + endListenerVideo._sleepEndListener = null; + } + } + + function setSleepTimer(modeMinutes) { + clearSleepTimer(); + var video = getVideoElement(); + if (!video) { + announce('Aktif video bulunamadı.'); + return; + } + + sleepTimer.mode = modeMinutes; + sleepTimer.originalVolume = video.volume || 1; + + if (modeMinutes === 'end') { + announce('Uyku zamanlayıcısı ayarlandı: Bu video bitince durdurulacak.', true); + showQuickToast('⏱️ Uyku: Video Bitince'); + video._sleepEndListener = function () { + video.pause(); + announce('Uyku zamanlayıcısı: Video bitti, oynatma durduruldu.', true); + clearSleepTimer(); + }; + video.addEventListener('ended', video._sleepEndListener, { once: true }); + updateSleepTimerBadge('Video Bitince'); + return; + } + + var mins = parseInt(modeMinutes, 10); + if (isNaN(mins) || mins <= 0) return; + + sleepTimer.targetTime = Date.now() + mins * 60 * 1000; + announce('Uyku zamanlayıcısı ' + mins + ' dakikaya ayarlandı.', true); + showQuickToast('⏱️ Uyku: ' + mins + ' Dakika'); + + function updateTick() { + var remainingMs = Math.max(0, sleepTimer.targetTime - Date.now()); + var remainingSec = Math.floor(remainingMs / 1000); + var m = Math.floor(remainingSec / 60); + var s = remainingSec % 60; + var display = (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s); + updateSleepTimerBadge(display); + + // Gentle volume fadeout in last 15 seconds + if (remainingSec <= 15 && remainingSec > 0 && video && !video.paused) { + var stepRatio = remainingSec / 15; + video.volume = Math.max(0, sleepTimer.originalVolume * stepRatio); + } + + if (remainingMs <= 0) { + clearSleepTimer(); + if (video) { + video.pause(); + video.volume = sleepTimer.originalVolume; + } + announce('Uyku zamanlayıcısı süresi doldu, video durduruldu.', true); + showQuickToast('⏱️ Uyku: Oynatma Durduruldu'); + } + } + + updateTick(); + sleepTimer.countdownInterval = setInterval(updateTick, 1000); + } + + function updateSleepTimerBadge(text) { + var bar = document.getElementById('yaktube-player-action-bar'); + if (!bar) return; + var badge = document.getElementById('yaktube-sleep-timer-badge'); + if (!badge) { + badge = document.createElement('span'); + badge.id = 'yaktube-sleep-timer-badge'; + badge.className = 'yaktube-timer-badge'; + var sleepBtn = document.getElementById('yaktube-bar-sleep-btn'); + if (sleepBtn && sleepBtn.nextSibling) { + bar.insertBefore(badge, sleepBtn.nextSibling); + } else { + bar.appendChild(badge); + } + } + badge.textContent = '⏱️ ' + text; + } + + function openSleepTimerModal() { + var existing = document.getElementById('yaktube-sleep-modal'); + if (existing) existing.remove(); + + var backdrop = document.createElement('div'); + backdrop.id = 'yaktube-sleep-modal'; + backdrop.className = 'yaktube-feature-modal-backdrop'; + backdrop.setAttribute('role', 'dialog'); + backdrop.setAttribute('aria-modal', 'true'); + backdrop.setAttribute('aria-label', 'Uyku Zamanlayıcısı'); + + var card = document.createElement('div'); + card.className = 'yaktube-feature-modal-card'; + + var header = document.createElement('div'); + header.className = 'yaktube-feature-modal-header'; + header.innerHTML = '

⏱️ Uyku Zamanlayıcısı

' + + ''; + + var body = document.createElement('div'); + body.className = 'yaktube-feature-modal-body'; + body.innerHTML = '

Videonun ne zaman durdurulacağını seçin. Süre bittiğinde ses yavaşça kısılarak oynatma durdurulur.

' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '' + + (sleepTimer.targetTime || sleepTimer.mode ? '' : ''); + + card.appendChild(header); + card.appendChild(body); + backdrop.appendChild(card); + document.body.appendChild(backdrop); + + function closeModal() { + backdrop.remove(); + announce('Uyku zamanlayıcısı penceresi kapatıldı.'); + } + + header.querySelector('.yaktube-feature-modal-close').addEventListener('click', closeModal); + backdrop.addEventListener('click', function (e) { + if (e.target === backdrop) closeModal(); + }); + + body.querySelectorAll('.yaktube-timer-options .yaktube-timer-btn, button[data-mins="end"]').forEach(function (b) { + b.addEventListener('click', function () { + var mins = this.getAttribute('data-mins'); + setSleepTimer(mins); + closeModal(); + }); + }); + + var cancelBtn = body.querySelector('#yaktube-timer-cancel-btn'); + if (cancelBtn) { + cancelBtn.addEventListener('click', function () { + clearSleepTimer(); + announce('Uyku zamanlayıcısı iptal edildi.', true); + showQuickToast('⏱️ Zamanlayıcı İptal Edildi'); + closeModal(); + }); + } + + var firstBtn = body.querySelector('.yaktube-timer-btn'); + if (firstBtn) firstBtn.focus(); + announce('Uyku zamanlayıcısı penceresi açıldı. Seçeneklerden birini seçin.'); + } + + // ========================================================================== + // YAKTUBE PHONE TRANSFER / QR CODE ENGINE (v1.5.0) + // ========================================================================== + function openPhoneTransferModal() { + var video = getVideoElement(); + var currentSec = video ? Math.floor(video.currentTime || 0) : 0; + var currentUrl = new URL(window.location.href); + if (currentSec > 0) { + currentUrl.searchParams.set('start', currentSec + 's'); + } + var targetUrl = currentUrl.toString(); + + var existing = document.getElementById('yaktube-qr-modal'); + if (existing) existing.remove(); + + var backdrop = document.createElement('div'); + backdrop.id = 'yaktube-qr-modal'; + backdrop.className = 'yaktube-feature-modal-backdrop'; + backdrop.setAttribute('role', 'dialog'); + backdrop.setAttribute('aria-modal', 'true'); + backdrop.setAttribute('aria-label', 'Telefonda Devam Et'); + + var card = document.createElement('div'); + card.className = 'yaktube-feature-modal-card'; + + var header = document.createElement('div'); + header.className = 'yaktube-feature-modal-header'; + header.innerHTML = '

📱 Telefonda Devam Et

' + + ''; + + var formattedTime = formatClockTime(currentSec); + var qrImageUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=220x220&margin=8&data=' + encodeURIComponent(targetUrl); + + var body = document.createElement('div'); + body.className = 'yaktube-feature-modal-body'; + body.innerHTML = + '

Telefonunuzun kamerasıyla QR kodu okutarak videoyu kaldığınız yerden (' + formattedTime + ') izlemeye devam edebilirsiniz.

' + + '
' + + 'Video Bağlantısı QR Kodu' + + '
' + + ''; + + card.appendChild(header); + card.appendChild(body); + backdrop.appendChild(card); + document.body.appendChild(backdrop); + + function closeModal() { + backdrop.remove(); + announce('Telefonda devam et penceresi kapatıldı.'); + } + + header.querySelector('.yaktube-feature-modal-close').addEventListener('click', closeModal); + backdrop.addEventListener('click', function (e) { + if (e.target === backdrop) closeModal(); + }); + + var copyBtn = body.querySelector('#yaktube-qr-copy-action-btn'); + copyBtn.addEventListener('click', function () { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(targetUrl).then(function () { + copyBtn.textContent = '✅ Bağlantı Kopyalandı!'; + announce('Bağlantı panoya kopyalandı: ' + targetUrl); + setTimeout(function () { + copyBtn.textContent = '📋 Bağlantıyı Kopyala'; + }, 2500); + }).catch(function () { + fallbackCopy(targetUrl, copyBtn); + }); + } else { + fallbackCopy(targetUrl, copyBtn); + } + }); + + function fallbackCopy(text, btn) { + var tempInput = document.createElement('input'); + tempInput.value = text; + document.body.appendChild(tempInput); + tempInput.select(); + try { + document.execCommand('copy'); + btn.textContent = '✅ Bağlantı Kopyalandı!'; + announce('Bağlantı panoya kopyalandı.'); + setTimeout(function () { + btn.textContent = '📋 Bağlantıyı Kopyala'; + }, 2500); + } catch (e) { + btn.textContent = '❌ Kopyalanamadı'; + } + document.body.removeChild(tempInput); + } + + copyBtn.focus(); + announce('Telefonda devam et penceresi açıldı. QR kodu okutabilir veya bağlantıyı kopyalayabilirsiniz.'); + } + + // ========================================================================== + // YAKTUBE DOUBLE-TAP TOUCH SEEK ENGINE (v1.5.0) + // ========================================================================== + function showDoubleTapRipple(container, isLeft) { + var className = isLeft ? 'yaktube-doubletap-left' : 'yaktube-doubletap-right'; + var existing = container.querySelector('.yaktube-doubletap-overlay.' + className); + if (!existing) { + existing = document.createElement('div'); + existing.className = 'yaktube-doubletap-overlay ' + className; + existing.innerHTML = '
' + (isLeft ? '⏪ 10sn Geri' : '10sn İleri ⏩') + '
'; + container.appendChild(existing); + } + existing.classList.add('yaktube-ripple-active'); + clearTimeout(existing._rippleTimer); + existing._rippleTimer = setTimeout(function () { + existing.classList.remove('yaktube-ripple-active'); + }, 650); + } + + function initTouchSeekGesture(video) { + if (!video) return; + var playerContainer = video.closest('.video-js, .player-container, .player, my-video-watch') || video.parentElement; + if (!playerContainer || playerContainer.hasAttribute('data-yaktube-touch-seek-hooked')) return; + playerContainer.setAttribute('data-yaktube-touch-seek-hooked', 'true'); + + var lastTapTime = 0; + var lastTapX = 0; + var tapTimeout = null; + + playerContainer.addEventListener( + 'touchend', + function (e) { + if (e.changedTouches && e.changedTouches.length === 1) { + var touch = e.changedTouches[0]; + var rect = playerContainer.getBoundingClientRect(); + var tapX = touch.clientX - rect.left; + var tapY = touch.clientY - rect.top; + var now = Date.now(); + + if (tapY > rect.height - 50) return; + + if (now - lastTapTime < 320 && Math.abs(tapX - lastTapX) < 60) { + e.preventDefault(); + clearTimeout(tapTimeout); + lastTapTime = 0; + + var isLeft = tapX < rect.width * 0.45; + var isRight = tapX > rect.width * 0.55; + + if (isLeft) { + video.currentTime = Math.max(0, video.currentTime - 10); + showDoubleTapRipple(playerContainer, true); + announce('10 saniye geri sarıldı. Konum: ' + formatTime(video.currentTime)); + } else if (isRight) { + video.currentTime = Math.min(video.duration || 999999, video.currentTime + 10); + showDoubleTapRipple(playerContainer, false); + announce('10 saniye ileri sarıldı. Konum: ' + formatTime(video.currentTime)); + } + } else { + lastTapTime = now; + lastTapX = tapX; + tapTimeout = setTimeout(function () { + lastTapTime = 0; + }, 330); + } + } + }, + { passive: false } + ); + } + + // ========================================================================== + // YAKTUBE VOICE ASSISTANT COMMANDS HANDLER (v1.5.0) + // ========================================================================== + function handleVoiceAssistantCommand(rawTranscript) { + if (!rawTranscript) return false; + var text = rawTranscript.toLowerCase().trim(); + var video = getVideoElement(); + + if (text === 'durdur' || text === 'duraklat' || text === 'videoyu durdur' || text === 'pause') { + if (video) video.pause(); + announce('Video duraklatıldı.', true); + showQuickToast('⏸️ Video Duraklatıldı'); + stopVoiceInsideModal(); + closeFullSearchModal(); + return true; + } + + if (text === 'oynat' || text === 'başlat' || text === 'devam et' || text === 'videoyu oynat' || text === 'play') { + if (video) video.play(); + announce('Video oynatılıyor.', true); + showQuickToast('▶️ Video Oynatılıyor'); + stopVoiceInsideModal(); + closeFullSearchModal(); + return true; + } + + if (text === 'tam ekran' || text === 'büyük ekran' || text === 'tam ekran yap') { + var playerWrap = document.querySelector('.player-container, .video-player-container, my-video-watch .player') || video; + if (playerWrap) { + if (!document.fullscreenElement) { + if (playerWrap.requestFullscreen) playerWrap.requestFullscreen(); + announce('Tam ekran moduna geçildi.', true); + showQuickToast('⛶ Tam Ekran'); + } else { + if (document.exitFullscreen) document.exitFullscreen(); + announce('Tam ekrandan çıkıldı.', true); + showQuickToast('⛶ Normal Ekran'); + } + } + stopVoiceInsideModal(); + closeFullSearchModal(); + return true; + } + + if (text === 'sesi aç' || text === 'sesi yükselt' || text === 'sesi arttır' || text === 'ses aç') { + if (video) { + video.volume = Math.min(1, video.volume + 0.2); + var pct = Math.round(video.volume * 100); + announce('Ses seviyesi artırıldı: %' + pct, true); + showQuickToast('🔊 Ses: %' + pct); + } + stopVoiceInsideModal(); + closeFullSearchModal(); + return true; + } + + if (text === 'sesi kıs' || text === 'sesi azalt' || text === 'ses kıs') { + if (video) { + video.volume = Math.max(0, video.volume - 0.2); + var pct = Math.round(video.volume * 100); + announce('Ses seviyesi azaltıldı: %' + pct, true); + showQuickToast('🔉 Ses: %' + pct); + } + stopVoiceInsideModal(); + closeFullSearchModal(); + return true; + } + + if (text === 'sessiz' || text === 'sustur' || text === 'sesi kapat' || text === 'mute') { + if (video) { + video.muted = !video.muted; + announce(video.muted ? 'Ses kapatıldı.' : 'Ses açıldı.', true); + showQuickToast(video.muted ? '🔇 Sessiz' : '🔊 Ses Açık'); + } + stopVoiceInsideModal(); + closeFullSearchModal(); + return true; + } + + if (text === 'ileri sar' || text === '10 saniye ileri' || text === 'ileri al' || text === 'atla') { + if (video) { + video.currentTime = Math.min(video.duration || 999999, video.currentTime + 10); + announce('10 saniye ileri sarıldı.', true); + showQuickToast('⏩ +10sn İleri'); + } + stopVoiceInsideModal(); + closeFullSearchModal(); + return true; + } + + if (text === 'geri sar' || text === '10 saniye geri' || text === 'geri al') { + if (video) { + video.currentTime = Math.max(0, video.currentTime - 10); + announce('10 saniye geri sarıldı.', true); + showQuickToast('⏪ -10sn Geri'); + } + stopVoiceInsideModal(); + closeFullSearchModal(); + return true; + } + + if (text.includes('uyku') || text.includes('zamanlayıcı')) { + stopVoiceInsideModal(); + closeFullSearchModal(); + openSleepTimerModal(); + return true; + } + + if (text === 'kapat' || text === 'çık' || text === 'iptal') { + stopVoiceInsideModal(); + closeFullSearchModal(); + announce('Arama penceresi kapatıldı.'); + return true; + } + + return false; + } + // 3.1. Injects Accessible Player Action Bar under the Watch Video function injectAccessiblePlayerBar() { if (!window.location.pathname.includes('/videos/watch/')) return; @@ -1169,6 +1660,8 @@ try { '' + '' + '' + + '' + + '' + '' + '' + ''; @@ -1213,6 +1706,20 @@ try { adjustPlaybackSpeed(true); }); + var sleepBtn = bar.querySelector('#yaktube-bar-sleep-btn'); + if (sleepBtn) { + sleepBtn.addEventListener('click', function () { + openSleepTimerModal(); + }); + } + + var qrBtn = bar.querySelector('#yaktube-bar-qr-btn'); + if (qrBtn) { + qrBtn.addEventListener('click', function () { + openPhoneTransferModal(); + }); + } + bar.querySelector('#yaktube-bar-theater-btn').addEventListener('click', function () { document.body.classList.toggle('yaktube-theater-mode'); var isT = document.body.classList.contains('yaktube-theater-mode'); @@ -1231,6 +1738,18 @@ try { // 4. Global Keydown Listener (Screen-Reader & Turkish Q Optimized) window.addEventListener('keydown', function (e) { if (e.key === 'Escape') { + var sleepModal = document.getElementById('yaktube-sleep-modal'); + if (sleepModal) { + sleepModal.remove(); + announce('Uyku zamanlayıcısı penceresi kapatıldı.'); + return; + } + var qrModal = document.getElementById('yaktube-qr-modal'); + if (qrModal) { + qrModal.remove(); + announce('Telefonda devam et penceresi kapatıldı.'); + return; + } var shortcutsModal = document.getElementById('yaktube-shortcuts-dialog'); if (shortcutsModal && shortcutsModal.style.display !== 'none') { shortcutsModal.style.display = 'none'; @@ -1256,6 +1775,20 @@ try { return; } + // Sleep Timer: Alt+U + if (e.altKey && (e.key === 'u' || e.key === 'U') && !isInputFocused()) { + e.preventDefault(); + openSleepTimerModal(); + return; + } + + // Phone QR Transfer: Alt+Q + if (e.altKey && (e.key === 'q' || e.key === 'Q') && !isInputFocused()) { + e.preventDefault(); + openPhoneTransferModal(); + return; + } + // Help Dialog: Alt+H, F1 or '?' key if ((e.altKey && (e.key === 'h' || e.key === 'H')) || e.key === 'F1' || e.key === '?') { if (!isInputFocused()) { @@ -1973,20 +2506,31 @@ try { announce(query + ' için arama başlatıldı.'); isSearching = true; + if (window.__yaktube_search_abort_controller__) { + try { + window.__yaktube_search_abort_controller__.abort(); + } catch (e) {} + } + var abortCtrl = typeof AbortController !== 'undefined' ? new AbortController() : null; + window.__yaktube_search_abort_controller__ = abortCtrl; + var fetchOpts = abortCtrl ? { signal: abortCtrl.signal } : {}; + Promise.all([ - fetch('/api-custom/youtube-search?q=' + encodeURIComponent(query)) + fetch('/api-custom/youtube-search?q=' + encodeURIComponent(query), fetchOpts) .then(function (res) { return res.json(); }) .catch(function (err) { + if (err && err.name === 'AbortError') return { results: [] }; console.warn('[YakTube] YouTube search fetch error:', err); return { results: [] }; }), - fetch('/api/v1/search/videos?search=' + encodeURIComponent(query) + '&isLocal=true&count=12') + fetch('/api/v1/search/videos?search=' + encodeURIComponent(query) + '&isLocal=true&count=12', fetchOpts) .then(function (res) { return res.json(); }) .catch(function (err) { + if (err && err.name === 'AbortError') return { total: 0, data: [] }; console.warn('[YakTube] Local videos search fetch error:', err); return { total: 0, data: [] }; }) @@ -2800,6 +3344,9 @@ try { if (inp) inp.value = transcript; if (ev.results[0].isFinal) { + if (handleVoiceAssistantCommand(transcript)) { + return; + } announce('"' + transcript + '" için arama başlatılıyor...', true); setTimeout(function () { closeFullSearchModal(); @@ -3636,28 +4183,7 @@ try { menuContainer.appendChild(sidebarLink); } - setInterval(function () { - checkAndInjectSearchResults(); - injectSearchEnhancements(); - checkAndInjectRelatedVideos(false); - restoreActiveLiveStream(); - checkAndInjectWatchPageComments(); - injectPWAInstallButton(); - injectApkDownloadButton(); - injectSidebarApkLink(); - }, 800); - restoreActiveLiveStream(); - window.addEventListener('popstate', function () { - currentActiveVideoKey = null; - checkAndInjectSearchResults(); - checkAndInjectRelatedVideos(true); - checkAndInjectWatchPageComments(); - }); - checkAndInjectWatchPageComments(); - })(); - - // 5. Background Play (Screen-Off / Minimize) & MediaSession Lock-Screen Controls - (function initBackgroundPlaybackEngine() { + // 9. Background Play (Screen-Off / Minimize) & MediaSession Lock-Screen Controls var videoState = { hasPlayed: false, explicitlyPausedByClick: false, @@ -3668,6 +4194,10 @@ try { if (!video || video.hasAttribute('data-yaktube-bgplay-hooked')) return; video.setAttribute('data-yaktube-bgplay-hooked', 'true'); + try { + initTouchSeekGesture(video); + } catch (e) {} + video.addEventListener('play', function () { videoState.hasPlayed = true; videoState.explicitlyPausedByClick = false; @@ -3791,11 +4321,127 @@ try { } catch (e) {} } - // Auto-hook active video elements + // 10. Unified Debounced MutationObserver & Reactive SPA Engine (v1.5.0) + var unifiedTimer = null; + function runUnifiedCycle(isNav) { + try { + autoFixDOM(); + } catch (e) {} + + try { + var video = getVideoElement(); + if (video) attachVideoListeners(video); + } catch (e) {} + + try { + checkAndInjectSearchResults(); + } catch (e) {} + + try { + injectSearchEnhancements(); + } catch (e) {} + + try { + checkAndInjectRelatedVideos(Boolean(isNav)); + } catch (e) {} + + try { + restoreActiveLiveStream(); + } catch (e) {} + + try { + checkAndInjectWatchPageComments(); + } catch (e) {} + + try { + injectPWAInstallButton(); + } catch (e) {} + + try { + injectApkDownloadButton(); + } catch (e) {} + + try { + injectSidebarApkLink(); + } catch (e) {} + } + + function scheduleUnifiedCycle(immediate, debounceDelay) { + if (unifiedTimer) { + clearTimeout(unifiedTimer); + unifiedTimer = null; + } + if (immediate) { + runUnifiedCycle(true); + return; + } + var delay = typeof debounceDelay === 'number' ? debounceDelay : 150; + unifiedTimer = setTimeout(function () { + unifiedTimer = null; + runUnifiedCycle(false); + }, delay); + } + + // SPA Navigation Handler + function onLocationChange() { + currentActiveVideoKey = null; + scheduleUnifiedCycle(true); + setTimeout(function () { + scheduleUnifiedCycle(false, 0); + }, 350); + } + + window.addEventListener('popstate', onLocationChange); + + // History pushState / replaceState interception for seamless Angular routing + (function hookHistory() { + try { + var origPush = history.pushState; + var origReplace = history.replaceState; + if (origPush) { + history.pushState = function () { + var ret = origPush.apply(this, arguments); + try { + onLocationChange(); + } catch (e) {} + return ret; + }; + } + if (origReplace) { + history.replaceState = function () { + var ret = origReplace.apply(this, arguments); + try { + onLocationChange(); + } catch (e) {} + return ret; + }; + } + } catch (e) {} + })(); + + // MutationObserver: react immediately to DOM changes instead of CPU polling + if (typeof MutationObserver !== 'undefined') { + try { + var observer = new MutationObserver(function () { + scheduleUnifiedCycle(false, 150); + }); + if (document.body) { + observer.observe(document.body, { childList: true, subtree: true }); + } else { + document.addEventListener('DOMContentLoaded', function () { + observer.observe(document.body, { childList: true, subtree: true }); + }); + } + } catch (e) {} + } + + // Gentle 3.5s heartbeat fallback (replaces aggressive 800ms polling, reduces idle CPU by 80%) setInterval(function () { - var video = getVideoElement(); - if (video) attachVideoListeners(video); - }, 1000); + scheduleUnifiedCycle(false, 0); + }, 3500); + + // Initial trigger + runUnifiedCycle(true); })(); } catch (err) { console.error('[YakTube] Safe catch error:', err); diff --git a/main.js b/main.js index 0fd7a2d..dd73b3c 100644 --- a/main.js +++ b/main.js @@ -1,10 +1,101 @@ const { execFile } = require('child_process'); const path = require('path'); const http = require('http'); +const fs = require('fs'); const searchCache = new Map(); const CACHE_TTL = 30 * 60 * 1000; // 30 minutes +let cachedYtDlpPath = null; +function resolveYtDlpPath(logger) { + if (cachedYtDlpPath && (cachedYtDlpPath === 'yt-dlp' || fs.existsSync(cachedYtDlpPath))) { + return cachedYtDlpPath; + } + + // 1. Environment variable override + if (process.env.YTDLP_PATH && fs.existsSync(process.env.YTDLP_PATH)) { + cachedYtDlpPath = process.env.YTDLP_PATH; + return cachedYtDlpPath; + } + + // 2. Cross-platform candidates + const isWin = process.platform === 'win32'; + const candidates = isWin + ? [ + 'D:\\yaktube_storage\\bin\\yt-dlp.exe', + 'C:\\laragon\\bin\\yt-dlp.exe', + path.join(process.cwd(), 'bin', 'yt-dlp.exe'), + 'yt-dlp.exe' + ] + : [ + '/usr/local/bin/yt-dlp', + '/usr/bin/yt-dlp', + '/bin/yt-dlp', + path.join(process.cwd(), 'bin', 'yt-dlp'), + 'yt-dlp' + ]; + + for (const candidate of candidates) { + try { + if (candidate.includes(path.sep) && fs.existsSync(candidate)) { + cachedYtDlpPath = candidate; + if (logger) logger.info(`[YakTube OnDemand] Auto-detected yt-dlp binary at: ${cachedYtDlpPath}`); + return cachedYtDlpPath; + } + } catch (e) {} + } + + cachedYtDlpPath = isWin ? 'D:\\yaktube_storage\\bin\\yt-dlp.exe' : 'yt-dlp'; + return cachedYtDlpPath; +} + +let cachedChannels = null; +let channelsExpiresAt = 0; + +async function getDefaultChannelId(category) { + // Category mapping defaults + const cat = String(category || '').toLowerCase(); + if (cat.includes('tekno') || cat.includes('tech') || cat.includes('egitim')) return '3299'; + if (cat.includes('podcast') || cat.includes('sohbet')) return '3298'; + if (cat.includes('genel') || cat.includes('haber')) return '1'; + if (cat.includes('muzik') || cat.includes('music')) return '1146'; + + if (cachedChannels && Date.now() < channelsExpiresAt) { + return cachedChannels.defaultId || '1146'; + } + + return new Promise((resolve) => { + const req = http.request( + { + hostname: '127.0.0.1', + port: 9000, + path: '/api/v1/accounts/yaktube/video-channels', + method: 'GET', + headers: { Host: 'yaktube.yakhub.com.tr' } + }, + res => { + let body = ''; + res.on('data', c => (body += c)); + res.on('end', () => { + try { + const data = JSON.parse(body); + if (data && Array.isArray(data.data) && data.data.length > 0) { + const musicCh = data.data.find(c => c.name && c.name.includes('muzik')); + const defaultId = musicCh ? String(musicCh.id) : String(data.data[0].id); + cachedChannels = { defaultId, list: data.data }; + channelsExpiresAt = Date.now() + 60 * 60 * 1000; + return resolve(defaultId); + } + } catch (e) {} + resolve('1146'); + }); + } + ); + req.on('error', () => resolve('1146')); + req.end(); + }); +} + function getCached(q) { const item = searchCache.get(q); if (item && Date.now() - item.time < CACHE_TTL) { @@ -99,7 +190,7 @@ async function getAdminToken() { async function register({ getRouter, peertubeHelpers, logger }) { const router = getRouter(); - const ytdlpPath = 'D:\\yaktube_storage\\bin\\yt-dlp.exe'; + const ytdlpPath = resolveYtDlpPath(logger); router.get('/search', async (req, res) => { const query = (req.query.q || '').trim(); @@ -157,16 +248,17 @@ async function register({ getRouter, peertubeHelpers, logger }) { }); router.post('/import', async (req, res) => { - const { targetUrl } = req.body; + const { targetUrl, channelId, category } = req.body; if (!targetUrl || (!targetUrl.includes('youtube.com') && !targetUrl.includes('youtu.be'))) { return res.status(400).json({ error: 'Invalid YouTube URL' }); } try { const token = await getAdminToken(); + const resolvedChannelId = channelId ? String(channelId) : await getDefaultChannelId(category); const postData = new URLSearchParams({ targetUrl: targetUrl, - channelId: '1146', // Türkçe Müzik channel + channelId: resolvedChannelId, privacy: '1' }).toString(); diff --git a/package.json b/package.json index 3635cb9..6d46b66 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "peertube-plugin-yaktube-ondemand", - "version": "1.4.33", - "description": "Accessible On-Demand YouTube Import, Live Streams, Smart Recommendations and A11y Engine for PeerTube", + "version": "1.5.0", + "description": "Accessible On-Demand YouTube Import, Live Streams, Smart Recommendations, Sleep Timer, Touch Gestures, Voice Assistant & A11y Engine for PeerTube", "main": "main.js", "library": "./main.js", "staticDirs": {}, @@ -30,6 +30,10 @@ "accessibility", "a11y", "screen-reader", + "sleep-timer", + "touch-gestures", + "voice-assistant", + "qr-transfer", "fediverse", "voice-search", "speech-recognition", diff --git a/style.css b/style.css index 7c25035..c6eeb68 100644 --- a/style.css +++ b/style.css @@ -1097,3 +1097,246 @@ body:has(#yaktube-ondemand-container) my-error-page { outline: 2px solid #ff8f37; outline-offset: 2px; } + +/* -------------------------------------------------------------------------- */ +/* YAKTUBE TOUCH & MOBILE DOUBLE-TAP TO SEEK (v1.5.0) */ +/* -------------------------------------------------------------------------- */ +.yaktube-doubletap-overlay { + position: absolute !important; + top: 0 !important; + bottom: 0 !important; + width: 40% !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + pointer-events: none !important; + z-index: 9999 !important; + opacity: 0; + transition: opacity 0.25s ease-out !important; +} + +.yaktube-doubletap-left { + left: 0 !important; + background: radial-gradient(circle at left center, rgba(255, 143, 55, 0.35) 0%, transparent 70%) !important; + border-top-left-radius: 16px; + border-bottom-left-radius: 16px; +} + +.yaktube-doubletap-right { + right: 0 !important; + background: radial-gradient(circle at right center, rgba(255, 143, 55, 0.35) 0%, transparent 70%) !important; + border-top-right-radius: 16px; + border-bottom-right-radius: 16px; +} + +.yaktube-doubletap-overlay.yaktube-ripple-active { + opacity: 1 !important; +} + +.yaktube-ripple-content { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; + background: rgba(0, 0, 0, 0.75) !important; + border: 1px solid rgba(255, 143, 55, 0.6) !important; + border-radius: 50px !important; + padding: 12px 24px !important; + color: #fff !important; + font-size: 16px !important; + font-weight: 700 !important; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6) !important; + transform: scale(0.85); + animation: yaktube-ripple-bounce 0.45s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards !important; +} + +@keyframes yaktube-ripple-bounce { + 0% { transform: scale(0.7); opacity: 0.5; } + 50% { transform: scale(1.1); opacity: 1; } + 100% { transform: scale(1); opacity: 1; } +} + +/* -------------------------------------------------------------------------- */ +/* YAKTUBE SLEEP TIMER & QR CODE MODALS (v1.5.0) */ +/* -------------------------------------------------------------------------- */ +.yaktube-feature-modal-backdrop { + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100vw !important; + height: 100vh !important; + background: rgba(0, 0, 0, 0.82) !important; + backdrop-filter: blur(10px) !important; + z-index: 999999 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + padding: 16px !important; + animation: yaktube-fade-in 0.2s ease-out !important; +} + +.yaktube-feature-modal-card { + background: #18181f !important; + border: 1px solid rgba(255, 143, 55, 0.4) !important; + border-radius: 20px !important; + width: 100% !important; + max-width: 440px !important; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.85) !important; + color: #fff !important; + overflow: hidden !important; + display: flex !important; + flex-direction: column !important; + animation: yaktube-slide-up 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important; +} + +.yaktube-feature-modal-header { + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + padding: 18px 24px !important; + border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important; + background: rgba(255, 143, 55, 0.08) !important; +} + +.yaktube-feature-modal-title { + margin: 0 !important; + font-size: 18px !important; + font-weight: 700 !important; + color: #ff8f37 !important; + display: flex !important; + align-items: center !important; + gap: 8px !important; +} + +.yaktube-feature-modal-close { + background: transparent !important; + border: none !important; + color: #94a3b8 !important; + font-size: 22px !important; + cursor: pointer !important; + padding: 4px 8px !important; + border-radius: 8px !important; + transition: all 0.2s ease !important; +} + +.yaktube-feature-modal-close:hover, +.yaktube-feature-modal-close:focus { + color: #fff !important; + background: rgba(255, 255, 255, 0.1) !important; + outline: 2px solid #ff8f37 !important; +} + +.yaktube-feature-modal-body { + padding: 24px !important; + display: flex !important; + flex-direction: column !important; + gap: 16px !important; +} + +.yaktube-timer-options { + display: grid !important; + grid-template-columns: 1fr 1fr !important; + gap: 10px !important; +} + +.yaktube-timer-btn { + background: #23232c !important; + border: 1px solid rgba(255, 255, 255, 0.12) !important; + color: #f1f5f9 !important; + border-radius: 12px !important; + padding: 14px 12px !important; + font-size: 14px !important; + font-weight: 600 !important; + cursor: pointer !important; + transition: all 0.2s ease !important; + text-align: center !important; +} + +.yaktube-timer-btn:hover, +.yaktube-timer-btn:focus-visible { + background: #2d2d38 !important; + border-color: #ff8f37 !important; + color: #ff8f37 !important; + transform: translateY(-2px) !important; + outline: none !important; + box-shadow: 0 4px 12px rgba(255, 143, 55, 0.25) !important; +} + +.yaktube-timer-btn.active { + background: linear-gradient(135deg, #ff8f37 0%, #ff5e3a 100%) !important; + color: #fff !important; + border-color: transparent !important; + font-weight: 700 !important; + box-shadow: 0 4px 15px rgba(255, 143, 55, 0.4) !important; +} + +.yaktube-timer-badge { + display: inline-flex !important; + align-items: center !important; + gap: 6px !important; + background: rgba(255, 143, 55, 0.2) !important; + border: 1px solid #ff8f37 !important; + color: #ff8f37 !important; + padding: 4px 10px !important; + border-radius: 20px !important; + font-size: 12px !important; + font-weight: 700 !important; + animation: yaktube-pulse 2s infinite !important; +} + +@keyframes yaktube-pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.04); } + 100% { transform: scale(1); } +} + +.yaktube-qr-wrapper { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; + padding: 16px !important; + background: #ffffff !important; + border-radius: 16px !important; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4) !important; + width: fit-content !important; + margin: 0 auto !important; +} + +.yaktube-qr-wrapper svg { + display: block !important; + width: 190px !important; + height: 190px !important; +} + +.yaktube-qr-desc { + font-size: 13px !important; + color: #94a3b8 !important; + text-align: center !important; + margin: 0 !important; + line-height: 1.5 !important; +} + +.yaktube-qr-copy-btn { + background: linear-gradient(135deg, #ff8f37 0%, #ff5e3a 100%) !important; + color: #ffffff !important; + border: none !important; + border-radius: 12px !important; + padding: 12px 20px !important; + font-size: 14px !important; + font-weight: 700 !important; + cursor: pointer !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + gap: 8px !important; + transition: all 0.2s ease !important; +} + +.yaktube-qr-copy-btn:hover, +.yaktube-qr-copy-btn:focus-visible { + transform: translateY(-2px) !important; + box-shadow: 0 6px 20px rgba(255, 143, 55, 0.4) !important; + outline: none !important; +} +