feat(v1.5.0): unified observer, sleep timer, double-tap seek, voice assistant commands & qr handoff

This commit is contained in:
Enes Yakıştır
2026-09-19 05:14:11 +03:00
parent eef5bd85ac
commit 3950d91070
4 changed files with 1022 additions and 37 deletions
+678 -32
View File
@@ -31,7 +31,10 @@ if (typeof window !== 'undefined') {
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', neuterIncompatible); document.addEventListener('DOMContentLoaded', neuterIncompatible);
} }
setInterval(neuterIncompatible, 1000); var neuterInterval = setInterval(neuterIncompatible, 1000);
setTimeout(function () {
clearInterval(neuterInterval);
}, 5000);
} catch (e) {} } catch (e) {}
} }
@@ -951,14 +954,12 @@ try {
} else if (type === 'checkbox') { } else if (type === 'checkbox') {
inp.setAttribute('aria-label', 'Seçim Kutusu'); inp.setAttribute('aria-label', 'Seçim Kutusu');
} else if (type === 'password') { } 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) // 3. Screen-Reader & Turkish Q Optimized Shortcuts & Player Bar (v1.4.1)
function toggleShortcutsModal() { function toggleShortcutsModal() {
var modal = document.getElementById('yaktube-shortcuts-dialog'); var modal = document.getElementById('yaktube-shortcuts-dialog');
@@ -997,6 +998,8 @@ try {
'<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + T</span> veya <span class="yaktube-kbd">T</span></td><td>Sinema / Tiyatro Modu</td></tr>' + '<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + T</span> veya <span class="yaktube-kbd">T</span></td><td>Sinema / Tiyatro Modu</td></tr>' +
'<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + I</span> veya <span class="yaktube-kbd">I</span></td><td>Hızlı Video Bilgisi & İstatistikler</td></tr>' + '<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + I</span> veya <span class="yaktube-kbd">I</span></td><td>Hızlı Video Bilgisi & İstatistikler</td></tr>' +
'<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + C</span> veya <span class="yaktube-kbd">C</span></td><td>Altyazıları Aç / Kapat</td></tr>' + '<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + C</span> veya <span class="yaktube-kbd">C</span></td><td>Altyazıları Aç / Kapat</td></tr>' +
'<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + U</span></td><td>⏱️ Uyku Zamanlayıcısı (Sleep Timer)</td></tr>' +
'<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + Q</span></td><td>📱 Telefonda Devam Et (QR Kod)</td></tr>' +
'<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + .</span> / <span class="yaktube-kbd">Alt + ,</span></td><td>Oynatma hızını artır / azalt</td></tr>' + '<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + .</span> / <span class="yaktube-kbd">Alt + ,</span></td><td>Oynatma hızını artır / azalt</td></tr>' +
'<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + S</span> veya <span class="yaktube-kbd">/</span> (Shift+7)</td><td>Arama penceresini aç</td></tr>' + '<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + S</span> veya <span class="yaktube-kbd">/</span> (Shift+7)</td><td>Arama penceresini aç</td></tr>' +
'<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + V</span> veya <span class="yaktube-kbd">Shift + V</span></td><td>Sesli aramayı başlat</td></tr>' + '<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + V</span> veya <span class="yaktube-kbd">Shift + V</span></td><td>Sesli aramayı başlat</td></tr>' +
@@ -1045,6 +1048,7 @@ try {
} }
return null; return null;
} }
window.getVideoElement = getVideoElement;
function formatTime(seconds) { function formatTime(seconds) {
if (isNaN(seconds) || seconds < 0) return '0 saniye'; 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 ? '<span>' + icon + '</span> ' : '') + '<span>' + message + '</span>';
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 = '<h3 class="yaktube-feature-modal-title"><span>⏱️</span> Uyku Zamanlayıcısı</h3>' +
'<button class="yaktube-feature-modal-close" aria-label="Kapat (Escape)">&times;</button>';
var body = document.createElement('div');
body.className = 'yaktube-feature-modal-body';
body.innerHTML = '<p class="yaktube-qr-desc">Videonun ne zaman durdurulacağını seçin. Süre bittiğinde ses yavaşça kısılarak oynatma durdurulur.</p>' +
'<div class="yaktube-timer-options">' +
'<button class="yaktube-timer-btn' + (sleepTimer.mode === '15' ? ' active' : '') + '" data-mins="15">15 Dakika</button>' +
'<button class="yaktube-timer-btn' + (sleepTimer.mode === '30' ? ' active' : '') + '" data-mins="30">30 Dakika</button>' +
'<button class="yaktube-timer-btn' + (sleepTimer.mode === '45' ? ' active' : '') + '" data-mins="45">45 Dakika</button>' +
'<button class="yaktube-timer-btn' + (sleepTimer.mode === '60' ? ' active' : '') + '" data-mins="60">60 Dakika</button>' +
'</div>' +
'<button class="yaktube-timer-btn' + (sleepTimer.mode === 'end' ? ' active' : '') + '" data-mins="end" style="width:100%;">🎬 Bu Video Bitince</button>' +
(sleepTimer.targetTime || sleepTimer.mode ? '<button id="yaktube-timer-cancel-btn" class="yaktube-timer-btn" style="background:#ef4444!important;color:#fff!important;border-color:#ef4444!important;">❌ Zamanlayıcıyı İptal Et</button>' : '');
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 = '<h3 class="yaktube-feature-modal-title"><span>📱</span> Telefonda Devam Et</h3>' +
'<button class="yaktube-feature-modal-close" aria-label="Kapat (Escape)">&times;</button>';
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 =
'<p class="yaktube-qr-desc">Telefonunuzun kamerasıyla QR kodu okutarak videoyu kaldığınız yerden (<strong>' + formattedTime + '</strong>) izlemeye devam edebilirsiniz.</p>' +
'<div class="yaktube-qr-wrapper">' +
'<img src="' + qrImageUrl + '" alt="Video Bağlantısı QR Kodu" width="190" height="190" style="display:block;border-radius:8px;" />' +
'</div>' +
'<button id="yaktube-qr-copy-action-btn" class="yaktube-qr-copy-btn">📋 Bağlantıyı Kopyala</button>';
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 = '<div class="yaktube-ripple-content">' + (isLeft ? '⏪ 10sn Geri' : '10sn İleri ⏩') + '</div>';
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 // 3.1. Injects Accessible Player Action Bar under the Watch Video
function injectAccessiblePlayerBar() { function injectAccessiblePlayerBar() {
if (!window.location.pathname.includes('/videos/watch/')) return; if (!window.location.pathname.includes('/videos/watch/')) return;
@@ -1169,6 +1660,8 @@ try {
'<button id="yaktube-bar-play-btn" class="yaktube-player-action-btn" aria-label="Oynat veya Duraklat (Alt+K)">⏯️ Oynat/Duraklat</button>' + '<button id="yaktube-bar-play-btn" class="yaktube-player-action-btn" aria-label="Oynat veya Duraklat (Alt+K)">⏯️ Oynat/Duraklat</button>' +
'<button id="yaktube-bar-forward-btn" class="yaktube-player-action-btn" aria-label="10 Saniye İleri Sar (Alt+L)">⏩ 10sn İleri</button>' + '<button id="yaktube-bar-forward-btn" class="yaktube-player-action-btn" aria-label="10 Saniye İleri Sar (Alt+L)">⏩ 10sn İleri</button>' +
'<button id="yaktube-bar-speed-btn" class="yaktube-player-action-btn" aria-label="Oynatma Hızını Değiştir (Alt+.)">⚡ Hız</button>' + '<button id="yaktube-bar-speed-btn" class="yaktube-player-action-btn" aria-label="Oynatma Hızını Değiştir (Alt+.)">⚡ Hız</button>' +
'<button id="yaktube-bar-sleep-btn" class="yaktube-player-action-btn" aria-label="Uyku Zamanlayıcısı (Alt+U)">⏱️ Uyku</button>' +
'<button id="yaktube-bar-qr-btn" class="yaktube-player-action-btn" aria-label="Telefonda Devam Et / QR Kod (Alt+Q)">📱 Telefonda Aç</button>' +
'<button id="yaktube-bar-theater-btn" class="yaktube-player-action-btn" aria-label="Sinema Modunu Aç/Kapat (Alt+T)">🎭 Sinema</button>' + '<button id="yaktube-bar-theater-btn" class="yaktube-player-action-btn" aria-label="Sinema Modunu Aç/Kapat (Alt+T)">🎭 Sinema</button>' +
'<button id="yaktube-bar-info-btn" class="yaktube-player-action-btn" aria-label="Video Bilgisi ve İstatistikleri (Alt+I)">📊 Bilgi</button>' + '<button id="yaktube-bar-info-btn" class="yaktube-player-action-btn" aria-label="Video Bilgisi ve İstatistikleri (Alt+I)">📊 Bilgi</button>' +
'<button id="yaktube-bar-help-btn" class="yaktube-player-action-btn" aria-label="Kısayol Kılavuzu (Alt+H veya F1)">⌨️ Kısayollar</button>'; '<button id="yaktube-bar-help-btn" class="yaktube-player-action-btn" aria-label="Kısayol Kılavuzu (Alt+H veya F1)">⌨️ Kısayollar</button>';
@@ -1213,6 +1706,20 @@ try {
adjustPlaybackSpeed(true); 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 () { bar.querySelector('#yaktube-bar-theater-btn').addEventListener('click', function () {
document.body.classList.toggle('yaktube-theater-mode'); document.body.classList.toggle('yaktube-theater-mode');
var isT = document.body.classList.contains('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) // 4. Global Keydown Listener (Screen-Reader & Turkish Q Optimized)
window.addEventListener('keydown', function (e) { window.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { 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'); var shortcutsModal = document.getElementById('yaktube-shortcuts-dialog');
if (shortcutsModal && shortcutsModal.style.display !== 'none') { if (shortcutsModal && shortcutsModal.style.display !== 'none') {
shortcutsModal.style.display = 'none'; shortcutsModal.style.display = 'none';
@@ -1256,6 +1775,20 @@ try {
return; 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 // Help Dialog: Alt+H, F1 or '?' key
if ((e.altKey && (e.key === 'h' || e.key === 'H')) || e.key === 'F1' || e.key === '?') { if ((e.altKey && (e.key === 'h' || e.key === 'H')) || e.key === 'F1' || e.key === '?') {
if (!isInputFocused()) { if (!isInputFocused()) {
@@ -1973,20 +2506,31 @@ try {
announce(query + ' için arama başlatıldı.'); announce(query + ' için arama başlatıldı.');
isSearching = true; 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([ Promise.all([
fetch('/api-custom/youtube-search?q=' + encodeURIComponent(query)) fetch('/api-custom/youtube-search?q=' + encodeURIComponent(query), fetchOpts)
.then(function (res) { .then(function (res) {
return res.json(); return res.json();
}) })
.catch(function (err) { .catch(function (err) {
if (err && err.name === 'AbortError') return { results: [] };
console.warn('[YakTube] YouTube search fetch error:', err); console.warn('[YakTube] YouTube search fetch error:', err);
return { results: [] }; 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) { .then(function (res) {
return res.json(); return res.json();
}) })
.catch(function (err) { .catch(function (err) {
if (err && err.name === 'AbortError') return { total: 0, data: [] };
console.warn('[YakTube] Local videos search fetch error:', err); console.warn('[YakTube] Local videos search fetch error:', err);
return { total: 0, data: [] }; return { total: 0, data: [] };
}) })
@@ -2800,6 +3344,9 @@ try {
if (inp) inp.value = transcript; if (inp) inp.value = transcript;
if (ev.results[0].isFinal) { if (ev.results[0].isFinal) {
if (handleVoiceAssistantCommand(transcript)) {
return;
}
announce('"' + transcript + '" için arama başlatılıyor...', true); announce('"' + transcript + '" için arama başlatılıyor...', true);
setTimeout(function () { setTimeout(function () {
closeFullSearchModal(); closeFullSearchModal();
@@ -3636,28 +4183,7 @@ try {
menuContainer.appendChild(sidebarLink); menuContainer.appendChild(sidebarLink);
} }
setInterval(function () { // 9. Background Play (Screen-Off / Minimize) & MediaSession Lock-Screen Controls
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() {
var videoState = { var videoState = {
hasPlayed: false, hasPlayed: false,
explicitlyPausedByClick: false, explicitlyPausedByClick: false,
@@ -3668,6 +4194,10 @@ try {
if (!video || video.hasAttribute('data-yaktube-bgplay-hooked')) return; if (!video || video.hasAttribute('data-yaktube-bgplay-hooked')) return;
video.setAttribute('data-yaktube-bgplay-hooked', 'true'); video.setAttribute('data-yaktube-bgplay-hooked', 'true');
try {
initTouchSeekGesture(video);
} catch (e) {}
video.addEventListener('play', function () { video.addEventListener('play', function () {
videoState.hasPlayed = true; videoState.hasPlayed = true;
videoState.explicitlyPausedByClick = false; videoState.explicitlyPausedByClick = false;
@@ -3791,11 +4321,127 @@ try {
} catch (e) {} } 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 () { setInterval(function () {
var video = getVideoElement(); scheduleUnifiedCycle(false, 0);
if (video) attachVideoListeners(video); }, 3500);
}, 1000);
// Initial trigger
runUnifiedCycle(true);
})(); })();
} catch (err) { } catch (err) {
console.error('[YakTube] Safe catch error:', err); console.error('[YakTube] Safe catch error:', err);
+95 -3
View File
@@ -1,10 +1,101 @@
const { execFile } = require('child_process'); const { execFile } = require('child_process');
const path = require('path'); const path = require('path');
const http = require('http'); const http = require('http');
const fs = require('fs');
const searchCache = new Map(); const searchCache = new Map();
const CACHE_TTL = 30 * 60 * 1000; // 30 minutes 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) { function getCached(q) {
const item = searchCache.get(q); const item = searchCache.get(q);
if (item && Date.now() - item.time < CACHE_TTL) { if (item && Date.now() - item.time < CACHE_TTL) {
@@ -99,7 +190,7 @@ async function getAdminToken() {
async function register({ getRouter, peertubeHelpers, logger }) { async function register({ getRouter, peertubeHelpers, logger }) {
const router = getRouter(); const router = getRouter();
const ytdlpPath = 'D:\\yaktube_storage\\bin\\yt-dlp.exe'; const ytdlpPath = resolveYtDlpPath(logger);
router.get('/search', async (req, res) => { router.get('/search', async (req, res) => {
const query = (req.query.q || '').trim(); const query = (req.query.q || '').trim();
@@ -157,16 +248,17 @@ async function register({ getRouter, peertubeHelpers, logger }) {
}); });
router.post('/import', async (req, res) => { 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'))) { if (!targetUrl || (!targetUrl.includes('youtube.com') && !targetUrl.includes('youtu.be'))) {
return res.status(400).json({ error: 'Invalid YouTube URL' }); return res.status(400).json({ error: 'Invalid YouTube URL' });
} }
try { try {
const token = await getAdminToken(); const token = await getAdminToken();
const resolvedChannelId = channelId ? String(channelId) : await getDefaultChannelId(category);
const postData = new URLSearchParams({ const postData = new URLSearchParams({
targetUrl: targetUrl, targetUrl: targetUrl,
channelId: '1146', // Türkçe Müzik channel channelId: resolvedChannelId,
privacy: '1' privacy: '1'
}).toString(); }).toString();
+6 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "peertube-plugin-yaktube-ondemand", "name": "peertube-plugin-yaktube-ondemand",
"version": "1.4.33", "version": "1.5.0",
"description": "Accessible On-Demand YouTube Import, Live Streams, Smart Recommendations and A11y Engine for PeerTube", "description": "Accessible On-Demand YouTube Import, Live Streams, Smart Recommendations, Sleep Timer, Touch Gestures, Voice Assistant & A11y Engine for PeerTube",
"main": "main.js", "main": "main.js",
"library": "./main.js", "library": "./main.js",
"staticDirs": {}, "staticDirs": {},
@@ -30,6 +30,10 @@
"accessibility", "accessibility",
"a11y", "a11y",
"screen-reader", "screen-reader",
"sleep-timer",
"touch-gestures",
"voice-assistant",
"qr-transfer",
"fediverse", "fediverse",
"voice-search", "voice-search",
"speech-recognition", "speech-recognition",
+243
View File
@@ -1097,3 +1097,246 @@ body:has(#yaktube-ondemand-container) my-error-page {
outline: 2px solid #ff8f37; outline: 2px solid #ff8f37;
outline-offset: 2px; 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;
}