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 T | Sinema / Tiyatro Modu |
' +
'| Alt + I veya I | Hızlı Video Bilgisi & İstatistikler |
' +
'| Alt + C veya C | Altyazı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 + V | Sesli 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.
' +
+ '' +
+ '

' +
+ '
' +
+ '';
+
+ 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;
+}
+