2026-09-04 12:54:11 +03:00
// 100% Incompatible Browser Suppression & Error Guardian
// CRITICAL: PeerTube's Angular app (my-app ngOnInit) relies on document.getElementById('incompatible-browser')
// to exist so it can do .className += " browser-ok" before calling this.loadUser()!
// Removing it from DOM causes a null pointer crash that breaks user session initialization!
if ( typeof window !== 'undefined' ) {
try {
window . displayIncompatibleBrowser = function () {};
var neuterIncompatible = function () {
var el = document . getElementById ( 'incompatible-browser' );
if ( el ) {
if ( ! el . classList . contains ( 'browser-ok' )) {
el . classList . add ( 'browser-ok' );
2026-09-04 06:42:38 +03:00
}
2026-09-04 12:54:11 +03:00
el . style . setProperty ( 'display' , 'none' , 'important' );
el . style . setProperty ( 'visibility' , 'hidden' , 'important' );
el . style . setProperty ( 'height' , '0px' , 'important' );
el . style . setProperty ( 'width' , '0px' , 'important' );
el . style . setProperty ( 'opacity' , '0' , 'important' );
el . style . setProperty ( 'pointer-events' , 'none' , 'important' );
} else {
try {
var dummy = document . createElement ( 'div' );
dummy . id = 'incompatible-browser' ;
dummy . className = 'browser-ok' ;
dummy . style . display = 'none' ;
( document . body || document . documentElement ). appendChild ( dummy );
} catch ( e ) {}
2026-09-04 06:42:38 +03:00
}
2026-09-04 12:54:11 +03:00
};
neuterIncompatible ();
if ( document . readyState === 'loading' ) {
document . addEventListener ( 'DOMContentLoaded' , neuterIncompatible );
2026-09-03 18:19:15 +03:00
}
2026-09-19 05:14:11 +03:00
var neuterInterval = setInterval ( neuterIncompatible , 1000 );
setTimeout ( function () {
clearInterval ( neuterInterval );
}, 5000 );
2026-09-04 12:54:11 +03:00
} catch ( e ) {}
}
// Immediate URL rewrite: redirect legacy /search/videos to /search
if ( typeof window !== 'undefined' && window . location ) {
if ( window . location . pathname === '/search/videos' || window . location . pathname . startsWith ( '/search/videos/' )) {
var targetSearch = window . location . search || '' ;
window . location . replace ( '/search' + targetSearch );
2026-09-03 18:19:15 +03:00
}
2026-09-04 12:54:11 +03:00
}
2026-08-31 18:48:19 +03:00
try {
( function () {
if ( window . __YAKTUBE_ENGINE_LOADED__ ) return ;
window . __YAKTUBE_ENGINE_LOADED__ = true ;
2026-08-31 20:52:34 +03:00
try {
localStorage . setItem ( 'hide-mobile-msg' , 'true' );
sessionStorage . setItem ( 'hide-mobile-msg' , 'true' );
} catch ( e ) {}
2026-08-31 18:48:19 +03:00
console . log ( '[YakTube] YakNet SSO, Custom Branding, A11y & On-Demand Engine Active' );
2026-08-29 12:42:36 +03:00
2026-09-05 15:48:27 +03:00
// Ensure Fediverse Global Search (SepiaSearch) is default in PeertubeServerConfig
try {
2026-09-19 05:20:33 +03:00
if (
window . PeertubeServerConfig &&
window . PeertubeServerConfig . search &&
window . PeertubeServerConfig . search . searchIndex
) {
2026-09-05 15:48:27 +03:00
window . PeertubeServerConfig . search . searchIndex . isDefaultSearch = true ;
}
} catch ( e ) {}
2026-08-31 18:48:19 +03:00
// Dynamic Instance Name Helper
function getInstanceName () {
if (
window . PeertubeServerConfig &&
window . PeertubeServerConfig . instance &&
window . PeertubeServerConfig . instance . name
) {
return window . PeertubeServerConfig . instance . name ;
2026-08-29 12:42:36 +03:00
}
2026-08-31 18:48:19 +03:00
var metaSite = document . querySelector ( 'meta[property="og:site_name"]' );
if ( metaSite && metaSite . content ) return metaSite . content ;
var brandEl = document . querySelector ( '.header-left .brand-name, .instance-name, a.brand' );
if ( brandEl && brandEl . textContent ) return brandEl . textContent . trim ();
return 'YakTube' ;
2026-08-29 12:42:36 +03:00
}
2026-09-06 08:13:21 +03:00
// 1. Screen Reader Live Announcer (W3C WCAG 2.2 AAA Compliant Auto-Clearing Engine)
function applySrOnlyStyles ( el ) {
el . className = 'yaktube-sr-only' ;
el . style . position = 'absolute' ;
el . style . width = '1px' ;
el . style . height = '1px' ;
el . style . padding = '0' ;
el . style . margin = '-1px' ;
el . style . overflow = 'hidden' ;
el . style . clip = 'rect(0, 0, 0, 0)' ;
el . style . whiteSpace = 'nowrap' ;
el . style . border = '0' ;
}
function getLiveRegion ( assertive ) {
var id = assertive ? 'yaktube-a11y-live-assertive' : 'yaktube-a11y-live-region' ;
var region = document . getElementById ( id );
if ( ! region || ! document . body . contains ( region )) {
region = document . createElement ( 'div' );
region . id = id ;
region . setAttribute ( 'aria-live' , assertive ? 'assertive' : 'polite' );
region . setAttribute ( 'aria-atomic' , 'true' );
region . setAttribute ( 'aria-relevant' , 'additions text' );
applySrOnlyStyles ( region );
document . body . appendChild ( region );
2026-08-31 18:48:19 +03:00
}
2026-09-06 08:13:21 +03:00
return region ;
2026-08-29 12:42:36 +03:00
}
2026-08-31 18:48:19 +03:00
function announce ( text , assertive ) {
2026-09-06 08:13:21 +03:00
if ( ! text || ! String ( text ). trim ()) return ;
var strText = String ( text ). trim ();
var region = getLiveRegion ( assertive );
if ( region . _setTimer ) {
clearTimeout ( region . _setTimer );
region . _setTimer = null ;
2026-08-31 18:48:19 +03:00
}
2026-09-06 08:13:21 +03:00
if ( region . _clearTimer ) {
clearTimeout ( region . _clearTimer );
region . _clearTimer = null ;
}
// Screen reader mutation trigger: clear first so repeat announcements are re-announced
2026-08-31 18:48:19 +03:00
region . textContent = '' ;
2026-09-06 08:13:21 +03:00
region . _setTimer = setTimeout ( function () {
region . textContent = strText ;
// Seslendirme yapı ldı ktan veya ekran okuyucu kuyruğuna alı ndı ktan sonra
// sayfanı n en altı nda ya da sanal imleç gezintisinde eski metin kalmaması için
// DOM'daki duyuru metnini temizle (Metin uzunluğuna göre 3 ile 7 saniye arası )
var clearDelay = Math . min ( 7000 , Math . max ( 3000 , strText . length * 50 ));
region . _clearTimer = setTimeout ( function () {
if ( region && document . body . contains ( region )) {
region . textContent = '' ;
}
region . _clearTimer = null ;
}, clearDelay );
region . _setTimer = null ;
2026-08-31 18:48:19 +03:00
}, 50 );
}
2026-08-29 12:42:36 +03:00
2026-08-31 20:35:08 +03:00
// Load official YakNet Web Component definition
if (
typeof customElements !== 'undefined' &&
! customElements . get ( 'yaknet-account' ) &&
! document . getElementById ( 'yaknet-account-script' )
) {
var s = document . createElement ( 'script' );
s . id = 'yaknet-account-script' ;
s . src = 'https://auth.yakhub.com.tr/js/yaknet-account.js' ;
s . async = true ;
document . head . appendChild ( s );
}
2026-09-10 09:57:59 +03:00
// Intercept and sanitize YakNet Account Web Component popup menu
function hookYakNetAccountComponent () {
if ( typeof customElements === 'undefined' ) return ;
2026-09-19 05:20:33 +03:00
customElements
. whenDefined ( 'yaknet-account' )
. then ( function () {
var YakNetAccount = customElements . get ( 'yaknet-account' );
if ( ! YakNetAccount || ! YakNetAccount . prototype || YakNetAccount . prototype . _yaktubeSanitizerHooked ) return ;
YakNetAccount . prototype . _yaktubeSanitizerHooked = true ;
var originalRender = YakNetAccount . prototype . render ;
if ( typeof originalRender === 'function' ) {
YakNetAccount . prototype . render = function () {
originalRender . call ( this );
try {
if ( this . shadowRoot ) {
var isOfficial = isOfficialYakTubeServer ();
// 1. In YakTube: Suppress redundant/recursive link to YakTube inside services list
var selfLinks = this . shadowRoot . querySelectorAll ( 'a[href*="yaktube.yakhub.com.tr"]' );
selfLinks . forEach ( function ( a ) {
a . style . display = 'none' ;
});
// 2. In third-party PeerTube instances: Suppress entire external services grid & header to avoid community backlash
if ( ! isOfficial ) {
var appsGrid = this . shadowRoot . querySelector ( '.yn-apps-grid' );
if ( appsGrid ) appsGrid . style . display = 'none' ;
var sectionTitle = this . shadowRoot . querySelector ( '#yn-services-label, .yn-section-title' );
if ( sectionTitle ) sectionTitle . style . display = 'none' ;
}
2026-09-10 09:57:59 +03:00
}
2026-09-19 05:20:33 +03:00
} catch ( e ) {}
};
}
})
. catch ( function () {});
2026-09-10 09:57:59 +03:00
}
hookYakNetAccountComponent ();
2026-08-31 20:35:08 +03:00
// Silent background SSO Probe Engine
function initSilentSSOProbe () {
var token = localStorage . getItem ( 'access_token' );
var isManualLogout = sessionStorage . getItem ( 'yaknet_manual_logout' );
if (
token ||
isManualLogout ||
window . location . search . includes ( 'local=true' ) ||
window . location . search . includes ( 'externalAuthError' ) ||
window . location . pathname . includes ( '/login' )
) {
return ;
}
var iframeId = 'yaknet-sso-iframe-instance' ;
if ( ! document . getElementById ( iframeId )) {
var iframe = document . createElement ( 'iframe' );
iframe . id = iframeId ;
iframe . src = 'https://auth.yakhub.com.tr/sdk/sso-frame' ;
iframe . title = 'YakNet Oturum Kontrolü' ;
iframe . setAttribute ( 'aria-hidden' , 'true' );
iframe . tabIndex = - 1 ;
iframe . style . cssText =
'display:none!important;width:0!important;height:0!important;border:0!important;position:absolute!important;pointer-events:none!important;' ;
document . body . appendChild ( iframe );
}
}
if ( document . readyState === 'loading' ) {
document . addEventListener ( 'DOMContentLoaded' , initSilentSSOProbe );
} else {
initSilentSSOProbe ();
}
2026-08-31 18:48:19 +03:00
function triggerYakNetOAuth () {
sessionStorage . removeItem ( 'yaknet_manual_logout' );
var callbackUrl = 'https://yaktube.yakhub.com.tr/plugins/peertube-plugin-auth-yaknet/router/auth-callback' ;
window . location . href =
'https://auth.yakhub.com.tr/oauth/authorize?client_id=01a03c41-f758-721e-b927-619bffde5c23&redirect_uri=' +
encodeURIComponent ( callbackUrl ) +
'&response_type=code&scope=' ;
}
// Handle direct postMessage from YakNet SSO frame
window . addEventListener ( 'message' , function ( ev ) {
if ( ev . data && ev . data . type === 'YAKNET_SSO_STATUS' ) {
if ( ev . data . authenticated && ev . data . user ) {
var token = localStorage . getItem ( 'access_token' );
if (
! token &&
! window . location . search . includes ( 'local=true' ) &&
! window . location . search . includes ( 'externalAuthError' )
) {
sessionStorage . removeItem ( 'yaknet_manual_logout' );
console . log ( '[YakTube] Live YakNet session detected via SSO frame! Connecting...' , ev . data . user );
triggerYakNetOAuth ();
}
} else if ( ev . data . authenticated === false ) {
var hasToken = localStorage . getItem ( 'access_token' ) || localStorage . getItem ( 'username' );
if ( hasToken ) {
console . log ( '[YakTube] YakNet session ended elsewhere. Clearing YakTube...' );
2026-09-04 07:32:05 +03:00
clearYakTubeAuthSession ();
2026-08-31 18:48:19 +03:00
window . location . reload ();
}
}
}
});
// Global listener for YakNet SSO broadcast from auth.yakhub.com.tr
window . addEventListener ( 'yaknet-auth' , function ( e ) {
if ( e . detail && e . detail . authenticated && e . detail . user ) {
2026-08-29 12:42:36 +03:00
var token = localStorage . getItem ( 'access_token' );
if (
! token &&
! window . location . search . includes ( 'local=true' ) &&
! window . location . search . includes ( 'externalAuthError' )
) {
sessionStorage . removeItem ( 'yaknet_manual_logout' );
2026-08-31 18:48:19 +03:00
console . log ( '[YakTube] Initiating automatic single-sign-on handshake for:' , e . detail . user );
2026-08-29 12:42:36 +03:00
triggerYakNetOAuth ();
}
2026-08-31 18:48:19 +03:00
} else if ( e . detail && e . detail . authenticated === false ) {
2026-08-29 12:42:36 +03:00
var hasToken = localStorage . getItem ( 'access_token' ) || localStorage . getItem ( 'username' );
if ( hasToken ) {
2026-09-04 07:32:05 +03:00
clearYakTubeAuthSession ();
2026-08-29 12:42:36 +03:00
window . location . reload ();
}
}
2026-08-31 14:04:28 +03:00
});
2026-09-04 07:32:05 +03:00
// Clear YakTube auth session and cloud search cache safely
function clearYakTubeAuthSession () {
try {
localStorage . removeItem ( 'access_token' );
localStorage . removeItem ( 'refresh_token' );
localStorage . removeItem ( 'token_type' );
localStorage . removeItem ( 'id' );
localStorage . removeItem ( 'username' );
localStorage . removeItem ( 'displayName' );
localStorage . removeItem ( 'email' );
localStorage . removeItem ( 'role' );
localStorage . removeItem ( 'yaktube_recent_searches' );
sessionStorage . clear ();
} catch ( e ) {}
}
2026-08-31 18:48:19 +03:00
// Unified single sign-out for YakTube and YakNet
function performUnifiedLogout () {
2026-09-04 07:32:05 +03:00
clearYakTubeAuthSession ();
2026-08-31 14:04:28 +03:00
try {
2026-08-31 18:48:19 +03:00
sessionStorage . setItem ( 'yaknet_manual_logout' , 'true' );
} catch ( err ) {}
window . location . replace (
'https://auth.yakhub.com.tr/sdk/logout?redirect_uri=' + encodeURIComponent ( 'https://yaktube.yakhub.com.tr' )
);
2026-08-29 12:42:36 +03:00
}
2026-08-31 18:48:19 +03:00
document . addEventListener (
'click' ,
function ( e ) {
var el = e . target ;
if ( el && el . closest ) {
var isLogout = el . closest (
'a[href*="logout"], button[title*="Log out"], button[title*="Çıkış"], .logout-button, [class*="logout"], [aria-label*="Çıkış"], [aria-label*="Log out"]'
2026-08-29 12:42:36 +03:00
);
2026-08-31 18:48:19 +03:00
if ( isLogout ) {
2026-08-29 12:42:36 +03:00
e . preventDefault ();
2026-08-31 18:48:19 +03:00
e . stopPropagation ();
performUnifiedLogout ();
return ;
2026-08-29 12:42:36 +03:00
}
2026-08-31 18:48:19 +03:00
var isLogin = el . closest (
'my-login-link, .login-button, a[href*="/login"]:not(#yaktube-show-admin-login), a[aria-label*="Giriş"], a[title*="Giriş"], a[title*="login"], a[aria-label*="login"], .yaknet-sso-login-btn'
);
if ( isLogin && ! window . location . search . includes ( 'local=true' )) {
e . preventDefault ();
e . stopPropagation ();
sessionStorage . removeItem ( 'yaknet_manual_logout' );
triggerYakNetOAuth ();
2026-08-29 12:42:36 +03:00
return ;
}
}
2026-08-31 18:48:19 +03:00
},
true
);
2026-08-29 12:42:36 +03:00
2026-08-31 18:48:19 +03:00
// 2.3 Dedicated Local Admin Login Modal (Alt+A)
function openLocalAdminLoginModal () {
var existingModal = document . getElementById ( 'yaktube-admin-login-modal' );
if ( existingModal ) {
existingModal . style . display = 'flex' ;
var uInput = existingModal . querySelector ( '#yaktube-admin-username' );
if ( uInput ) uInput . focus ();
return ;
2026-08-29 12:42:36 +03:00
}
2026-08-31 18:48:19 +03:00
var modal = document . createElement ( 'div' );
modal . id = 'yaktube-admin-login-modal' ;
2026-08-29 12:42:36 +03:00
modal . className = 'yaktube-modal-overlay' ;
modal . setAttribute ( 'role' , 'dialog' );
modal . setAttribute ( 'aria-modal' , 'true' );
2026-08-31 18:48:19 +03:00
modal . setAttribute ( 'aria-labelledby' , 'yaktube-admin-modal-title' );
2026-08-29 12:42:36 +03:00
modal . innerHTML =
2026-08-31 18:48:19 +03:00
'<div class="yaktube-modal-card" style="max-width: 420px; background:#181824; border:1px solid rgba(255,143,55,0.4); border-radius:18px; padding:26px; color:#fff; box-shadow:0 16px 40px rgba(0,0,0,0.8); text-align:left;">' +
'<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:18px;">' +
'<h3 id="yaktube-admin-modal-title" style="margin:0; font-size:18px; color:#ff8f37;">⚙️ Sunucu Yöneticisi Girişi</h3>' +
'<button id="yaktube-admin-modal-close" class="yaktube-voice-modal-close-btn" aria-label="Kapat">✕</button>' +
2026-08-30 02:21:21 +03:00
'</div>' +
2026-08-31 18:48:19 +03:00
'<p style="font-size:13px; color:#aaa; margin-bottom:16px;">Yerel yönetici (admin) kullanı cı adı ve şifrenizle giriş yaparak PeerTube yönetim paneline erişebilirsiniz.</p>' +
'<form id="yaktube-admin-direct-form" style="display:flex; flex-direction:column; gap:12px;">' +
'<div style="display:flex; flex-direction:column; gap:4px;">' +
'<label for="yaktube-admin-username" style="font-size:13px; font-weight:600; color:#ddd;">Kullanı cı Adı veya E-posta:</label>' +
'<input type="text" id="yaktube-admin-username" required style="background:#22222e; border:1px solid #444; border-radius:8px; padding:10px 12px; color:#fff; font-size:14px; outline:none;" />' +
'</div>' +
'<div style="display:flex; flex-direction:column; gap:4px;">' +
'<label for="yaktube-admin-password" style="font-size:13px; font-weight:600; color:#ddd;">Şifre:</label>' +
'<input type="password" id="yaktube-admin-password" required style="background:#22222e; border:1px solid #444; border-radius:8px; padding:10px 12px; color:#fff; font-size:14px; outline:none;" />' +
'</div>' +
'<div id="yaktube-admin-login-error" style="color:#ef4444; font-size:13px; display:none; margin-top:4px;"></div>' +
'<button type="submit" id="yaktube-admin-login-submit" style="background:linear-gradient(135deg, #ff8f37 0%, #ff5e3a 100%); color:#fff; border:none; border-radius:8px; padding:12px; font-weight:700; font-size:14px; cursor:pointer; margin-top:6px;">Yönetici Olarak Giriş Yap</button>' +
'</form>' +
2026-08-29 12:42:36 +03:00
'</div>' ;
2026-08-31 18:48:19 +03:00
2026-08-29 12:42:36 +03:00
document . body . appendChild ( modal );
2026-08-31 18:48:19 +03:00
modal . querySelector ( '#yaktube-admin-modal-close' ). addEventListener ( 'click' , function () {
modal . style . display = 'none' ;
});
2026-08-30 02:21:21 +03:00
modal . addEventListener ( 'click' , function ( e ) {
if ( e . target === modal ) modal . style . display = 'none' ;
2026-08-29 12:42:36 +03:00
});
2026-08-31 18:48:19 +03:00
var form = modal . querySelector ( '#yaktube-admin-direct-form' );
var errEl = modal . querySelector ( '#yaktube-admin-login-error' );
var uInput = modal . querySelector ( '#yaktube-admin-username' );
var pInput = modal . querySelector ( '#yaktube-admin-password' );
var sBtn = modal . querySelector ( '#yaktube-admin-login-submit' );
2026-08-29 12:42:36 +03:00
2026-08-31 18:48:19 +03:00
uInput . focus ();
2026-08-29 12:42:36 +03:00
2026-08-31 18:48:19 +03:00
form . addEventListener ( 'submit' , async function ( ev ) {
ev . preventDefault ();
errEl . style . display = 'none' ;
sBtn . disabled = true ;
sBtn . innerText = 'Giriş Yapı lı yor...' ;
2026-08-29 12:42:36 +03:00
2026-08-31 18:48:19 +03:00
try {
var clientRes = await fetch ( '/api/v1/oauth-clients/local' );
var clientData = await clientRes . json ();
2026-08-29 12:42:36 +03:00
2026-08-31 18:48:19 +03:00
var bodyParams = new URLSearchParams ({
client_id : clientData . client_id ,
client_secret : clientData . client_secret ,
grant_type : 'password' ,
username : uInput . value . trim (),
password : pInput . value
});
2026-08-30 02:21:21 +03:00
2026-08-31 18:48:19 +03:00
var tokenRes = await fetch ( '/api/v1/users/token' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/x-www-form-urlencoded' },
body : bodyParams . toString ()
});
2026-08-30 02:21:21 +03:00
2026-08-31 18:48:19 +03:00
var tokenData = await tokenRes . json ();
if ( ! tokenRes . ok || ! tokenData . access_token ) {
throw new Error ( tokenData . error_description || tokenData . error || 'Kullanı cı adı veya şifre hatalı .' );
}
2026-08-30 02:21:21 +03:00
2026-08-31 18:48:19 +03:00
// Store tokens
localStorage . setItem ( 'access_token' , tokenData . access_token );
localStorage . setItem ( 'refresh_token' , tokenData . refresh_token );
localStorage . setItem ( 'token_type' , tokenData . token_type || 'Bearer' );
localStorage . setItem ( 'username' , uInput . value . trim ());
2026-08-30 02:21:21 +03:00
2026-08-31 18:48:19 +03:00
// Get user details
var meRes = await fetch ( '/api/v1/users/me' , {
headers : { Authorization : 'Bearer ' + tokenData . access_token }
});
if ( meRes . ok ) {
var meData = await meRes . json ();
localStorage . setItem ( 'email' , meData . email || '' );
localStorage . setItem ( 'displayName' , meData . displayName || meData . username || '' );
}
2026-08-30 02:21:21 +03:00
2026-08-31 18:48:19 +03:00
announce ( 'Yönetici girişi başarı lı ! Yönetim paneline yönlendiriliyorsunuz.' , true );
window . location . href = '/admin' ;
} catch ( err ) {
errEl . innerText = err . message ;
errEl . style . display = 'block' ;
sBtn . disabled = false ;
sBtn . innerText = 'Yönetici Olarak Giriş Yap' ;
}
2026-08-30 11:38:46 +03:00
});
}
2026-08-31 18:48:19 +03:00
// 2. YakNet SSO Header & Login Flow
function injectYakNetSSO () {
// 2.1 Header: Place YakNet Account widget in top-right .buttons-container
var headerRight =
document . querySelector ( 'my-header .buttons-container' ) ||
document . querySelector ( '.buttons-container' ) ||
document . querySelector ( 'my-header .header-right' ) ||
document . querySelector ( 'my-header .user-menu' ) ||
document . querySelector ( 'my-header .action-buttons' ) ||
document . querySelector ( '.header-right' ) ||
document . querySelector ( 'header .right' );
var token = localStorage . getItem ( 'access_token' );
var uName = localStorage . getItem ( 'username' );
var uEmail = localStorage . getItem ( 'email' );
var displayName = localStorage . getItem ( 'displayName' ) || uName || '' ;
var emailStr = uEmail || '' ;
2026-09-04 07:07:34 +03:00
var isAuth = !! token && !! uName ;
2026-08-31 18:48:19 +03:00
if ( headerRight ) {
var liveWidget = headerRight . querySelector ( '#yaknet-header-account-widget' );
2026-08-31 20:29:13 +03:00
if ( isAuth ) {
2026-09-04 06:55:20 +03:00
// 1. AUTHENTICATED: Add class to body & Hide ALL native login links completely
document . body . classList . add ( 'yaktube-user-logged-in' );
document
. querySelectorAll ( 'my-login-link, a[href*="/login"], .login-button, [class*="login-link"]' )
. forEach ( function ( btn ) {
btn . style . setProperty ( 'display' , 'none' , 'important' );
btn . setAttribute ( 'aria-hidden' , 'true' );
btn . setAttribute ( 'tabindex' , '-1' );
});
2026-08-31 20:29:13 +03:00
2026-09-10 09:57:59 +03:00
if ( ! liveWidget && isOfficialYakTubeServer ()) {
2026-08-31 20:29:13 +03:00
var widgetWrap = document . createElement ( 'div' );
widgetWrap . id = 'yaknet-header-account-widget' ;
widgetWrap . className = 'yaknet-header-widget' ;
widgetWrap . style . display = 'inline-flex' ;
widgetWrap . style . alignItems = 'center' ;
widgetWrap . style . marginRight = '8px' ;
widgetWrap . innerHTML =
'<yaknet-account ' +
'client-id="01a03c41-f758-721e-b927-619bffde5c23" ' +
'redirect-uri="https://yaktube.yakhub.com.tr/plugins/peertube-plugin-auth-yaknet/router/auth-callback" ' +
'base-url="https://auth.yakhub.com.tr" ' +
'login-url="/login" ' +
'authenticated="true" ' +
'user-name="' +
displayName . replace ( /"/g , '"' ) +
'" ' +
'user-email="' +
emailStr . replace ( /"/g , '"' ) +
'" ' +
'theme="dark">' +
'</yaknet-account>' ;
var targetSibling =
headerRight . querySelector ( 'my-user-menu' ) ||
headerRight . querySelector ( '.user-menu' ) ||
headerRight . querySelector ( '.settings-button' ) ||
headerRight . querySelector ( 'my-user-notifications' ) ||
headerRight . querySelector ( 'my-notification-bell' );
if ( targetSibling && targetSibling . parentElement === headerRight ) {
headerRight . insertBefore ( widgetWrap , targetSibling );
} else {
headerRight . prepend ( widgetWrap );
}
2026-09-10 09:57:59 +03:00
} else if ( liveWidget ) {
if ( ! isOfficialYakTubeServer ()) {
liveWidget . remove ();
} else {
var el = liveWidget . querySelector ( 'yaknet-account' );
if ( el ) {
if ( el . getAttribute ( 'authenticated' ) !== 'true' || el . getAttribute ( 'user-name' ) !== displayName ) {
el . setAttribute ( 'authenticated' , 'true' );
el . setAttribute ( 'user-name' , displayName );
el . setAttribute ( 'user-email' , emailStr );
if ( typeof el . syncStateFromAttributes === 'function' ) {
el . syncStateFromAttributes ();
el . render ();
}
2026-08-31 20:29:13 +03:00
}
2026-09-10 09:57:59 +03:00
if ( el . shadowRoot ) {
// Direct DOM safety: remove self-link on polling cycle too
var selfLinks = el . shadowRoot . querySelectorAll ( 'a[href*="yaktube.yakhub.com.tr"]' );
selfLinks . forEach ( function ( a ) {
a . style . display = 'none' ;
});
var shadowLogoutBtn =
el . shadowRoot . getElementById ( 'logoutBtn' ) ||
el . shadowRoot . querySelector ( '.yn-logout-btn' ) ||
el . shadowRoot . querySelector ( 'button[title*="Çıkış"], button[aria-label*="Çıkış"]' );
if ( shadowLogoutBtn && ! shadowLogoutBtn . hasAttribute ( 'data-yaktube-hooked' )) {
shadowLogoutBtn . setAttribute ( 'data-yaktube-hooked' , 'true' );
shadowLogoutBtn . addEventListener (
'click' ,
function ( ev ) {
ev . preventDefault ();
ev . stopPropagation ();
performUnifiedLogout ();
},
true
);
}
2026-08-31 20:29:13 +03:00
}
2026-08-31 20:20:55 +03:00
}
}
2026-08-31 20:29:13 +03:00
}
} else {
// 2. GUEST / NOT LOGGED IN:
2026-09-04 06:55:20 +03:00
document . body . classList . remove ( 'yaktube-user-logged-in' );
2026-08-31 20:29:13 +03:00
if ( liveWidget ) {
liveWidget . remove ();
}
2026-08-31 18:48:19 +03:00
2026-09-04 06:55:20 +03:00
document
. querySelectorAll ( 'my-login-link, a[href*="/login"], .login-button, [class*="login-link"]' )
. forEach ( function ( btn ) {
btn . style . display = '' ;
btn . removeAttribute ( 'aria-hidden' );
btn . removeAttribute ( 'tabindex' );
2026-08-31 20:29:13 +03:00
2026-09-04 06:55:20 +03:00
var loginAnchor = btn . tagName . toLowerCase () === 'a' ? btn : btn . querySelector ( 'a' );
if ( loginAnchor ) {
loginAnchor . setAttribute ( 'aria-label' , 'YakNet ile Giriş Yap' );
loginAnchor . setAttribute ( 'title' , 'YakNet ile Giriş Yap' );
var spanText = loginAnchor . querySelector ( 'span' );
if ( spanText ) {
spanText . textContent = 'YakNet ile Giriş Yap' ;
} else {
loginAnchor . textContent = 'YakNet ile Giriş Yap' ;
}
2026-08-31 18:48:19 +03:00
}
2026-09-04 06:55:20 +03:00
});
2026-08-31 18:48:19 +03:00
}
2026-08-30 11:38:46 +03:00
}
2026-08-31 18:48:19 +03:00
// 2.2 Login Modal / Page
var loginCard =
document . querySelector ( 'my-login' ) ||
document . querySelector ( '.login-page' ) ||
document . querySelector ( '.login-container' ) ||
document . querySelector ( 'form.login' ) ||
document . querySelector ( 'form[action*="login"]' ) ||
document . querySelector ( 'my-login-modal' );
if ( loginCard && ! document . getElementById ( 'yaknet-login-section' )) {
var isLocalLogin = window . location . search . includes ( 'local=true' ) || window . location . pathname . includes ( '/admin' );
var ssoBox = document . createElement ( 'div' );
ssoBox . id = 'yaknet-login-section' ;
ssoBox . className = 'yaknet-login-banner' ;
ssoBox . innerHTML =
'<div style="font-size: 19px; font-weight: 700; color: #ff8f37; margin-bottom: 6px;">YakNet ile YakTube\'a Katı lı n</div>' +
'<p style="font-size: 13px; color: #94a3b8; margin-bottom: 16px; line-height: 1.45;">' +
'YouTube gibi tek bir YakNet hesabı yla güvenle giriş yapabilir, kendi video ve müzik kanalları nı zı oluşturabilirsiniz.' +
'</p>' +
'<a href="https://auth.yakhub.com.tr/oauth/authorize?client_id=01a03c41-f758-721e-b927-619bffde5c23&redirect_uri=https%3A%2F%2Fyaktube.yakhub.com.tr%2Fplugins%2Fpeertube-plugin-auth-yaknet%2Frouter%2Fauth-callback&response_type=code&scope=" class="yaknet-sso-login-btn" aria-label="YakNet Hesabı nı z ile Tek Tı kla Giriş Yapı n">' +
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>' +
'<span>YakNet ile Giriş Yap</span>' +
'</a>' +
'<div style="margin-top: 14px; font-size: 13px; color: #94a3b8;">' +
'YakNet hesabı nı z yok mu? <a href="https://auth.yakhub.com.tr/register" target="_blank" rel="noopener noreferrer" style="color: #ff8f37; font-weight: 600; text-decoration: underline;">YakNet Hesabı Oluştur</a>' +
'</div>' +
'<div style="margin-top: 14px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.08); font-size: 11.5px; color: #64748b; line-height: 1.45;">' +
'Giriş yaparak <a href="/about/instance" target="_blank" style="color: #94a3b8; text-decoration: underline;">Kullanı m Koşulları </a>\'nı ve <a href="/about/instance" target="_blank" style="color: #94a3b8; text-decoration: underline;">Gizlilik Politikası </a>\'nı kabul etmiş sayı lı rsı nı z.' +
'</div>' +
'<div class="yaktube-admin-login-link" style="margin-top: 14px;">' +
( isLocalLogin
? '<span>⚙️ Yönetici (Local Admin) Giriş Modu Aktif</span>'
: '<a href="#" id="yaktube-show-admin-login">⚙️ Sunucu Yöneticisi Girişi</a>' ) +
'</div>' ;
var formFirstChild = loginCard . firstElementChild ;
if ( formFirstChild ) {
loginCard . insertBefore ( ssoBox , formFirstChild );
} else {
loginCard . appendChild ( ssoBox );
}
var registerLinks = document . querySelectorAll ( 'a[href*="/signup"], a[href*="/register"]' );
registerLinks . forEach ( function ( l ) {
l . href = 'https://auth.yakhub.com.tr/register' ;
l . target = '_blank' ;
l . innerText = 'YakNet Hesabı Oluştur' ;
});
var formInputs = loginCard . querySelector ( '.form-group, .inputs, form' ) || loginCard . querySelector ( 'input' );
var containerToHide = formInputs ? formInputs . closest ( 'form' ) || formInputs . parentElement : null ;
if ( containerToHide && ! isLocalLogin ) {
var subElements = Array . from ( loginCard . children ). filter ( function ( el ) {
return (
el . id !== 'yaknet-login-section' &&
! el . classList . contains ( 'title' ) &&
! el . classList . contains ( 'page-title' )
);
});
subElements . forEach ( function ( el ) {
el . style . display = 'none' ;
});
var showAdminBtn = document . getElementById ( 'yaktube-show-admin-login' );
if ( showAdminBtn ) {
showAdminBtn . addEventListener ( 'click' , function ( e ) {
e . preventDefault ();
openLocalAdminLoginModal ();
});
}
}
}
}
// 3. Intelligent Branding & A11y DOM Auto-Fixer
function autoFixDOM () {
injectAccessiblePlayerBar ();
// Check if on 403 / unauthorized page and provide admin login button
var notAuthorizedEl = document . querySelector ( 'my-not-found, .not-found, .forbidden, my-forbidden, .error-page' );
if ( notAuthorizedEl && ! document . getElementById ( 'yaktube-403-admin-btn' )) {
var btn403 = document . createElement ( 'div' );
btn403 . id = 'yaktube-403-admin-btn' ;
btn403 . style . margin = '20px auto' ;
btn403 . style . textAlign = 'center' ;
btn403 . innerHTML =
'<button style="background:linear-gradient(135deg, #ff8f37 0%, #ff5e3a 100%); color:#fff; border:none; padding:12px 24px; border-radius:10px; font-weight:700; cursor:pointer; font-size:15px;">🔑 Sunucu Yöneticisi Olarak Giriş Yap (Alt+A)</button>' ;
btn403 . querySelector ( 'button' ). addEventListener ( 'click' , openLocalAdminLoginModal );
notAuthorizedEl . appendChild ( btn403 );
}
injectYakNetSSO ();
2026-09-04 06:55:20 +03:00
// Check auth status for login button rendering
var tokenVal = localStorage . getItem ( 'access_token' );
var userVal = localStorage . getItem ( 'username' );
2026-09-04 07:07:34 +03:00
var isAlreadyAuth = !! tokenVal && !! userVal ;
2026-09-04 06:55:20 +03:00
if ( ! isAlreadyAuth ) {
// Replace all login buttons labels and titles ONLY for guests
document
. querySelectorAll (
'my-login-link, a[href*="/login"], a[title*="Giriş sayfası na git"], [aria-label*="Giriş sayfası na git"]'
)
. forEach ( function ( el ) {
if ( el . getAttribute ( 'title' ) && el . getAttribute ( 'title' ). includes ( 'Giriş sayfası na git' )) {
el . setAttribute ( 'title' , 'YakNet ile Giriş Yap' );
}
if ( el . getAttribute ( 'aria-label' ) && el . getAttribute ( 'aria-label' ). includes ( 'Giriş sayfası na git' )) {
el . setAttribute ( 'aria-label' , 'YakNet ile Giriş Yap' );
}
var anchor = el . tagName . toLowerCase () === 'a' ? el : el . querySelector ( 'a' );
if ( anchor ) {
anchor . setAttribute ( 'title' , 'YakNet ile Giriş Yap' );
anchor . setAttribute ( 'aria-label' , 'YakNet ile Giriş Yap' );
var spans = anchor . querySelectorAll ( 'span' );
spans . forEach ( function ( s ) {
if (
s . textContent . includes ( 'Giriş sayfası na git' ) ||
s . textContent . trim () === 'Giriş' ||
s . textContent . trim () === 'Giriş yap'
) {
s . textContent = 'YakNet ile Giriş Yap' ;
}
});
}
});
}
2026-08-31 18:48:19 +03:00
// Aggressively remove generic Framasoft Open-in-App / Play Store banners
2026-08-31 20:52:34 +03:00
document
. querySelectorAll (
'.mobile-msg, [class*="mobile-msg"], button[title*="Bu mesajı kapat"], button[aria-label*="Bu mesajı kapat"]'
)
. forEach ( function ( m ) {
m . remove ();
});
2026-08-31 18:48:19 +03:00
var genericAppBanners = document . querySelectorAll (
'my-open-app, my-client-app-banner, .open-app-container, .app-banner, [class*="open-app"], [class*="open-in-app"], .mobile-app-banner, my-open-in-app, .app-promotion, a[href*="play.google.com/store/apps/details?id=app.joinpeertube"], a[href*="f-droid.org"]'
);
genericAppBanners . forEach ( function ( banner ) {
banner . setAttribute ( 'aria-hidden' , 'true' );
banner . setAttribute ( 'tabindex' , '-1' );
if ( banner . parentElement ) {
try {
banner . remove ();
} catch ( e ) {
banner . style . display = 'none' ;
}
}
});
// Replace leftover PeerTube branding in visible UI texts
var walker = document . createTreeWalker ( document . body , NodeFilter . SHOW_TEXT , null , false );
var node ;
while (( node = walker . nextNode ())) {
var val = node . nodeValue ;
if ( val && ( val . includes ( 'PeerTube' ) || val . includes ( 'Peertube' ))) {
var parentTag = node . parentElement ? node . parentElement . tagName : '' ;
var parentElem = node . parentElement ;
if (
parentElem &&
( parentElem . closest ( '#yaktube-ondemand-container' ) ||
parentElem . closest ( '#yaktube-live-watch-container' ) ||
parentElem . tagName === 'SCRIPT' ||
parentElem . tagName === 'STYLE' )
) {
continue ;
}
if ( true ) {
node . nodeValue = val
. replace ( /PeerTube hakkı nda/gi , 'YakTube Hakkı nda' )
. replace ( /PeerTube'a katı lı n/gi , "YakTube'a Katı lı n" )
. replace ( /PeerTube örneği/gi , 'YakTube Sunucusu' )
. replace ( /PeerTube/g , 'YakTube' )
. replace ( /Giriş sayfası na git/gi , 'YakNet ile Giriş Yap' );
}
}
}
// Semantic Headings (H3 for video titles, H2 for sections)
document
. querySelectorAll ( '.video-miniature, my-video-miniature, .video-card, .yaktube-card, [class*="miniature"]' )
. forEach ( function ( card ) {
var titleElement = card . querySelector (
'.video-miniature-name, .video-name, .yaktube-video-name, a[title], .title'
);
if ( titleElement ) {
if ( ! [ 'H1' , 'H2' , 'H3' , 'H4' , 'H5' , 'H6' ]. includes ( titleElement . tagName )) {
titleElement . setAttribute ( 'role' , 'heading' );
titleElement . setAttribute ( 'aria-level' , '3' );
}
}
});
document
. querySelectorAll ( '.section-title, .sub-header, .videos-header, .page-header, .yaktube-title-h3' )
. forEach ( function ( sec ) {
if ( ! [ 'H1' , 'H2' ]. includes ( sec . tagName )) {
sec . setAttribute ( 'role' , 'heading' );
sec . setAttribute ( 'aria-level' , '2' );
}
});
// Make native PeerTube comment authors accessible as H3 headings for screen readers
document
. querySelectorAll ( 'my-video-comment, .video-comment, my-video-comment-thread, .comment, .comment-thread' )
. forEach ( function ( commentEl ) {
var authorEl = commentEl . querySelector (
'.display-name, .actor-name, .comment-author, a.actor-link, .video-comment-header-left a, .video-comment-header-left span'
);
if (
authorEl &&
! [ 'H1' , 'H2' , 'H3' , 'H4' , 'H5' , 'H6' ]. includes ( authorEl . tagName ) &&
! authorEl . getAttribute ( 'role' )
) {
authorEl . setAttribute ( 'role' , 'heading' );
authorEl . setAttribute ( 'aria-level' , '3' );
}
});
// Auto-fix unlabeled buttons
document . querySelectorAll ( 'button:not([data-a11y-fixed])' ). forEach ( function ( btn ) {
btn . setAttribute ( 'data-a11y-fixed' , 'true' );
var text = ( btn . textContent || '' ). trim ();
var hasAria = btn . getAttribute ( 'aria-label' ) || btn . getAttribute ( 'title' );
if ( ! text && ! hasAria ) {
var html = btn . innerHTML . toLowerCase ();
var className = ( btn . className || '' ). toLowerCase ();
var icon = btn . querySelector ( 'my-global-icon, i, svg, [class*="icon-"]' );
var iconName = icon ? icon . className || icon . getAttribute ( 'name' ) || '' : '' ;
if ( html . includes ( 'search' ) || className . includes ( 'search' ) || iconName . includes ( 'search' )) {
btn . setAttribute ( 'aria-label' , 'Ara' );
} else if ( html . includes ( 'bell' ) || className . includes ( 'notification' ) || iconName . includes ( 'bell' )) {
btn . setAttribute ( 'aria-label' , 'Bildirimler' );
} else if ( html . includes ( 'cog' ) || html . includes ( 'setting' ) || className . includes ( 'setting' )) {
btn . setAttribute ( 'aria-label' , 'Ayarlar' );
} else if (
html . includes ( 'menu' ) ||
html . includes ( 'bars' ) ||
className . includes ( 'burger' ) ||
className . includes ( 'menu' )
) {
btn . setAttribute ( 'aria-label' , 'Ana Gezinme Menüsü' );
} else if ( html . includes ( 'play' ) || className . includes ( 'play' )) {
btn . setAttribute ( 'aria-label' , 'Oynat' );
} else if ( html . includes ( 'pause' ) || className . includes ( 'pause' )) {
btn . setAttribute ( 'aria-label' , 'Duraklat' );
} else if ( html . includes ( 'volume' ) || html . includes ( 'mute' ) || className . includes ( 'volume' )) {
btn . setAttribute ( 'aria-label' , 'Ses Kontrolü' );
} else if ( html . includes ( 'fullscreen' ) || className . includes ( 'fullscreen' )) {
btn . setAttribute ( 'aria-label' , 'Tam Ekran' );
} else if (
html . includes ( 'ellipsis' ) ||
html . includes ( 'more' ) ||
className . includes ( 'more' ) ||
className . includes ( 'action' )
) {
btn . setAttribute ( 'aria-label' , 'Diğer İşlemler ve Seçenekler' );
} else if ( html . includes ( 'trash' ) || html . includes ( 'delete' ) || className . includes ( 'delete' )) {
btn . setAttribute ( 'aria-label' , 'Sil' );
} else if ( html . includes ( 'edit' ) || className . includes ( 'edit' )) {
btn . setAttribute ( 'aria-label' , 'Düzenle' );
} else if ( html . includes ( 'share' ) || className . includes ( 'share' )) {
btn . setAttribute ( 'aria-label' , 'Paylaş' );
} else if ( html . includes ( 'thumb-up' ) || html . includes ( 'like' ) || className . includes ( 'like' )) {
btn . setAttribute ( 'aria-label' , 'Beğen' );
} else if ( html . includes ( 'thumb-down' ) || html . includes ( 'dislike' ) || className . includes ( 'dislike' )) {
btn . setAttribute ( 'aria-label' , 'Beğenme' );
} else if ( html . includes ( 'filter' ) || className . includes ( 'filter' )) {
btn . setAttribute ( 'aria-label' , 'Filtreler' );
} else if ( html . includes ( 'close' ) || html . includes ( 'times' ) || className . includes ( 'close' )) {
btn . setAttribute ( 'aria-label' , 'Kapat' );
} else if ( btn . closest ( '.vjs-control-bar' )) {
btn . setAttribute ( 'aria-label' , 'Oynatı cı Düğmesi' );
} else {
var parent = btn . parentElement ;
if ( parent && parent . title ) {
btn . setAttribute ( 'aria-label' , parent . title );
} else {
btn . setAttribute ( 'aria-label' , 'Düğme' );
}
}
}
});
// Auto-fix unlabeled images
document . querySelectorAll ( 'img:not([data-a11y-fixed])' ). forEach ( function ( img ) {
img . setAttribute ( 'data-a11y-fixed' , 'true' );
var alt = img . getAttribute ( 'alt' );
if ( ! alt || alt . trim () === '' ) {
var card = img . closest ( '.video-miniature, .video-card, .yaktube-card, [class*="miniature"]' );
if ( card ) {
var titleEl = card . querySelector (
'.video-miniature-name, .video-name, .yaktube-video-name, h3, h4, a[title]'
);
var titleText = titleEl ? ( titleEl . textContent || titleEl . getAttribute ( 'title' ) || '' ). trim () : '' ;
if ( titleText ) {
img . setAttribute ( 'alt' , titleText + ' video kapağı' );
return ;
}
}
var avatarContainer = img . closest ( '.actor-avatar, .channel-avatar, .avatar, [class*="avatar"]' );
if ( avatarContainer ) {
var nameEl = avatarContainer . parentElement
? avatarContainer . parentElement . querySelector ( '.name, .actor-name, .channel-name' )
: null ;
var nameText = nameEl ? ( nameEl . textContent || '' ). trim () : '' ;
img . setAttribute ( 'alt' , ( nameText ? nameText + ' ' : '' ) + 'Profil Resmi' );
return ;
}
if ( img . src && ( img . src . includes ( 'logo' ) || img . className . includes ( 'logo' ))) {
img . setAttribute ( 'alt' , getInstanceName () + ' Logo' );
return ;
}
var parentLink = img . closest ( 'a' );
if ( parentLink && parentLink . textContent && parentLink . textContent . trim (). length > 0 ) {
img . setAttribute ( 'alt' , '' );
} else if ( parentLink && parentLink . getAttribute ( 'title' )) {
img . setAttribute ( 'alt' , parentLink . getAttribute ( 'title' ));
} else {
img . setAttribute ( 'alt' , 'Görsel' );
}
}
});
// Auto-fix unlabeled links
document . querySelectorAll ( 'a:not([data-a11y-fixed])' ). forEach ( function ( a ) {
a . setAttribute ( 'data-a11y-fixed' , 'true' );
var text = ( a . textContent || '' ). trim ();
var hasAria = a . getAttribute ( 'aria-label' ) || a . getAttribute ( 'title' );
if ( ! text && ! hasAria ) {
var img = a . querySelector ( 'img' );
if ( img && img . alt ) {
a . setAttribute ( 'aria-label' , img . alt );
} else if ( a . href ) {
if ( a . href . includes ( '/videos/watch/' )) {
a . setAttribute ( 'aria-label' , 'Videoyu İzle' );
} else if ( a . href . includes ( '/video-channels/' )) {
a . setAttribute ( 'aria-label' , 'Kanal Sayfası na Git' );
} else if ( a . href . includes ( '/accounts/' )) {
a . setAttribute ( 'aria-label' , 'Hesap Profiline Git' );
} else if ( a . href . includes ( '/videos/search' )) {
a . setAttribute ( 'aria-label' , 'Arama Sayfası ' );
} else {
a . setAttribute ( 'aria-label' , 'Bağlantı ' );
}
}
}
});
// Auto-fix search inputs
document . querySelectorAll ( 'input:not([data-a11y-fixed])' ). forEach ( function ( inp ) {
inp . setAttribute ( 'data-a11y-fixed' , 'true' );
if ( ! inp . getAttribute ( 'aria-label' ) && ! inp . getAttribute ( 'placeholder' ) && ! ( inp . labels && inp . labels . length )) {
var type = ( inp . getAttribute ( 'type' ) || 'text' ). toLowerCase ();
if ( type === 'search' ) {
inp . setAttribute ( 'aria-label' , getInstanceName () + ' Video Arama' );
} else if ( type === 'checkbox' ) {
inp . setAttribute ( 'aria-label' , 'Seçim Kutusu' );
} else if ( type === 'password' ) {
2026-09-19 05:14:11 +03:00
inp . setAttribute ( 'aria-label' , 'Şifre' );
2026-08-31 18:48:19 +03:00
}
}
});
}
// 3. Screen-Reader & Turkish Q Optimized Shortcuts & Player Bar (v1.4.1)
function toggleShortcutsModal () {
var modal = document . getElementById ( 'yaktube-shortcuts-dialog' );
if ( modal ) {
if ( modal . style . display === 'none' || ! modal . style . display ) {
modal . style . display = 'flex' ;
announce ( 'Klavye kı sayolları yardı m penceresi açıldı . Kapatmak için Escape tuşuna bası n.' );
var closeBtn = document . getElementById ( 'yaktube-shortcuts-close' );
if ( closeBtn ) closeBtn . focus ();
} else {
modal . style . display = 'none' ;
announce ( 'Yardı m penceresi kapatı ldı .' );
}
} else {
modal = document . createElement ( 'div' );
modal . id = 'yaktube-shortcuts-dialog' ;
modal . className = 'yaktube-modal-overlay' ;
modal . setAttribute ( 'role' , 'dialog' );
modal . setAttribute ( 'aria-modal' , 'true' );
modal . setAttribute ( 'aria-labelledby' , 'yaktube-shortcuts-title' );
modal . innerHTML =
'<div class="yaktube-modal-card" style="max-width: 600px; background:#1e1e24; border-radius:18px; padding:24px; color:#fff; box-shadow:0 16px 40px rgba(0,0,0,0.8);">' +
'<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">' +
'<h3 id="yaktube-shortcuts-title" style="margin:0; font-size:18px; color:#ff8f37;">⌨️ Klavye Kı sayolları (Ekran Okuyucu Uyumlu)</h3>' +
'<button id="yaktube-shortcuts-close" class="yaktube-voice-modal-close-btn" aria-label="Kapat">✕</button>' +
'</div>' +
'<div style="font-size:12px; color:#aaa; margin-bottom:14px;">💡 <b>Ekran Okuyucu Notu:</b> Sanal imleç (Browse mode) açıkken kı sayolları <b>Alt</b> tuşuyla birlikte kullanabilir veya video oynatı cı butonları ndan faydalanabilirsiniz.</div>' +
'<table class="yaktube-shortcuts-table" style="width:100%; border-collapse:collapse; font-size:13px;">' +
'<thead><tr style="border-bottom:1px solid rgba(255,255,255,0.1); text-align:left; color:#aaa;"><th style="padding:6px 0;">Kı sayol (Evrensel / Tek Tuş)</th><th style="padding:6px 0;">İşlev</th></tr></thead>' +
'<tbody>' +
'<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + K</span> veya <span class="yaktube-kbd">Space</span></td><td>Oynat / Duraklat</td></tr>' +
'<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + J</span> veya <span class="yaktube-kbd">J</span></td><td>10 saniye geri sar</td></tr>' +
'<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + L</span> veya <span class="yaktube-kbd">L</span></td><td>10 saniye ileri sar</td></tr>' +
'<tr style="border-bottom:1px solid rgba(255,255,255,0.05);"><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + Yukarı / Aşağı</span></td><td>Sesi %5 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 + M</span> veya <span class="yaktube-kbd">M</span></td><td>Sesi Kapat / Aç (Mute)</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 + C</span> veya <span class="yaktube-kbd">C</span></td><td>Altyazı ları Aç / Kapat</td></tr>' +
2026-09-19 05:14:11 +03:00
'<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>' +
2026-08-31 18:48:19 +03:00
'<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 + V</span> veya <span class="yaktube-kbd">Shift + V</span></td><td>Sesli aramayı başlat</td></tr>' +
'<tr><td style="padding:8px 0;"><span class="yaktube-kbd">Alt + H</span> veya <span class="yaktube-kbd">F1</span> veya <span class="yaktube-kbd">?</span></td><td>Bu kı lavuzu aç / kapat</td></tr>' +
'</tbody>' +
'</table>' +
'</div>' ;
document . body . appendChild ( modal );
var closeBtn = modal . querySelector ( '#yaktube-shortcuts-close' );
if ( closeBtn ) {
closeBtn . addEventListener ( 'click' , function () {
modal . style . display = 'none' ;
});
}
modal . addEventListener ( 'click' , function ( e ) {
if ( e . target === modal ) modal . style . display = 'none' ;
});
announce ( 'Klavye kı sayolları yardı m penceresi açıldı . Kapatmak için Escape tuşuna bası n.' );
}
}
function isInputFocused () {
var el = document . activeElement ;
if ( ! el ) return false ;
var tag = el . tagName ? el . tagName . toUpperCase () : '' ;
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el . isContentEditable ;
}
function getVideoElement () {
2026-08-31 21:34:30 +03:00
// 1. Direct video tag in DOM
var v = document . querySelector ( 'video' );
if ( v ) return v ;
// 2. Video inside iframes (e.g. embed player)
var iframes = document . querySelectorAll ( 'iframe' );
for ( var i = 0 ; i < iframes . length ; i ++ ) {
try {
var doc = iframes [ i ]. contentDocument || ( iframes [ i ]. contentWindow && iframes [ i ]. contentWindow . document );
if ( doc ) {
var iv = doc . querySelector ( 'video' );
if ( iv ) return iv ;
}
} catch ( e ) {}
}
return null ;
2026-08-31 18:48:19 +03:00
}
2026-09-19 05:14:11 +03:00
window . getVideoElement = getVideoElement ;
2026-08-31 18:48:19 +03:00
function formatTime ( seconds ) {
if ( isNaN ( seconds ) || seconds < 0 ) return '0 saniye' ;
var mins = Math . floor ( seconds / 60 );
var secs = Math . floor ( seconds % 60 );
if ( mins === 0 ) return secs + ' saniye' ;
return mins + ' dakika ' + secs + ' saniye' ;
}
function showQuickVideoInfo () {
var video = getVideoElement ();
if ( ! video ) {
announce ( 'Şu anda aktif oynatı lan bir video bulunmuyor.' );
return ;
}
var titleEl = document . querySelector ( 'h1.video-title, .video-name, my-video-watch h1, .title' );
var title = titleEl ? titleEl . textContent . trim () : 'Video' ;
var curTime = formatTime ( video . currentTime );
var totalTime = formatTime ( video . duration || 0 );
var remaining = formatTime (( video . duration || 0 ) - video . currentTime );
var speed = ( video . playbackRate || 1 ) + 'x' ;
var volume = Math . round (( video . volume || 1 ) * 100 ) + '%' ;
var quality = video . videoHeight ? video . videoHeight + 'p' : 'Otomatik' ;
// Visual Toast
var existingToast = document . querySelector ( '.yaktube-video-info-toast' );
if ( existingToast ) existingToast . remove ();
var toast = document . createElement ( 'div' );
toast . className = 'yaktube-video-info-toast' ;
toast . innerHTML =
'<div class="yaktube-info-toast-title">📊 Video Bilgisi & Durum</div>' +
'<div class="yaktube-info-toast-row"><span>Başlı k:</span> <b>' +
( title . length > 28 ? title . substring ( 0 , 28 ) + '...' : title ) +
'</b></div>' +
'<div class="yaktube-info-toast-row"><span>Konum / Süre:</span> <b>' +
curTime +
' / ' +
totalTime +
'</b></div>' +
'<div class="yaktube-info-toast-row"><span>Kalan Süre:</span> <b>' +
remaining +
'</b></div>' +
'<div class="yaktube-info-toast-row"><span>Çözünürlük:</span> <b>' +
quality +
'</b></div>' +
'<div class="yaktube-info-toast-row"><span>Hı z & Ses:</span> <b>' +
speed +
' | ' +
volume +
'</b></div>' ;
document . body . appendChild ( toast );
setTimeout ( function () {
if ( toast . parentElement ) toast . remove ();
}, 4000 );
announce (
'Video Bilgisi: ' +
title +
'. Konum: ' +
curTime +
', Toplam Süre: ' +
totalTime +
', Kalan: ' +
remaining +
', Oynatma Hı zı : ' +
speed +
', Ses: yüzde ' +
Math . round ( video . volume * 100 ) +
', Çözünürlük: ' +
quality ,
true
);
}
var speedSteps = [ 0.25 , 0.5 , 0.75 , 1 , 1.25 , 1.5 , 1.75 , 2 ];
function adjustPlaybackSpeed ( increase ) {
var video = getVideoElement ();
if ( ! video ) return ;
var curSpeed = video . playbackRate || 1 ;
if ( increase ) {
var nextIdx = speedSteps . findIndex ( function ( s ) {
return s > curSpeed ;
});
video . playbackRate = nextIdx !== - 1 ? speedSteps [ nextIdx ] : 2 ;
announce ( 'Oynatma hı zı ' + video . playbackRate + ' katı na çıkarı ldı .' );
} else {
var prevSteps = speedSteps . filter ( function ( s ) {
return s < curSpeed ;
});
video . playbackRate = prevSteps . length > 0 ? prevSteps [ prevSteps . length - 1 ] : 0.25 ;
announce ( 'Oynatma hı zı ' + video . playbackRate + ' katı na düşürüldü.' );
}
}
2026-09-19 05:14:11 +03:00
// ==========================================================================
// 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' ;
2026-09-19 05:20:33 +03:00
header . innerHTML =
'<h3 class="yaktube-feature-modal-title"><span>⏱️</span> Uyku Zamanlayı cı sı </h3>' +
2026-09-19 05:14:11 +03:00
'<button class="yaktube-feature-modal-close" aria-label="Kapat (Escape)">×</button>' ;
var body = document . createElement ( 'div' );
body . className = 'yaktube-feature-modal-body' ;
2026-09-19 05:20:33 +03:00
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>' +
2026-09-19 05:14:11 +03:00
'<div class="yaktube-timer-options">' +
2026-09-19 05:20:33 +03:00
'<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>' +
2026-09-19 05:14:11 +03:00
'</div>' +
2026-09-19 05:20:33 +03:00
'<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>'
: '' );
2026-09-19 05:14:11 +03:00
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' ;
2026-09-19 05:20:33 +03:00
header . innerHTML =
'<h3 class="yaktube-feature-modal-title"><span>📱</span> Telefonda Devam Et</h3>' +
2026-09-19 05:14:11 +03:00
'<button class="yaktube-feature-modal-close" aria-label="Kapat (Escape)">×</button>' ;
var formattedTime = formatClockTime ( currentSec );
2026-09-19 05:20:33 +03:00
var qrImageUrl =
'https://api.qrserver.com/v1/create-qr-code/?size=220x220&margin=8&data=' + encodeURIComponent ( targetUrl );
2026-09-19 05:14:11 +03:00
var body = document . createElement ( 'div' );
body . className = 'yaktube-feature-modal-body' ;
body . innerHTML =
2026-09-19 05:20:33 +03:00
'<p class="yaktube-qr-desc">Telefonunuzun kamerası yla QR kodu okutarak videoyu kaldığını z yerden (<strong>' +
formattedTime +
'</strong>) izlemeye devam edebilirsiniz.</p>' +
2026-09-19 05:14:11 +03:00
'<div class="yaktube-qr-wrapper">' +
2026-09-19 05:20:33 +03:00
'<img src="' +
qrImageUrl +
'" alt="Video Bağlantı sı QR Kodu" width="190" height="190" style="display:block;border-radius:8px;" />' +
2026-09-19 05:14:11 +03:00
'</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 ) {
2026-09-19 05:20:33 +03:00
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 );
});
2026-09-19 05:14:11 +03:00
} 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 ;
2026-09-19 05:20:33 +03:00
existing . innerHTML =
'<div class="yaktube-ripple-content">' + ( isLeft ? '⏪ 10sn Geri' : '10sn İleri ⏩' ) + '</div>' ;
2026-09-19 05:14:11 +03:00
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 ;
2026-09-19 05:20:33 +03:00
var playerContainer =
video . closest ( '.video-js, .player-container, .player, my-video-watch' ) || video . parentElement ;
2026-09-19 05:14:11 +03:00
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' ) {
2026-09-19 05:20:33 +03:00
var playerWrap =
document . querySelector ( '.player-container, .video-player-container, my-video-watch .player' ) || video ;
2026-09-19 05:14:11 +03:00
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 ;
}
2026-08-31 18:48:19 +03:00
// 3.1. Injects Accessible Player Action Bar under the Watch Video
function injectAccessiblePlayerBar () {
if ( ! window . location . pathname . includes ( '/videos/watch/' )) return ;
var video = getVideoElement ();
if ( ! video ) return ;
var targetContainer = document . querySelector (
'my-video-watch, .video-watch, .watch-container, #yaktube-ondemand-container'
);
if ( ! targetContainer ) return ;
var existingBar = document . getElementById ( 'yaktube-player-action-bar' );
if ( existingBar ) return ;
var bar = document . createElement ( 'div' );
bar . id = 'yaktube-player-action-bar' ;
bar . className = 'yaktube-player-action-bar' ;
bar . setAttribute ( 'role' , 'toolbar' );
bar . setAttribute ( 'aria-label' , 'Erişilebilir Video Kontrolleri' );
bar . innerHTML =
'<span class="yaktube-player-bar-label">⚡ Oynatı cı Kontrolleri:</span>' +
'<button id="yaktube-bar-rewind-btn" class="yaktube-player-action-btn" aria-label="10 Saniye Geri Sar (Alt+J)">⏪ 10sn Geri</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-speed-btn" class="yaktube-player-action-btn" aria-label="Oynatma Hı zı nı Değiştir (Alt+.)">⚡ Hı z</button>' +
2026-09-19 05:14:11 +03:00
'<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>' +
2026-08-31 18:48:19 +03:00
'<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-help-btn" class="yaktube-player-action-btn" aria-label="Kı sayol Kı lavuzu (Alt+H veya F1)">⌨️ Kı sayollar</button>' ;
var playerWrap =
document . querySelector ( '.player-container, .video-player-container, .player-wrapper, my-video-watch .player' ) ||
targetContainer ;
if ( playerWrap && playerWrap . nextSibling ) {
playerWrap . parentNode . insertBefore ( bar , playerWrap . nextSibling );
} else {
targetContainer . prepend ( bar );
}
// Attach button actions
bar . querySelector ( '#yaktube-bar-rewind-btn' ). addEventListener ( 'click' , function () {
if ( video ) {
video . currentTime = Math . max ( 0 , video . currentTime - 10 );
announce ( '10 saniye geri sarı ldı . Konum: ' + formatTime ( video . currentTime ));
}
});
bar . querySelector ( '#yaktube-bar-play-btn' ). addEventListener ( 'click' , function () {
if ( video ) {
if ( video . paused ) {
video . play ();
announce ( 'Video oynatı lı yor.' );
} else {
video . pause ();
announce ( 'Video duraklatı ldı .' );
}
}
});
bar . querySelector ( '#yaktube-bar-forward-btn' ). addEventListener ( 'click' , function () {
if ( video ) {
video . currentTime = Math . min ( video . duration || 99999 , video . currentTime + 10 );
announce ( '10 saniye ileri sarı ldı . Konum: ' + formatTime ( video . currentTime ));
}
});
bar . querySelector ( '#yaktube-bar-speed-btn' ). addEventListener ( 'click' , function () {
adjustPlaybackSpeed ( true );
});
2026-09-19 05:14:11 +03:00
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 ();
});
}
2026-08-31 18:48:19 +03:00
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' );
announce ( isT ? 'Sinema modu açıldı .' : 'Sinema modundan çıkı ldı .' );
});
bar . querySelector ( '#yaktube-bar-info-btn' ). addEventListener ( 'click' , function () {
showQuickVideoInfo ();
});
bar . querySelector ( '#yaktube-bar-help-btn' ). addEventListener ( 'click' , function () {
toggleShortcutsModal ();
});
}
// 4. Global Keydown Listener (Screen-Reader & Turkish Q Optimized)
window . addEventListener ( 'keydown' , function ( e ) {
if ( e . key === 'Escape' ) {
2026-09-19 05:14:11 +03:00
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 ;
}
2026-08-31 18:48:19 +03:00
var shortcutsModal = document . getElementById ( 'yaktube-shortcuts-dialog' );
if ( shortcutsModal && shortcutsModal . style . display !== 'none' ) {
shortcutsModal . style . display = 'none' ;
announce ( 'Pencere kapatı ldı .' );
return ;
}
var importModal = document . getElementById ( 'yaktube-import-modal' );
if ( importModal && importModal . style . display !== 'none' ) {
importModal . style . display = 'none' ;
announce ( 'İndirme penceresi kapatı ldı .' );
return ;
}
if ( isInputFocused ()) {
document . activeElement . blur ();
}
return ;
}
// Admin Login: Alt+A
if ( e . altKey && ( e . key === 'a' || e . key === 'A' ) && ! isInputFocused ()) {
e . preventDefault ();
openLocalAdminLoginModal ();
return ;
}
2026-09-19 05:14:11 +03:00
// 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 ;
}
2026-08-31 18:48:19 +03:00
// Help Dialog: Alt+H, F1 or '?' key
if (( e . altKey && ( e . key === 'h' || e . key === 'H' )) || e . key === 'F1' || e . key === '?' ) {
if ( ! isInputFocused ()) {
e . preventDefault ();
toggleShortcutsModal ();
return ;
}
}
// Search Open: Alt+S, Ctrl+K or '/' key (Turkish Q Shift+7)
if (
(( e . altKey && ( e . key === 's' || e . key === 'S' )) ||
( e . ctrlKey && ( e . key === 'k' || e . key === 'K' )) ||
e . key === '/' ) &&
! isInputFocused ()
) {
e . preventDefault ();
openFullSearchModal ( '' , false );
return ;
}
// Voice Search Open: Alt+V or Shift+V
if (
(( e . altKey && ( e . key === 'v' || e . key === 'V' )) || ( e . shiftKey && ( e . key === 'v' || e . key === 'V' ))) &&
! isInputFocused ()
) {
e . preventDefault ();
openFullSearchModal ( '' , true );
return ;
}
if ( isInputFocused ()) {
return ;
}
var video = getVideoElement ();
if ( ! video ) return ;
var key = e . key . toLowerCase ();
var alt = e . altKey ;
// Play / Pause (Alt+K, Space or K)
if ( key === ' ' || key === 'k' || ( alt && key === 'k' )) {
e . preventDefault ();
2026-08-30 11:38:46 +03:00
if ( video . paused ) {
video . play ();
announce ( 'Video oynatı lı yor.' );
} else {
video . pause ();
announce ( 'Video duraklatı ldı .' );
}
}
2026-08-31 18:48:19 +03:00
// Seek 10s Back / Forward (Alt+J / Alt+L or J / L)
else if ( key === 'j' || ( alt && key === 'j' )) {
e . preventDefault ();
video . currentTime = Math . max ( 0 , video . currentTime - 10 );
announce ( '10 saniye geri sarı ldı . Konum: ' + formatTime ( video . currentTime ));
} else if ( key === 'l' || ( alt && key === 'l' )) {
e . preventDefault ();
2026-08-30 11:38:46 +03:00
video . currentTime = Math . min ( video . duration || 99999 , video . currentTime + 10 );
announce ( '10 saniye ileri sarı ldı . Konum: ' + formatTime ( video . currentTime ));
}
2026-08-31 18:48:19 +03:00
// Seek 5s Back / Forward (ArrowLeft / ArrowRight or Alt+Left/Right)
else if ( e . key === 'ArrowLeft' || ( alt && e . key === 'ArrowLeft' )) {
2026-08-29 12:42:36 +03:00
e . preventDefault ();
2026-08-31 18:48:19 +03:00
video . currentTime = Math . max ( 0 , video . currentTime - 5 );
announce ( '5 saniye geri sarı ldı . Konum: ' + formatTime ( video . currentTime ));
} else if ( e . key === 'ArrowRight' || ( alt && e . key === 'ArrowRight' )) {
e . preventDefault ();
video . currentTime = Math . min ( video . duration || 99999 , video . currentTime + 5 );
announce ( '5 saniye ileri sarı ldı . Konum: ' + formatTime ( video . currentTime ));
}
// Volume Up / Down (ArrowUp / ArrowDown or Alt+Up/Down)
else if ( e . key === 'ArrowUp' || ( alt && e . key === 'ArrowUp' )) {
e . preventDefault ();
video . volume = Math . min ( 1 , ( video . volume || 1 ) + 0.05 );
announce ( 'Ses seviyesi yüzde ' + Math . round ( video . volume * 100 ));
} else if ( e . key === 'ArrowDown' || ( alt && e . key === 'ArrowDown' )) {
e . preventDefault ();
video . volume = Math . max ( 0 , ( video . volume || 1 ) - 0.05 );
announce ( 'Ses seviyesi yüzde ' + Math . round ( video . volume * 100 ));
}
// Mute (Alt+M or M)
else if ( key === 'm' || ( alt && key === 'm' )) {
e . preventDefault ();
video . muted = ! video . muted ;
announce ( video . muted ? 'Ses kapatı ldı (Sessiz).' : 'Ses açıldı .' );
}
// Fullscreen (Alt+F or F)
else if ( key === 'f' || ( alt && key === 'f' )) {
e . preventDefault ();
if ( ! document . fullscreenElement ) {
var container = video . closest ( '.video-js' ) || video . parentElement || video ;
if ( container . requestFullscreen ) container . requestFullscreen ();
announce ( 'Tam ekran moduna geçildi.' );
} else {
if ( document . exitFullscreen ) document . exitFullscreen ();
announce ( 'Tam ekrandan çıkı ldı .' );
}
}
// Theater Mode (Alt+T or T)
else if ( key === 't' || ( alt && key === 't' )) {
e . preventDefault ();
document . body . classList . toggle ( 'yaktube-theater-mode' );
var isTheater = document . body . classList . contains ( 'yaktube-theater-mode' );
announce ( isTheater ? 'Sinema modu açıldı .' : 'Sinema modundan çıkı ldı .' );
}
// Quick Video Info (Alt+I or I)
else if ( key === 'i' || ( alt && key === 'i' )) {
e . preventDefault ();
showQuickVideoInfo ();
}
// Subtitles (Alt+C or C)
else if ( key === 'c' || ( alt && key === 'c' )) {
e . preventDefault ();
var tracks = video . textTracks ;
if ( tracks && tracks . length > 0 ) {
var isShowing = tracks [ 0 ]. mode === 'showing' ;
tracks [ 0 ]. mode = isShowing ? 'disabled' : 'showing' ;
announce ( isShowing ? 'Altyazı lar kapatı ldı .' : 'Altyazı lar açıldı .' );
} else {
announce ( 'Bu videoda altyazı bulunmuyor.' );
}
}
// Speed Controls (Alt+. / Alt+, or > / <)
else if (( alt && ( e . key === '.' || e . key === '>' )) || e . key === '>' ) {
e . preventDefault ();
adjustPlaybackSpeed ( true );
} else if (( alt && ( e . key === ',' || e . key === '<' )) || e . key === '<' ) {
e . preventDefault ();
adjustPlaybackSpeed ( false );
}
// Percentage Jumps (0 - 9)
else if ( key >= '0' && key <= '9' && ! e . ctrlKey && ! e . altKey && ! e . metaKey ) {
e . preventDefault ();
var percent = parseInt ( key , 10 ) * 10 ;
if ( video . duration ) {
video . currentTime = ( video . duration * percent ) / 100 ;
announce ( 'Videonun yüzde ' + percent + ' noktası na atlandı . Konum: ' + formatTime ( video . currentTime ));
}
}
});
// 5. On-Demand Search & Import with Duplicate Awareness
var lastQuery = null ;
2026-09-05 15:48:27 +03:00
var lastSearchTarget = null ;
2026-08-31 18:48:19 +03:00
var isSearching = false ;
2026-09-05 15:48:27 +03:00
function escapeHtml ( str ) {
if ( ! str ) return '' ;
return String ( str )
. replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /"/g , '"' )
. replace ( /'/g , ''' );
}
function formatClockTime ( seconds ) {
if ( isNaN ( seconds ) || seconds < 0 ) return '00:00' ;
var hrs = Math . floor ( seconds / 3600 );
var mins = Math . floor (( seconds % 3600 ) / 60 );
var secs = Math . floor ( seconds % 60 );
var sSecs = ( secs < 10 ? '0' : '' ) + secs ;
if ( hrs > 0 ) {
var sMins = ( mins < 10 ? '0' : '' ) + mins ;
return hrs + ':' + sMins + ':' + sSecs ;
}
return ( mins < 10 ? '0' : '' ) + mins + ':' + sSecs ;
}
function switchSearchTarget ( target ) {
var radio = document . getElementById ( target === 'local' ? 'searchTargetLocal' : 'searchTargetSearchIndex' );
if ( radio ) {
radio . checked = true ;
radio . dispatchEvent ( new Event ( 'change' , { bubbles : true }));
radio . dispatchEvent ( new Event ( 'input' , { bubbles : true }));
var form = radio . closest ( 'form' );
var submitBtn = form ? form . querySelector ( '.submit-button button' ) : null ;
if ( submitBtn ) {
submitBtn . click ();
return ;
}
}
var url = new URL ( window . location . href );
url . searchParams . set ( 'searchTarget' , target );
window . location . href = url . toString ();
}
function ensureFediverseBridge ( query , currentSearchTarget ) {
var bridge = document . getElementById ( 'yaktube-fediverse-bridge' );
if ( ! bridge ) {
bridge = document . createElement ( 'div' );
bridge . id = 'yaktube-fediverse-bridge' ;
bridge . className = 'yaktube-fediverse-bridge' ;
bridge . setAttribute ( 'role' , 'region' );
bridge . setAttribute ( 'aria-label' , 'Fediverse Video Arama Ağı ve Kapsam Seçimi' );
var searchResult = document . querySelector ( '.search-result, my-search .search-result' );
var ondemandContainer = document . getElementById ( 'yaktube-ondemand-container' );
if ( searchResult && searchResult . parentNode ) {
searchResult . parentNode . insertBefore ( bridge , searchResult );
} else if ( ondemandContainer && ondemandContainer . parentNode ) {
ondemandContainer . parentNode . insertBefore ( bridge , ondemandContainer . nextSibling );
} else {
var mainCol = document . querySelector ( 'my-search, main, .main-col' ) || document . body ;
mainCol . appendChild ( bridge );
}
}
var isFediverse = currentSearchTarget === 'search-index' ;
bridge . innerHTML =
'<div class="yaktube-fediverse-bridge-content">' +
2026-09-19 05:20:33 +03:00
'<div class="yaktube-fediverse-bridge-left">' +
'<div class="yaktube-fediverse-bridge-badge-wrap">' +
'<span class="yaktube-fediverse-badge">🌐 Fediverse Ağı</span>' +
'<span class="yaktube-fediverse-status">' +
( isFediverse
? 'SepiaSearch Küresel Fediverse Arama Aktif'
: escapeHtml ( getInstanceName ()) + ' Yerel Arama Aktif' ) +
'</span>' +
'</div>' +
'<h2 class="yaktube-fediverse-title">🌐 Fediverse & Küresel Ağ Arama Sonuçları </h2>' +
'<p class="yaktube-fediverse-sub">Merkeziyetsiz federe ağdaki (SepiaSearch ve bağımsı z PeerTube sunucuları ) video sonuçları aşağıda listelenmektedir.</p>' +
'</div>' +
'<div class="yaktube-scope-selector" role="tablist" aria-label="Arama Kapsamı Seçimi">' +
'<button type="button" id="yaktube-scope-global-btn" class="yaktube-scope-btn ' +
( isFediverse ? 'active' : '' ) +
'" role="tab" aria-selected="' +
( isFediverse ? 'true' : 'false' ) +
'" aria-label="Tüm Fediverse ve SepiaSearch ağındaki videoları ara" title="Tüm Fediverse genelinde ara">' +
'🌐 Tüm Fediverse (SepiaSearch)' +
'</button>' +
'<button type="button" id="yaktube-scope-local-btn" class="yaktube-scope-btn ' +
( ! isFediverse ? 'active' : '' ) +
'" role="tab" aria-selected="' +
( ! isFediverse ? 'true' : 'false' ) +
'" aria-label="Sadece ' +
escapeHtml ( getInstanceName ()) +
' yerel sunucusundaki videoları ara" title="Sadece ' +
escapeHtml ( getInstanceName ()) +
' yerel kütüphanesinde ara">' +
'🏠 Sadece ' +
escapeHtml ( getInstanceName ()) +
' (Yerel)' +
'</button>' +
'</div>' +
2026-09-05 15:48:27 +03:00
'</div>' ;
var globalBtn = document . getElementById ( 'yaktube-scope-global-btn' );
if ( globalBtn ) {
globalBtn . addEventListener ( 'click' , function () {
if ( currentSearchTarget !== 'search-index' ) {
switchSearchTarget ( 'search-index' );
}
});
}
var localBtn = document . getElementById ( 'yaktube-scope-local-btn' );
if ( localBtn ) {
localBtn . addEventListener ( 'click' , function () {
if ( currentSearchTarget !== 'local' ) {
switchSearchTarget ( 'local' );
}
});
}
}
2026-08-31 18:48:19 +03:00
function showModal ( title , desc ) {
var overlay = document . getElementById ( 'yaktube-import-modal' );
if ( ! overlay ) {
overlay = document . createElement ( 'div' );
overlay . id = 'yaktube-import-modal' ;
overlay . className = 'yaktube-modal-overlay' ;
overlay . setAttribute ( 'role' , 'dialog' );
overlay . setAttribute ( 'aria-modal' , 'true' );
overlay . setAttribute ( 'aria-labelledby' , 'yaktube-modal-title' );
overlay . innerHTML =
'<div class="yaktube-modal-card">' +
'<div class="yaktube-spinner" aria-hidden="true"></div>' +
'<h3 class="yaktube-modal-title" id="yaktube-modal-title" style="margin-top:0;">' +
( title || "YakTube'a Aktarı lı yor..." ) +
'</h3>' +
'<p class="yaktube-modal-desc" id="yaktube-modal-desc">' +
( desc || 'Seçtiğiniz video D: diskinize aktarı lı yor...' ) +
'</p>' +
'</div>' ;
document . body . appendChild ( overlay );
} else {
var titleElem = document . getElementById ( 'yaktube-modal-title' );
var descElem = document . getElementById ( 'yaktube-modal-desc' );
if ( titleElem ) titleElem . innerText = title ;
if ( descElem ) descElem . innerText = desc ;
overlay . style . display = 'flex' ;
}
announce (( title || '' ) + '. ' + ( desc || '' ), true );
}
function hideModal () {
var overlay = document . getElementById ( 'yaktube-import-modal' );
if ( overlay ) overlay . style . display = 'none' ;
}
function handleLiveStream ( url , videoTitle , isRestoring ) {
var videoIdMatch = url . match ( /[?&]v=([^&]+)/ ) || url . match ( /youtu\.be\/([^?]+)/ );
var videoId = videoIdMatch ? videoIdMatch [ 1 ] : '' ;
if ( ! videoId ) {
alert ( 'Canl\u0131 yay\u0131n ID bulunamad\u0131.' );
2026-08-29 12:42:36 +03:00
return ;
}
2026-08-31 18:48:19 +03:00
// Persist active live stream state in sessionStorage
2026-08-29 12:42:36 +03:00
try {
2026-08-31 18:48:19 +03:00
sessionStorage . setItem (
'yaktube_active_live' ,
JSON . stringify ({
url : url ,
title : videoTitle ,
videoId : videoId
})
);
2026-08-29 12:42:36 +03:00
} catch ( e ) {}
2026-08-31 18:48:19 +03:00
var liveContainer = document . getElementById ( 'yaktube-live-watch-container' );
if ( liveContainer ) liveContainer . remove ();
liveContainer = document . createElement ( 'div' );
liveContainer . id = 'yaktube-live-watch-container' ;
liveContainer . className = 'yaktube-live-watch-section' ;
liveContainer . setAttribute ( 'role' , 'region' );
liveContainer . setAttribute ( 'tabindex' , '-1' );
liveContainer . setAttribute ( 'aria-label' , videoTitle + ' Canl\u0131 Yay\u0131n\u0131' );
liveContainer . innerHTML =
'<div style="background: #0f172a; border-radius: 12px; padding: 20px; margin-bottom: 24px; box-shadow: 0 10px 30px rgba(0,0,0,0.6); border: 1px solid rgba(255,255,255,0.08); outline: none;">' +
'<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; flex-wrap: wrap; gap: 10px;">' +
'<div style="display: flex; align-items: center; gap: 10px;">' +
'<span style="background: #dc2626; color: #fff; font-size: 12px; padding: 4px 10px; border-radius: 6px; font-weight: 700; display: inline-flex; align-items: center; gap: 6px;">\ud83d\udd34 CANLI YAYIN</span>' +
'<h1 id="yaktube-live-title" tabindex="0" style="margin: 0; font-size: 1.25rem; font-weight: 700; color: #fff; line-height: 1.3; outline: none;">' +
videoTitle +
'</h1>' +
'</div>' +
'<button id="yaktube-close-live-theater" style="background: #334155; color: #fff; border: none; padding: 8px 16px; border-radius: 8px; font-weight: 600; cursor: pointer; font-size: 13px; display: inline-flex; align-items: center; gap: 6px;" aria-label="Canl\u0131 Yay\u0131n \u0130zleme Alan\u0131n\u0131 Kapat (Escape)">' +
'\u2715 Yay\u0131n\u0131 Kapat (Esc)' +
'</button>' +
'</div>' +
'<div style="position: relative; padding-top: 56.25%; width: 100%; background: #000; border-radius: 10px; overflow: hidden; box-shadow: 0 8px 24px rgba(0,0,0,0.7);">' +
'<iframe id="yaktube-live-iframe" src="https://www.youtube-nocookie.com/embed/' +
videoId +
'?autoplay=1&rel=0&modestbranding=1&enablejsapi=1" title="' +
videoTitle . replace ( /"/g , '"' ) +
'" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0;" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>' +
'</div>' +
'<div style="margin-top: 14px; display: flex; justify-content: space-between; align-items: center; color: #94a3b8; font-size: 13px;">' +
'<div>\ud83d\udce1 YakTube Canl\u0131 Yay\u0131n Ak\u0131\u015f\u0131 \u2022 Reklams\u0131z ve Kesintisiz</div>' +
'<div style="color: #64748b;">ESC tu\u015funa basarak yay\u0131n\u0131 kapatabilirsiniz.</div>' +
'</div>' +
'<div id="yaktube-live-comments-panel" style="margin-top: 18px; background: #0b1120; border-radius: 10px; padding: 16px; border: 1px solid rgba(255,255,255,0.08);">' +
'<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">' +
'<h2 style="margin: 0; font-size: 15px; font-weight: 700; color: #fff; display: flex; align-items: center; gap: 8px;">' +
'\ud83d\udcac YouTube Topluluk Yorumlar\u0131' +
'</h2>' +
'<button id="yaktube-toggle-comments-btn" style="background: #ff8f37; color: #000; border: none; padding: 7px 16px; border-radius: 6px; font-weight: 700; cursor: pointer; font-size: 12px; display: inline-flex; align-items: center; gap: 6px;" aria-label="YouTube yorumlar\u0131n\u0131 y\u00fckle ve g\u00f6ster">' +
'\ud83d\udcac Yorumlar\u0131 G\u00f6ster (20 Yorum)' +
'</button>' +
'</div>' +
'<div id="yaktube-comments-list" style="display: none; margin-top: 14px; flex-direction: column; gap: 12px;"></div>' +
'<div id="yaktube-more-comments-wrap" style="text-align: center; margin-top: 16px; display: none;">' +
'<button id="yaktube-load-more-comments-btn" style="background: #1e293b; color: #ff8f37; border: 1px solid #ff8f37; padding: 8px 20px; border-radius: 8px; font-weight: 700; cursor: pointer; font-size: 13px; display: inline-flex; align-items: center; gap: 6px;" aria-label="Daha fazla yorum y\u00fckle">' +
'\u2795 Daha Fazla Yorum G\u00f6ster (+20 Yorum)' +
'</button>' +
'</div>' +
'</div>' +
'</div>' ;
var mainMount =
document . querySelector ( 'my-search, my-search-results, router-outlet, main, .main-col' ) || document . body ;
if ( mainMount && mainMount . firstChild ) {
mainMount . insertBefore ( liveContainer , mainMount . firstChild );
} else if ( mainMount ) {
mainMount . appendChild ( liveContainer );
2026-08-29 12:42:36 +03:00
}
2026-08-31 18:48:19 +03:00
if ( ! isRestoring ) {
liveContainer . scrollIntoView ({ behavior : 'smooth' , block : 'start' });
setTimeout ( function () {
var liveTitle = document . getElementById ( 'yaktube-live-title' );
if ( liveTitle ) liveTitle . focus ();
else liveContainer . focus ();
}, 150 );
announce (
videoTitle +
' canl\u0131 yay\u0131n\u0131 ba\u015flat\u0131ld\u0131. Canl\u0131 yay\u0131n alan\u0131na odaklan\u0131ld\u0131.' ,
true
);
2026-08-29 12:42:36 +03:00
}
2026-08-31 18:48:19 +03:00
var commentNextToken = null ;
var commentsList = document . getElementById ( 'yaktube-comments-list' );
var toggleCommentsBtn = document . getElementById ( 'yaktube-toggle-comments-btn' );
var moreCommentsWrap = document . getElementById ( 'yaktube-more-comments-wrap' );
var moreCommentsBtn = document . getElementById ( 'yaktube-load-more-comments-btn' );
2026-08-29 12:42:36 +03:00
2026-08-31 18:48:19 +03:00
function renderCommentCard ( c ) {
var card = document . createElement ( 'div' );
card . className = 'yaktube-comment-item' ;
card . setAttribute ( 'role' , 'article' );
card . setAttribute ( 'tabindex' , '0' );
card . setAttribute ( 'aria-label' , c . author + ' isimli kullan\u0131c\u0131n\u0131n yorumu: ' + c . content );
card . style . cssText =
'background: #1e293b; padding: 12px 14px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.05); display: flex; gap: 12px; align-items: flex-start;' ;
var avatarLetter = ( c . author || 'U' ). replace ( '@' , '' ). charAt ( 0 ). toUpperCase ();
var avatarHtml =
'<div style="width: 36px; height: 36px; border-radius: 50%; background: #334155; color: #ff8f37; font-weight: 700; display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0;">' +
avatarLetter +
'</div>' ;
var safeContent = c . content
. replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /\n/g , '<br>' );
card . innerHTML =
avatarHtml +
'<div style="flex-grow: 1; min-width: 0;">' +
'<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px; flex-wrap: wrap;">' +
'<h3 class="yaktube-comment-author-heading" style="margin: 0; font-size: 13px; font-weight: 700; color: #f1f5f9; display: inline-block;">' +
c . author +
'</h3>' +
'<span style="color: #64748b; font-size: 11px;">' +
c . published +
'</span>' +
( c . likeCount && c . likeCount !== '0'
? '<span style="background: rgba(255,255,255,0.08); color: #94a3b8; font-size: 11px; padding: 2px 6px; border-radius: 4px; display: inline-flex; align-items: center; gap: 4px;">👍 ' +
c . likeCount +
'</span>'
: '' ) +
'</div>' +
'<div style="color: #cbd5e1; font-size: 13px; line-height: 1.45; word-break: break-word;">' +
safeContent +
'</div>' +
'</div>' ;
return card ;
2026-08-29 12:42:36 +03:00
}
2026-08-31 18:48:19 +03:00
function fetchComments ( token ) {
if ( ! token ) {
toggleCommentsBtn . disabled = true ;
toggleCommentsBtn . textContent = '⏳ Yorumlar Y\u00fckleniyor...' ;
} else {
moreCommentsBtn . disabled = true ;
moreCommentsBtn . textContent = '⏳ Y\u00fckleniyor...' ;
2026-08-29 12:42:36 +03:00
}
2026-08-31 18:48:19 +03:00
var fetchUrl =
'/api-custom/youtube-comments?id=' +
encodeURIComponent ( videoId ) +
( token ? '&token=' + encodeURIComponent ( token ) : '' );
fetch ( fetchUrl )
. then ( function ( r ) {
return r . json ();
})
. then ( function ( res ) {
if ( ! token ) {
commentsList . innerHTML = '' ;
commentsList . style . display = 'flex' ;
toggleCommentsBtn . textContent = '🔄 Yorumlar\u0131 Yenile' ;
toggleCommentsBtn . disabled = false ;
} else {
moreCommentsBtn . textContent = '➕ Daha Fazla Yorum G\u00f6ster (+20 Yorum)' ;
moreCommentsBtn . disabled = false ;
2026-08-29 12:42:36 +03:00
}
2026-08-31 18:48:19 +03:00
var list = res . comments || [];
if ( list . length === 0 && ! token ) {
commentsList . innerHTML =
'<div style="color: #94a3b8; font-size: 13px; text-align: center; padding: 12px;">Bu videoda/yay\u0131nda hen\u00fcz yorum bulunmuyor.</div>' ;
moreCommentsWrap . style . display = 'none' ;
announce ( 'Bu videoda henüz yorum bulunmuyor.' );
return ;
}
list . forEach ( function ( c ) {
commentsList . appendChild ( renderCommentCard ( c ));
});
commentNextToken = res . nextToken ;
if ( commentNextToken ) {
moreCommentsWrap . style . display = 'block' ;
} else {
moreCommentsWrap . style . display = 'none' ;
}
announce ( list . length + ' adet YouTube yorumu yüklendi.' );
})
. catch ( function ( err ) {
if ( ! token ) {
toggleCommentsBtn . disabled = false ;
toggleCommentsBtn . textContent = '⚠️ Tekrar Dene' ;
} else {
moreCommentsBtn . disabled = false ;
moreCommentsBtn . textContent = '⚠️ Tekrar Dene' ;
}
alert ( 'Yorumlar yüklenirken hata oluştu: ' + err . message );
});
}
if ( toggleCommentsBtn ) {
toggleCommentsBtn . addEventListener ( 'click' , function () {
fetchComments ( null );
});
}
if ( moreCommentsBtn ) {
moreCommentsBtn . addEventListener ( 'click' , function () {
if ( commentNextToken ) fetchComments ( commentNextToken );
});
}
function closeLiveTheater () {
try {
sessionStorage . removeItem ( 'yaktube_active_live' );
} catch ( e ) {}
if ( liveContainer && liveContainer . parentNode ) {
var ifr = liveContainer . querySelector ( 'iframe' );
if ( ifr ) ifr . src = '' ;
liveContainer . remove ();
}
document . removeEventListener ( 'keydown' , handleEsc );
announce ( 'Canl\u0131 yay\u0131n izleme alan\u0131 kapat\u0131ld\u0131.' );
}
function handleEsc ( e ) {
if ( e . key === 'Escape' || e . keyCode === 27 ) {
closeLiveTheater ();
}
}
var closeBtn = document . getElementById ( 'yaktube-close-live-theater' );
if ( closeBtn ) closeBtn . addEventListener ( 'click' , closeLiveTheater );
document . addEventListener ( 'keydown' , handleEsc );
}
function restoreActiveLiveStream () {
try {
var saved = sessionStorage . getItem ( 'yaktube_active_live' );
if ( ! saved ) return ;
var data = JSON . parse ( saved );
if ( data && data . url && data . title && ! document . getElementById ( 'yaktube-live-watch-container' )) {
handleLiveStream ( data . url , data . title , true );
}
} catch ( e ) {}
}
function handleImport ( url , btn , videoTitle ) {
btn . disabled = true ;
btn . innerHTML = '⏳ Aktarı lı yor...' ;
showModal (
'[1/2] 📥 Video İndiriliyor...' ,
( videoTitle ? '"' + videoTitle + '" ' : 'Video ' ) + "YouTube'dan sunucunuza aktarı lı yor. Lütfen bekleyin..."
);
fetch ( '/api-custom/ondemand-import' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ({ targetUrl : url , title : videoTitle })
})
. then ( function ( res ) {
return res . json ();
})
. then ( function ( data ) {
if ( ! data || ! data . ok ) {
throw new Error ( data && data . error ? data . error : 'İçe aktarma başlatı lamadı .' );
}
var videoId = data . video . id ;
var videoUuid = data . video . uuid ;
var attempts = 0 ;
var pollInterval = setInterval ( function () {
attempts ++ ;
fetch ( '/api-custom/status/' + videoId )
. then ( function ( sRes ) {
return sRes . json ();
})
. then ( function ( sData ) {
if ( sData . isImporting ) {
showModal ( '[1/2] 📥 Video İndiriliyor...' , 'Video sunucuya indiriliyor (' + attempts * 2 + ' sn)...' );
} else if ( sData . isTranscoding ) {
showModal (
'[2/2] ⚙️ Oynatı cı Hazı rlanı yor...' ,
'Video işleniyor ve yüksek kalite akış hazı rlanı yor...'
);
}
if ( sData . isPublished || ( sData . hasFiles && attempts >= 8 )) {
clearInterval ( pollInterval );
showModal ( '🎉 Video Hazı r!' , 'Video kütüphanenize eklendi. Oynatı cı açılı yor...' );
setTimeout ( function () {
window . location . href = '/videos/watch/' + ( sData . uuid || videoUuid );
}, 600 );
} else if ( sData . isFailed ) {
clearInterval ( pollInterval );
hideModal ();
btn . disabled = false ;
2026-09-05 15:54:10 +03:00
btn . innerHTML = '📥 ' + escapeHtml ( getInstanceName ()) + "'a Aktar & İzle" ;
2026-08-31 18:48:19 +03:00
alert ( 'Video indirme sı rası nda bir hata oluştu.' );
announce ( 'Video indirme sı rası nda bir hata oluştu.' , true );
}
})
. catch ( function ( e ) {
console . error ( '[YakTube] Status poll error:' , e );
});
if ( attempts > 35 ) {
clearInterval ( pollInterval );
hideModal ();
btn . disabled = false ;
2026-09-05 15:54:10 +03:00
btn . innerHTML = '📥 ' + escapeHtml ( getInstanceName ()) + "'a Aktar & İzle" ;
2026-08-31 18:48:19 +03:00
alert (
'İndirme arka planda tamamlanı yor. Ana sayfada veya Videoları m sayfası nda kı sa süre içinde hazı r olacaktı r.'
);
announce ( 'İndirme arka planda tamamlanı yor. Ana sayfada kı sa süre içinde hazı r olacaktı r.' );
}
}, 1500 );
})
. catch ( function ( err ) {
hideModal ();
btn . disabled = false ;
2026-09-05 15:54:10 +03:00
btn . innerHTML = '📥 ' + escapeHtml ( getInstanceName ()) + "'a Aktar & İzle" ;
2026-08-31 18:48:19 +03:00
alert ( 'İndirme hatası : ' + err . message );
announce ( 'İndirme hatası : ' + err . message , true );
});
}
function checkAndInjectSearchResults () {
2026-09-03 18:19:15 +03:00
// Clean up stray 404 error mascot pages if on search
try {
var errPages = document . querySelectorAll ( 'my-error-page' );
2026-09-04 12:54:11 +03:00
errPages . forEach ( function ( ep ) {
ep . remove ();
});
2026-09-03 18:19:15 +03:00
} catch ( e ) {}
2026-09-04 12:54:11 +03:00
// Instant URL fixer: redirect legacy /search/videos to native /search
if ( window . location . pathname === '/search/videos' || window . location . pathname . startsWith ( '/search/videos/' )) {
window . location . replace ( '/search' + window . location . search );
return ;
}
2026-09-03 18:19:15 +03:00
2026-08-31 18:48:19 +03:00
var url = new URL ( window . location . href );
var searchParam = url . searchParams . get ( 'search' );
if ( ! window . location . pathname . includes ( '/search' ) || ! searchParam || searchParam . trim (). length < 2 ) {
var existing = document . getElementById ( 'yaktube-ondemand-container' );
if ( existing ) existing . remove ();
2026-09-05 15:48:27 +03:00
var existingBridge = document . getElementById ( 'yaktube-fediverse-bridge' );
if ( existingBridge ) existingBridge . remove ();
2026-08-31 18:48:19 +03:00
lastQuery = null ;
2026-09-05 15:48:27 +03:00
lastSearchTarget = null ;
2026-08-31 18:48:19 +03:00
return ;
}
var query = searchParam . trim ();
2026-09-05 15:48:27 +03:00
// Ensure Fediverse Global Search (SepiaSearch) is default target if not specified in URL
if ( ! url . searchParams . has ( 'searchTarget' )) {
url . searchParams . set ( 'searchTarget' , 'search-index' );
window . location . replace ( url . toString ());
return ;
}
var currentSearchTarget = url . searchParams . get ( 'searchTarget' ) || 'search-index' ;
var queryKey = query + '___' + currentSearchTarget ;
var lastQueryKey = lastQuery + '___' + lastSearchTarget ;
if ( queryKey === lastQueryKey ) {
var existingBridge = document . getElementById ( 'yaktube-fediverse-bridge' );
if ( ! existingBridge && document . querySelector ( 'my-search' )) {
ensureFediverseBridge ( query , currentSearchTarget );
}
return ;
}
2026-08-31 18:48:19 +03:00
lastQuery = query ;
2026-09-05 15:48:27 +03:00
lastSearchTarget = currentSearchTarget ;
2026-08-31 18:48:19 +03:00
var container = document . getElementById ( 'yaktube-ondemand-container' );
if ( ! container ) {
container = document . createElement ( 'div' );
container . id = 'yaktube-ondemand-container' ;
container . className = 'yaktube-ondemand-section' ;
container . setAttribute ( 'role' , 'region' );
2026-09-05 15:54:10 +03:00
container . setAttribute ( 'aria-label' , 'Canlı YouTube ve ' + getInstanceName () + ' Arama Sonuçları ' );
2026-08-31 18:48:19 +03:00
var mainContent =
document . querySelector ( 'my-search, my-search-results, router-outlet, main, .main-col' ) || document . body ;
if ( mainContent && mainContent . firstChild ) {
mainContent . insertBefore ( container , mainContent . firstChild );
} else if ( mainContent ) {
mainContent . appendChild ( container );
}
}
container . innerHTML =
'<div class="yaktube-header">' +
'<div class="yaktube-header-left">' +
2026-09-05 15:48:27 +03:00
'<span class="yaktube-badge">⚡ Canlı YouTube & Web Arama</span>' +
2026-08-31 18:48:19 +03:00
'<h2 class="yaktube-title-h3" style="margin:0; font-size:1.15rem;">"' +
2026-09-05 15:48:27 +03:00
escapeHtml ( query ) +
'" için Sonuçlar Taranı yor...</h2>' +
2026-08-31 18:48:19 +03:00
'</div>' +
2026-09-05 15:48:27 +03:00
'<button class="yaktube-shortcuts-btn" id="yaktube-open-shortcuts" aria-label="Klavye Kı sayolları Kı lavuzunu Aç">' +
'⌨️ Kı sayollar (?)' +
2026-08-31 18:48:19 +03:00
'</button>' +
'</div>' +
'<div class="yaktube-loading">' +
'<div class="yaktube-spinner" aria-hidden="true"></div>' +
2026-09-19 05:20:33 +03:00
'<div style="margin-top: 10px;">YouTube ve ' +
escapeHtml ( getInstanceName ()) +
' yerel kütüphanesindeki eşleşmeler taranı yor...</div>' +
2026-08-31 18:48:19 +03:00
'</div>' ;
var shortcutsBtn = document . getElementById ( 'yaktube-open-shortcuts' );
if ( shortcutsBtn ) {
shortcutsBtn . addEventListener ( 'click' , toggleShortcutsModal );
}
2026-09-05 15:48:27 +03:00
ensureFediverseBridge ( query , currentSearchTarget );
announce ( query + ' için arama başlatı ldı .' );
2026-08-31 18:48:19 +03:00
isSearching = true ;
2026-09-19 05:14:11 +03:00
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 } : {};
2026-09-05 15:48:27 +03:00
Promise . all ([
2026-09-19 05:14:11 +03:00
fetch ( '/api-custom/youtube-search?q=' + encodeURIComponent ( query ), fetchOpts )
2026-09-05 15:48:27 +03:00
. then ( function ( res ) {
return res . json ();
})
. catch ( function ( err ) {
2026-09-19 05:14:11 +03:00
if ( err && err . name === 'AbortError' ) return { results : [] };
2026-09-05 15:48:27 +03:00
console . warn ( '[YakTube] YouTube search fetch error:' , err );
return { results : [] };
}),
2026-09-19 05:14:11 +03:00
fetch ( '/api/v1/search/videos?search=' + encodeURIComponent ( query ) + '&isLocal=true&count=12' , fetchOpts )
2026-09-05 15:48:27 +03:00
. then ( function ( res ) {
return res . json ();
})
. catch ( function ( err ) {
2026-09-19 05:14:11 +03:00
if ( err && err . name === 'AbortError' ) return { total : 0 , data : [] };
2026-09-05 15:48:27 +03:00
console . warn ( '[YakTube] Local videos search fetch error:' , err );
return { total : 0 , data : [] };
})
])
. then ( function ( responses ) {
var ytData = responses [ 0 ] || {};
var localData = responses [ 1 ] || {};
var rawYtResults = ytData . results || [];
var uniqueYtResults = [];
2026-08-31 18:48:19 +03:00
var seenIds = {};
2026-09-05 15:48:27 +03:00
rawYtResults . forEach ( function ( item ) {
2026-08-31 18:48:19 +03:00
if ( item && item . id && ! seenIds [ item . id ] && item . title ) {
seenIds [ item . id ] = true ;
2026-09-05 15:48:27 +03:00
uniqueYtResults . push ( item );
2026-08-31 18:48:19 +03:00
}
});
2026-09-05 15:48:27 +03:00
var ytCount = uniqueYtResults . length ;
var localResults = localData . data || [];
var localTotal = typeof localData . total === 'number' ? localData . total : localResults . length ;
if ( ytCount === 0 && localResults . length === 0 ) {
2026-08-29 12:42:36 +03:00
container . innerHTML =
'<div class="yaktube-header">' +
'<div class="yaktube-header-left">' +
'<span class="yaktube-badge">⚡ Canlı YouTube & Web Arama</span>' +
'<h2 class="yaktube-title-h3" style="margin:0; font-size:1.15rem;">"' +
2026-09-05 15:48:27 +03:00
escapeHtml ( query ) +
'" için Sonuç Bulunamadı </h2>' +
2026-08-29 12:42:36 +03:00
'</div>' +
2026-09-05 15:48:27 +03:00
'<button class="yaktube-shortcuts-btn" id="yaktube-open-shortcuts" aria-label="Klavye Kı sayolları Kı lavuzunu Aç">' +
'⌨️ Kı sayollar (?)' +
'</button>' +
2026-08-31 18:48:19 +03:00
'</div>' ;
2026-09-05 15:48:27 +03:00
announce ( query + ' için YouTube veya yerel sunucuda eşleşen video bulunamadı .' );
2026-08-31 18:48:19 +03:00
} else {
2026-09-05 15:48:27 +03:00
var visibleYtCount = Math . min ( 12 , ytCount );
2026-08-29 12:42:36 +03:00
2026-09-05 15:48:27 +03:00
function renderAll () {
// 1. YouTube Section
var ytHeaderHtml = '' ;
var ytCardsHtml = '' ;
var ytLoadMoreHtml = '' ;
2026-08-29 12:42:36 +03:00
2026-09-05 15:48:27 +03:00
if ( ytCount > 0 ) {
var countText = ytCount === 1 ? '1 Video Bulundu' : ytCount + ' Video Bulundu' ;
ytHeaderHtml =
'<div class="yaktube-header">' +
'<div class="yaktube-header-left">' +
'<span class="yaktube-badge">⚡ Canlı YouTube & Web Arama</span>' +
'<h2 class="yaktube-title-h3" style="margin:0; font-size:1.15rem;">"' +
escapeHtml ( query ) +
'" için ' +
countText +
'</h2>' +
2026-08-31 18:48:19 +03:00
'</div>' +
2026-09-05 15:48:27 +03:00
'<button class="yaktube-shortcuts-btn" id="yaktube-open-shortcuts" aria-label="Klavye Kı sayolları Kı lavuzunu Aç">' +
'⌨️ Kı sayollar (?)' +
2026-08-31 18:48:19 +03:00
'</button>' +
'</div>' ;
2026-09-05 15:48:27 +03:00
var visibleResults = uniqueYtResults . slice ( 0 , visibleYtCount );
visibleResults . forEach ( function ( item ) {
var isLocal = !! item . isDownloaded ;
var isLive = !! item . isLive ;
var statusBadge = '' ;
if ( isLive ) {
statusBadge =
'<span class="yaktube-badge" style="background: #dc2626; color: #fff; margin-bottom: 6px; display: inline-block; font-weight: 700;">🔴 CANLI YAYIN</span>' ;
} else if ( isLocal ) {
statusBadge =
'<span class="yaktube-badge" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); margin-bottom: 6px; display: inline-block;">✅ ' +
escapeHtml ( getInstanceName ()) +
2026-09-19 05:20:33 +03:00
"'da Mevcut</span>" ;
2026-09-05 15:48:27 +03:00
}
var actionButton = '' ;
if ( isLive ) {
actionButton =
'<button class="yaktube-live-btn" data-url="' +
escapeHtml ( item . url ) +
'" data-title="' +
escapeHtml ( item . title ) +
'" style="background: linear-gradient(135deg, #ef4444 0%, #b91c1c 100%); color: #fff; border: none; padding: 8px 14px; border-radius: 6px; font-weight: 700; cursor: pointer; display: inline-flex; align-items: center; gap: 6px;" aria-label="' +
escapeHtml ( item . title ) +
' canlı yayı nı nı hemen izle">🔴 Canlı Yayı nı İzle</button>' ;
} else if ( isLocal && item . localVideo && item . localVideo . url ) {
actionButton =
'<a href="' +
escapeHtml ( item . localVideo . url ) +
'" class="yaktube-play-btn" aria-label="' +
escapeHtml ( item . title ) +
2026-09-19 05:20:33 +03:00
' videosunu ' +
2026-09-05 15:48:27 +03:00
escapeHtml ( getInstanceName ()) +
2026-09-19 05:20:33 +03:00
'\'dan hemen izle">▶️ ' +
escapeHtml ( getInstanceName ()) +
"'dan İzle</a>" ;
2026-09-05 15:48:27 +03:00
} else {
actionButton =
'<button class="yaktube-import-btn" data-url="' +
escapeHtml ( item . url ) +
'" data-title="' +
escapeHtml ( item . title ) +
'" aria-label="' +
escapeHtml ( item . title ) +
' videosunu ' +
escapeHtml ( getInstanceName ()) +
'\'a aktar ve izle">📥 ' +
escapeHtml ( getInstanceName ()) +
2026-09-19 05:20:33 +03:00
"'a Aktar & İzle</button>" ;
2026-09-05 15:48:27 +03:00
}
2026-09-19 05:20:33 +03:00
var durationText = isLive ? 'CANLI' : item . durationFormatted || formatClockTime ( item . duration );
2026-09-05 15:48:27 +03:00
var cardLabel =
item . title +
( isLive ? ' (Canlı Yayı n)' : isLocal ? ' (' + getInstanceName () + ' kütüphanenizde mevcut)' : '' ) +
', ' +
item . channel +
' kanalı , Süre: ' +
durationText ;
ytCardsHtml +=
'<div class="yaktube-card" role="article" tabindex="0" aria-label="' +
escapeHtml ( cardLabel ) +
'">' +
'<div class="yaktube-thumb-wrap">' +
'<img class="yaktube-thumb-img" src="' +
escapeHtml ( item . thumbnail ) +
'" alt="' +
escapeHtml ( item . title ) +
'" loading="lazy" />' +
'<span class="yaktube-duration-badge" style="' +
( isLive ? 'background:#dc2626;font-weight:700;' : '' ) +
'" aria-label="Süre">' +
escapeHtml ( durationText ) +
'</span>' +
'</div>' +
'<div class="yaktube-info">' +
'<div>' +
statusBadge +
'<h3 class="yaktube-video-name" title="' +
escapeHtml ( item . title ) +
'" style="margin:0; font-size:13px; font-weight:600; line-height:1.35;">' +
escapeHtml ( item . title ) +
'</h3>' +
'<div class="yaktube-channel-name">' +
escapeHtml ( item . channel ) +
'</div>' +
'</div>' +
actionButton +
'</div>' +
'</div>' ;
});
if ( visibleYtCount < ytCount ) {
ytLoadMoreHtml =
'<div style="text-align: center; margin: 20px 0 10px 0;">' +
'<button id="yaktube-load-more-btn" class="yaktube-load-more-btn" style="background: #1e293b; color: #ff8f37; border: 1px solid #ff8f37; padding: 10px 24px; border-radius: 8px; font-weight: 700; font-size: 14px; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; transition: all 0.2s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.3);" aria-label="Daha fazla video yükle">' +
'➕ Daha Fazla Video Göster (' +
visibleYtCount +
' / ' +
ytCount +
')' +
'</button>' +
'</div>' ;
}
}
// 2. Local Videos Shelf
var localShelfHtml = '' ;
if ( localResults . length > 0 ) {
var localCardsHtml = '' ;
localResults . forEach ( function ( lItem ) {
2026-09-19 05:20:33 +03:00
var lThumb =
lItem . thumbnailPath ||
( lItem . thumbnails && lItem . thumbnails [ 0 ]
? lItem . thumbnails [ 0 ]. fileUrl
: '/client/assets/images/default-thumbnail.jpg' );
2026-09-05 15:48:27 +03:00
var lDuration = formatClockTime ( lItem . duration );
2026-09-19 05:20:33 +03:00
var lChannel =
( lItem . channel && ( lItem . channel . displayName || lItem . channel . name )) ||
( lItem . account && ( lItem . account . displayName || lItem . account . name )) ||
getInstanceName ();
var lWatchUrl = lItem . url || '/videos/watch/' + ( lItem . shortUUID || lItem . uuid );
2026-09-05 15:48:27 +03:00
var lViews = ( lItem . views || 0 ) + ' görüntüleme' ;
2026-09-19 05:20:33 +03:00
var lLabel =
lItem . name +
', ' +
lChannel +
', ' +
getInstanceName () +
' yerel sunucusunda mevcut, Süre: ' +
lDuration ;
2026-09-05 15:48:27 +03:00
localCardsHtml +=
'<div class="yaktube-card yaktube-local-card" role="article" tabindex="0" aria-label="' +
escapeHtml ( lLabel ) +
'">' +
2026-09-19 05:20:33 +03:00
'<a href="' +
escapeHtml ( lWatchUrl ) +
'" style="text-decoration:none; color:inherit; display:flex; flex-direction:column; height:100%;">' +
2026-09-05 15:48:27 +03:00
'<div class="yaktube-thumb-wrap">' +
'<img class="yaktube-thumb-img" src="' +
escapeHtml ( lThumb ) +
'" alt="' +
escapeHtml ( lItem . name ) +
'" loading="lazy" onerror="this.src=\'/client/assets/images/default-thumbnail.jpg\'" />' +
'<span class="yaktube-duration-badge" aria-label="Süre">' +
escapeHtml ( lDuration ) +
'</span>' +
'</div>' +
'<div class="yaktube-info">' +
'<div>' +
2026-09-19 05:20:33 +03:00
'<span class="yaktube-badge" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); margin-bottom: 6px; display: inline-block;">✅ ' +
escapeHtml ( getInstanceName ()) +
"'da Mevcut</span>" +
2026-09-05 15:48:27 +03:00
'<h3 class="yaktube-video-name" title="' +
escapeHtml ( lItem . name ) +
'" style="margin:0; font-size:13px; font-weight:600; line-height:1.35;">' +
escapeHtml ( lItem . name ) +
'</h3>' +
'<div class="yaktube-channel-name">' +
2026-09-19 05:20:33 +03:00
escapeHtml ( lChannel ) +
' • ' +
escapeHtml ( lViews ) +
2026-09-05 15:48:27 +03:00
'</div>' +
'</div>' +
2026-09-19 05:20:33 +03:00
'<div class="yaktube-play-btn" style="margin-top:8px;">▶️ ' +
escapeHtml ( getInstanceName ()) +
"'dan Hemen İzle</div>" +
2026-09-05 15:48:27 +03:00
'</div>' +
'</a>' +
'</div>' ;
});
localShelfHtml =
2026-09-19 05:20:33 +03:00
'<div id="yaktube-local-shelf" class="yaktube-local-shelf" role="region" aria-label="' +
escapeHtml ( getInstanceName ()) +
' Yerel Kütüphanesi">' +
2026-09-05 15:48:27 +03:00
'<div class="yaktube-header" style="margin-top: 24px; border-bottom-color: rgba(16, 185, 129, 0.3);">' +
'<div class="yaktube-header-left">' +
'<span class="yaktube-badge" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%);">✅ Yerel Kütüphane</span>' +
'<h2 class="yaktube-title-h3" style="margin:0; font-size:1.15rem;">"' +
escapeHtml ( query ) +
2026-09-19 05:20:33 +03:00
'" için ' +
escapeHtml ( getInstanceName ()) +
' Sunucusunda ' +
localTotal +
' Video Bulundu</h2>' +
2026-09-05 15:48:27 +03:00
'</div>' +
'<span class="yaktube-local-shelf-sub">Doğrudan sunucumuzda barı ndı rı lan, yüksek hı zlı ve anı nda izlenebilen videolar</span>' +
'</div>' +
'<div class="yaktube-grid">' +
localCardsHtml +
'</div>' +
'</div>' ;
2026-08-31 18:48:19 +03:00
}
container . innerHTML =
2026-09-05 15:48:27 +03:00
ytHeaderHtml +
( ytCount > 0 ? '<div class="yaktube-grid">' + ytCardsHtml + '</div>' : '' ) +
ytLoadMoreHtml +
localShelfHtml ;
2026-08-31 18:48:19 +03:00
var sBtn = document . getElementById ( 'yaktube-open-shortcuts' );
if ( sBtn ) {
sBtn . addEventListener ( 'click' , toggleShortcutsModal );
}
var loadMoreBtn = document . getElementById ( 'yaktube-load-more-btn' );
if ( loadMoreBtn ) {
loadMoreBtn . addEventListener ( 'click' , function () {
2026-09-05 15:48:27 +03:00
visibleYtCount = Math . min ( visibleYtCount + 6 , ytCount );
renderAll ();
announce ( visibleYtCount + ' adet video gösteriliyor.' );
2026-08-31 18:48:19 +03:00
});
}
container . querySelectorAll ( '.yaktube-live-btn' ). forEach ( function ( btn ) {
btn . addEventListener ( 'click' , function ( e ) {
var liveUrl = e . currentTarget . getAttribute ( 'data-url' );
var liveTitle = e . currentTarget . getAttribute ( 'data-title' );
handleLiveStream ( liveUrl , liveTitle );
});
});
container . querySelectorAll ( '.yaktube-import-btn' ). forEach ( function ( btn ) {
btn . addEventListener ( 'click' , function ( e ) {
var videoUrl = e . currentTarget . getAttribute ( 'data-url' );
var videoTitle = e . currentTarget . getAttribute ( 'data-title' );
handleImport ( videoUrl , e . currentTarget , videoTitle );
});
2026-08-29 12:42:36 +03:00
});
}
2026-09-05 15:48:27 +03:00
renderAll ();
var announceMsg = '"' + query + '" için arama sonuçları listelendi: ' ;
if ( localTotal > 0 ) {
announceMsg += localTotal + ' adet yerel ' + getInstanceName () + ' videosu, ' ;
}
if ( ytCount > 0 ) {
announceMsg += ytCount + ' adet YouTube sonucu ve ' ;
}
announceMsg += 'Fediverse sonuçları hazı r.' ;
announce ( announceMsg );
2026-08-29 12:42:36 +03:00
}
2026-08-31 18:48:19 +03:00
})
. catch ( function ( err ) {
console . error ( '[YakTube] Search fetch error:' , err );
container . innerHTML =
'<div class="yaktube-header">' +
'<div class="yaktube-header-left">' +
2026-09-05 15:48:27 +03:00
'<span class="yaktube-badge">⚡ Canlı YouTube & Web Arama</span>' +
'<h2 class="yaktube-title-h3" style="margin:0; font-size:1.15rem;">Arama sı rası nda bir bağlantı sorunu oluştu.</h2>' +
2026-08-31 18:48:19 +03:00
'</div>' +
'</div>' ;
2026-09-05 15:48:27 +03:00
announce ( 'Arama sı rası nda bir bağlantı sorunu oluştu.' );
2026-08-31 18:48:19 +03:00
})
. finally ( function () {
isSearching = false ;
2026-09-05 15:48:27 +03:00
ensureFediverseBridge ( query , currentSearchTarget );
2026-08-31 18:48:19 +03:00
});
}
2026-08-29 12:42:36 +03:00
2026-08-31 18:48:19 +03:00
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
// 8. FULL YOUTUBE SEARCH & VOICE MODAL ENGINE (v1.3.3 - Guaranteed Close Fix)
// --------------------------------------------------------------------------
var activeVoiceRec = null ;
var isVoiceListening = false ;
2026-09-03 20:16:33 +03:00
// Dynamic Search History & Trending Helpers with YakNet Cloud Sync
function isUserLoggedIn () {
try {
var token = localStorage . getItem ( 'access_token' );
var username = localStorage . getItem ( 'username' );
return Boolean ( token || username );
} catch ( e ) {
return false ;
}
}
function isAccountSyncEnabled () {
try {
var userPref = localStorage . getItem ( 'yaktube_sync_search_history' );
if ( userPref === 'false' ) return false ;
return isUserLoggedIn ();
} catch ( e ) {
return false ;
}
}
2026-08-31 18:48:19 +03:00
function getRecentSearches () {
try {
var data = localStorage . getItem ( 'yaktube_recent_searches' );
if ( data ) {
var parsed = JSON . parse ( data );
if ( Array . isArray ( parsed ) && parsed . length > 0 ) return parsed ;
2026-08-29 12:42:36 +03:00
}
2026-08-31 18:48:19 +03:00
} catch ( e ) {}
// Default initial recent searches for first-time visitors
var defaults = [ 'Tarkan' , 'Barış Manço' , 'Yapay Zeka' , 'Canlı Haber' ];
2026-09-04 07:32:05 +03:00
if ( ! isUserLoggedIn ()) {
try {
localStorage . setItem ( 'yaktube_recent_searches' , JSON . stringify ( defaults ));
} catch ( e ) {}
}
2026-08-31 18:48:19 +03:00
return defaults ;
}
2026-08-29 12:42:36 +03:00
2026-09-04 07:32:05 +03:00
function callCloudSearchHistory ( method , queryParams , body , onSuccess , onError ) {
var token = localStorage . getItem ( 'access_token' );
var email = localStorage . getItem ( 'email' );
2026-09-04 07:38:28 +03:00
var username = localStorage . getItem ( 'username' );
2026-09-04 07:32:05 +03:00
var headers = {};
if ( body ) headers [ 'Content-Type' ] = 'application/json' ;
if ( token ) headers [ 'Authorization' ] = 'Bearer ' + token ;
if ( email ) headers [ 'X-User-Email' ] = email ;
2026-09-04 07:38:28 +03:00
if ( username ) headers [ 'X-User-Username' ] = username ;
2026-09-04 07:32:05 +03:00
var authUrl = 'https://auth.yakhub.com.tr/api/yaktube/search-history' + ( queryParams || '' );
var bridgeUrl = '/api-custom/search-history' + ( queryParams || '' );
var options = {
method : method ,
headers : headers ,
credentials : 'include'
};
if ( body ) options . body = JSON . stringify ( body );
fetch ( authUrl , options )
. then ( function ( res ) {
if ( ! res . ok ) throw new Error ( 'Auth server status ' + res . status );
return res . json ();
})
. then ( function ( data ) {
if ( typeof onSuccess === 'function' ) onSuccess ( data );
})
. catch ( function () {
// Fallback to local PeerTube bridge proxy
fetch ( bridgeUrl , options )
2026-09-04 12:54:11 +03:00
. then ( function ( res ) {
return res . json ();
})
2026-09-04 07:32:05 +03:00
. then ( function ( data ) {
if ( typeof onSuccess === 'function' ) onSuccess ( data );
})
. catch ( function ( e ) {
if ( typeof onError === 'function' ) onError ( e );
});
});
}
2026-09-03 20:16:33 +03:00
var isSyncingHistory = false ;
function syncCloudSearchHistory ( callback ) {
if ( ! isAccountSyncEnabled () || isSyncingHistory ) return ;
isSyncingHistory = true ;
2026-09-04 12:54:11 +03:00
callCloudSearchHistory (
'GET' ,
'' ,
null ,
function ( data ) {
isSyncingHistory = false ;
if ( data && data . status === 'success' && Array . isArray ( data . history )) {
if ( data . history . length > 0 ) {
localStorage . setItem ( 'yaktube_recent_searches' , JSON . stringify ( data . history ));
}
if ( typeof callback === 'function' ) callback ();
2026-09-03 20:16:33 +03:00
}
2026-09-04 12:54:11 +03:00
},
function () {
isSyncingHistory = false ;
2026-09-04 07:32:05 +03:00
}
2026-09-04 12:54:11 +03:00
);
2026-09-03 20:16:33 +03:00
}
2026-08-31 18:48:19 +03:00
function saveRecentSearch ( query ) {
if ( ! query || query . trim (). length < 2 ) return ;
var q = query . trim ();
try {
var list = getRecentSearches ();
list = list . filter ( function ( item ) {
return item . toLowerCase () !== q . toLowerCase ();
});
list . unshift ( q );
2026-09-03 20:16:33 +03:00
if ( list . length > 12 ) list = list . slice ( 0 , 12 );
2026-08-31 18:48:19 +03:00
localStorage . setItem ( 'yaktube_recent_searches' , JSON . stringify ( list ));
} catch ( e ) {}
2026-09-03 20:16:33 +03:00
// If user is logged in and sync is active, sync with YakNet account
if ( isAccountSyncEnabled ()) {
2026-09-04 07:32:05 +03:00
callCloudSearchHistory ( 'POST' , '' , { query : q , device_name : 'YakTube Web' });
2026-09-03 20:16:33 +03:00
}
2026-08-31 18:48:19 +03:00
}
2026-08-29 19:22:42 +03:00
2026-08-31 18:48:19 +03:00
function clearRecentSearches () {
try {
localStorage . removeItem ( 'yaktube_recent_searches' );
} catch ( e ) {}
2026-09-03 20:16:33 +03:00
if ( isAccountSyncEnabled ()) {
2026-09-04 07:32:05 +03:00
callCloudSearchHistory ( 'DELETE' , '' , null );
2026-09-03 20:16:33 +03:00
}
2026-08-31 18:48:19 +03:00
renderDynamicSearchChips ();
announce ( 'Arama geçmişi temizlendi.' );
}
2026-08-31 14:43:22 +03:00
2026-08-31 18:48:19 +03:00
var popularTrendingQueries = [
{ title : '🎵 Türkçe Pop' , q : 'Türkçe Pop' },
{ title : '🎸 Anadolu Rock' , q : 'Anadolu Rock' },
{ title : '🤖 Yapay Zeka' , q : 'Yapay Zeka' },
{ title : '💻 Yazı lı m Dersleri' , q : 'Yazı lı m Dersleri' },
{ title : '🎧 Sesli Kitap & Radyo Tiyatrosu' , q : 'Sesli Kitap' },
{ title : '📺 Canlı Haber' , q : 'Canlı Haber' },
{ title : '🌍 Bilim & Belgesel' , q : 'Bilim Belgeseli' },
{ title : '🎼 Akustik Canlı Performans' , q : 'Akustik Canlı ' },
{ title : '🎙️ Podcast & Söyleşi' , q : 'Podcast Türkçe' }
];
2026-08-31 14:43:22 +03:00
2026-08-31 18:48:19 +03:00
// Auto capture search query from URL into recent searches
function captureUrlSearchQuery () {
try {
var params = new URLSearchParams ( window . location . search );
var q = params . get ( 'search' ) || params . get ( 'q' );
if ( q && q . trim (). length >= 2 ) {
saveRecentSearch ( q . trim ());
}
} catch ( e ) {}
}
captureUrlSearchQuery ();
window . addEventListener ( 'popstate' , captureUrlSearchQuery );
2026-08-31 14:43:22 +03:00
2026-08-31 18:48:19 +03:00
function renderDynamicSearchChips () {
var modal = document . getElementById ( 'yaktube-full-search-modal' );
if ( ! modal ) return ;
2026-08-31 14:43:22 +03:00
2026-08-31 18:48:19 +03:00
var container = modal . querySelector ( '#yaktube-modal-dynamic-chips-section' );
if ( ! container ) return ;
2026-08-31 18:46:13 +03:00
2026-08-31 18:48:19 +03:00
var recent = getRecentSearches ();
2026-09-03 20:16:33 +03:00
var loggedIn = isUserLoggedIn ();
var syncEnabled = isAccountSyncEnabled ();
2026-08-31 18:48:19 +03:00
var html = '' ;
2026-08-31 14:43:22 +03:00
2026-09-03 20:16:33 +03:00
// 1. Recent searches section
html +=
'<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; flex-wrap: wrap; gap: 8px;">' +
'<div class="yaktube-modal-section-title" style="margin-bottom: 0;">🕒 Son Aramaları nı z</div>' +
'<div style="display: flex; align-items: center; gap: 8px;">' ;
if ( loggedIn ) {
var syncTitle = syncEnabled
? 'YakNet Bulut Eşitlemesi Aktif (Tüm cihazları nı zda senkronize edilir. Kapatmak için tı klayı n.)'
: 'Eşitleme Kapalı (Aramalar sadece bu cihazda saklanı r. Açmak için tı klayı n.)' ;
var syncText = syncEnabled ? '☁️ Eşitleme: Açık' : '☁️ Eşitleme: Kapalı ' ;
var syncBg = syncEnabled ? '#0f766e' : '#334155' ;
var syncColor = syncEnabled ? '#5eead4' : '#94a3b8' ;
html +=
'<button id="yaktube-sync-toggle-btn" style="background: ' +
syncBg +
'; border: 1px solid rgba(255,255,255,0.15); border-radius: 6px; padding: 2px 8px; color: ' +
syncColor +
'; font-size: 11px; font-weight: 600; cursor: pointer; transition: all 0.2s ease;" title="' +
syncTitle +
'" aria-label="' +
syncTitle +
'">' +
syncText +
'</button>' ;
} else {
html +=
'<span style="font-size: 11px; color: #94a3b8; background: #1e293b; border-radius: 6px; padding: 2px 8px;" title="Misafir modundası nı z, aramalar sadece bu cihazda saklanı r">🔒 Bu Cihazda</span>' ;
}
2026-08-31 18:48:19 +03:00
if ( recent . length > 0 ) {
2026-08-31 14:43:22 +03:00
html +=
2026-09-03 20:16:33 +03:00
'<button id="yaktube-clear-recent-btn" style="background: transparent; border: none; color: #ff8f37; font-size: 12px; font-weight: 600; cursor: pointer; text-decoration: underline;" aria-label="Arama geçmişini temizle">Geçmişi Temizle</button>' ;
}
2026-08-31 18:48:19 +03:00
2026-09-03 20:16:33 +03:00
html += '</div></div>' ;
if ( recent . length > 0 ) {
html += '<div class="yaktube-modal-chips-grid" style="margin-bottom: 18px;">' ;
2026-08-31 18:48:19 +03:00
recent . forEach ( function ( term ) {
var safeTerm = term . replace ( /"/g , '"' );
html +=
'<button class="yaktube-modal-chip yaktube-recent-chip" data-query="' +
safeTerm +
'" aria-label="Son arama: ' +
safeTerm +
'">🕒 ' +
safeTerm +
'</button>' ;
});
html += '</div>' ;
}
// 2. Popular & Trending searches section
html +=
'<div class="yaktube-modal-section-title">🔥 Hı zlı ve Popüler Aramalar</div>' +
'<div class="yaktube-modal-chips-grid">' ;
popularTrendingQueries . forEach ( function ( item ) {
var safeQ = item . q . replace ( /"/g , '"' );
html +=
'<button class="yaktube-modal-chip" data-query="' +
safeQ +
'" aria-label="Popüler arama: ' +
safeQ +
'">' +
item . title +
2026-08-31 14:43:22 +03:00
'</button>' ;
});
html += '</div>' ;
2026-08-31 18:48:19 +03:00
container . innerHTML = html ;
// Attach click events
container . querySelectorAll ( '.yaktube-modal-chip' ). forEach ( function ( chip ) {
chip . addEventListener ( 'click' , function () {
var query = chip . getAttribute ( 'data-query' );
var inp = modal . querySelector ( '#yaktube-modal-input' );
if ( inp ) inp . value = query ;
if ( typeof window . __yaktube_execute_search__ === 'function' ) {
window . __yaktube_execute_search__ ( query );
}
});
});
var clearBtn = container . querySelector ( '#yaktube-clear-recent-btn' );
if ( clearBtn ) {
clearBtn . addEventListener ( 'click' , function ( e ) {
e . preventDefault ();
e . stopPropagation ();
clearRecentSearches ();
});
}
2026-09-03 20:16:33 +03:00
var syncToggleBtn = container . querySelector ( '#yaktube-sync-toggle-btn' );
if ( syncToggleBtn ) {
syncToggleBtn . addEventListener ( 'click' , function ( e ) {
e . preventDefault ();
e . stopPropagation ();
var current = localStorage . getItem ( 'yaktube_sync_search_history' ) !== 'false' ;
var next = ! current ;
localStorage . setItem ( 'yaktube_sync_search_history' , next ? 'true' : 'false' );
if ( next ) {
announce ( 'YakNet arama geçmişi eşitlemesi açıldı .' );
syncCloudSearchHistory ( renderDynamicSearchChips );
} else {
announce ( 'Arama geçmişi eşitlemesi kapatı ldı . Aramalar sadece bu cihazda saklanacak.' );
}
renderDynamicSearchChips ();
});
}
2026-08-31 14:43:22 +03:00
}
2026-08-31 18:48:19 +03:00
function createFullSearchModal () {
var existingModal = document . getElementById ( 'yaktube-full-search-modal' );
if ( existingModal ) return existingModal ;
2026-08-31 14:43:22 +03:00
2026-08-31 18:48:19 +03:00
var modal = document . createElement ( 'div' );
modal . id = 'yaktube-full-search-modal' ;
modal . className = 'yaktube-full-search-modal' ;
modal . setAttribute ( 'role' , 'dialog' );
modal . setAttribute ( 'aria-modal' , 'true' );
modal . setAttribute ( 'aria-labelledby' , 'yaktube-modal-search-title' );
2026-08-31 14:43:22 +03:00
2026-08-31 18:48:19 +03:00
modal . innerHTML =
'<div class="yaktube-modal-search-card">' +
'<div class="yaktube-modal-search-bar">' +
'<button id="yaktube-modal-back-btn" class="yaktube-modal-back-btn" aria-label="Aramadan Çık (Escape)" title="Aramadan Çık (Escape)">←</button>' +
'<div class="yaktube-modal-input-wrap">' +
'<input id="yaktube-modal-input" type="search" placeholder="Video veya kanal ara..." aria-label="Arama Metni">' +
'<button id="yaktube-modal-clear-btn" class="yaktube-modal-clear-btn" aria-label="Metni Temizle">✕</button>' +
'</div>' +
'<button id="yaktube-modal-submit-btn" class="yaktube-modal-search-action-btn" aria-label="Ara">🔍 Ara</button>' +
'<button id="yaktube-modal-voice-btn" class="yaktube-modal-voice-btn" aria-label="Sesle Ara (Shift+V)" title="Sesle Ara">🎙️</button>' +
'</div>' +
'<div class="yaktube-modal-body">' +
'<div id="yaktube-modal-voice-section" class="yaktube-modal-voice-section">' +
'<div id="yaktube-modal-voice-status" class="yaktube-modal-voice-status">🎙️ Dinleniyor...</div>' +
'<div id="yaktube-modal-voice-transcript" class="yaktube-modal-voice-transcript">Lütfen aramak istediğiniz videoyu söyleyin...</div>' +
'<button id="yaktube-modal-mic-pulse" class="yaktube-modal-mic-pulse-btn" aria-label="Mikrofonu durdur">🎙️</button>' +
'</div>' +
'<div id="yaktube-modal-dynamic-chips-section"></div>' +
'<div class="yaktube-modal-hint-footer">' +
'<span>💡 <b>Enter</b> ile ara, <b>Shift+V</b> ile sesli ara</span>' +
'<span><b>Escape</b> ile kapat</span>' +
'</div>' +
'</div>' +
'</div>' ;
2026-08-31 14:43:22 +03:00
2026-08-31 18:48:19 +03:00
document . body . appendChild ( modal );
var inp = modal . querySelector ( '#yaktube-modal-input' );
var clearBtn = modal . querySelector ( '#yaktube-modal-clear-btn' );
var backBtn = modal . querySelector ( '#yaktube-modal-back-btn' );
var submitBtn = modal . querySelector ( '#yaktube-modal-submit-btn' );
var voiceBtn = modal . querySelector ( '#yaktube-modal-voice-btn' );
var micPulseBtn = modal . querySelector ( '#yaktube-modal-mic-pulse' );
// Input typing & clear
inp . addEventListener ( 'input' , function () {
if ( inp . value && inp . value . trim (). length > 0 ) {
clearBtn . style . display = 'inline-flex' ;
} else {
clearBtn . style . display = 'none' ;
2026-08-31 14:43:22 +03:00
}
});
2026-08-31 18:48:19 +03:00
clearBtn . addEventListener ( 'click' , function () {
inp . value = '' ;
clearBtn . style . display = 'none' ;
inp . focus ();
announce ( 'Arama metni temizlendi.' );
});
// Close on Back button or Escape
backBtn . addEventListener ( 'click' , function ( e ) {
2026-08-31 14:43:22 +03:00
e . preventDefault ();
e . stopPropagation ();
2026-08-31 18:48:19 +03:00
closeFullSearchModal ();
2026-08-31 14:43:22 +03:00
});
2026-08-31 18:48:19 +03:00
modal . addEventListener ( 'click' , function ( e ) {
if ( e . target === modal ) {
closeFullSearchModal ();
}
});
2026-08-29 19:22:42 +03:00
2026-08-31 18:48:19 +03:00
// Search execute
function executeSearch ( query ) {
var q = ( query || inp . value || '' ). trim ();
if ( q ) {
saveRecentSearch ( q );
announce ( '"' + q + '" için arama yapı lı yor...' , true );
closeFullSearchModal ();
2026-09-05 15:48:27 +03:00
window . location . href = '/search?search=' + encodeURIComponent ( q ) + '&searchTarget=search-index' ;
2026-08-31 18:48:19 +03:00
}
2026-08-29 21:47:30 +03:00
}
2026-08-31 18:48:19 +03:00
window . __yaktube_execute_search__ = executeSearch ;
renderDynamicSearchChips ();
2026-08-29 21:47:30 +03:00
2026-08-31 18:48:19 +03:00
submitBtn . addEventListener ( 'click' , function () {
2026-08-29 23:02:50 +03:00
executeSearch ();
2026-08-31 18:48:19 +03:00
});
inp . addEventListener ( 'keydown' , function ( e ) {
if ( e . key === 'Enter' ) {
e . preventDefault ();
executeSearch ();
} else if ( e . key === 'Escape' ) {
e . preventDefault ();
closeFullSearchModal ();
}
});
// Chips click
modal . querySelectorAll ( '.yaktube-modal-chip' ). forEach ( function ( chip ) {
chip . addEventListener ( 'click' , function () {
var query = chip . getAttribute ( 'data-query' );
executeSearch ( query );
});
});
// Voice button click
voiceBtn . addEventListener ( 'click' , function () {
toggleVoiceInsideModal ();
});
micPulseBtn . addEventListener ( 'click' , function () {
toggleVoiceInsideModal ();
});
return modal ;
}
function openFullSearchModal ( initialQuery , autoStartVoice ) {
var modal = createFullSearchModal ();
modal . classList . add ( 'yaktube-modal-open' );
modal . style . display = 'flex' ;
renderDynamicSearchChips ();
2026-09-03 20:16:33 +03:00
syncCloudSearchHistory ( renderDynamicSearchChips );
2026-08-31 18:48:19 +03:00
var inp = modal . querySelector ( '#yaktube-modal-input' );
var clearBtn = modal . querySelector ( '#yaktube-modal-clear-btn' );
if ( inp ) {
inp . value = initialQuery || '' ;
if ( inp . value ) clearBtn . style . display = 'inline-flex' ;
setTimeout ( function () {
inp . focus ();
inp . select ();
}, 50 );
}
announce ( 'Arama penceresi açıldı . Arayacağını z konuyu yazı n veya sesle arayı n.' , true );
if ( autoStartVoice ) {
startVoiceInsideModal ();
}
}
function closeFullSearchModal () {
var modal = document . getElementById ( 'yaktube-full-search-modal' );
if ( modal ) {
modal . classList . remove ( 'yaktube-modal-open' );
modal . style . display = 'none' ;
}
stopVoiceInsideModal ();
announce ( 'Arama penceresi kapatı ldı .' );
var trigger = document . querySelector ( '.yaktube-search-trigger-pill' );
if ( trigger ) {
try {
trigger . focus ();
} catch ( e ) {}
}
}
function toggleVoiceInsideModal () {
if ( isVoiceListening ) {
stopVoiceInsideModal ();
} else {
startVoiceInsideModal ();
}
}
function startVoiceInsideModal () {
var SpeechRec = window . SpeechRecognition || window . webkitSpeechRecognition ;
var modal = document . getElementById ( 'yaktube-full-search-modal' );
if ( ! modal ) return ;
var voiceSec = modal . querySelector ( '#yaktube-modal-voice-section' );
var statusText = modal . querySelector ( '#yaktube-modal-voice-status' );
var transcriptEl = modal . querySelector ( '#yaktube-modal-voice-transcript' );
var inp = modal . querySelector ( '#yaktube-modal-input' );
if ( voiceSec ) voiceSec . style . display = 'block' ;
if ( statusText ) statusText . textContent = '🎙️ Dinleniyor...' ;
if ( transcriptEl ) transcriptEl . textContent = 'Lütfen aramak istediğiniz videoyu söyleyin...' ;
if ( ! SpeechRec ) {
if ( statusText ) statusText . textContent = 'Tarayı cı da Ses Tanı ma Desteklenmiyor' ;
if ( transcriptEl ) transcriptEl . textContent = 'Lütfen Google Chrome veya Microsoft Edge kullanı n.' ;
announce ( 'Tarayı cı nı z ses tanı mayı desteklemiyor. Lütfen Chrome veya Edge kullanı n.' );
return ;
}
announce ( 'Sesli arama dinleniyor, lütfen aramak istediğiniz videoyu söyleyin.' , true );
try {
activeVoiceRec = new SpeechRec ();
activeVoiceRec . lang = navigator . language || 'tr-TR' ;
activeVoiceRec . continuous = false ;
activeVoiceRec . interimResults = true ;
activeVoiceRec . maxAlternatives = 1 ;
isVoiceListening = true ;
activeVoiceRec . onresult = function ( ev ) {
if ( ev . results && ev . results [ 0 ]) {
var transcript = ev . results [ 0 ][ 0 ]. transcript . trim ();
if ( transcriptEl ) transcriptEl . textContent = '"' + transcript + '"' ;
if ( inp ) inp . value = transcript ;
if ( ev . results [ 0 ]. isFinal ) {
2026-09-19 05:14:11 +03:00
if ( handleVoiceAssistantCommand ( transcript )) {
return ;
}
2026-08-31 18:48:19 +03:00
announce ( '"' + transcript + '" için arama başlatı lı yor...' , true );
setTimeout ( function () {
closeFullSearchModal ();
2026-09-19 05:20:33 +03:00
window . location . href =
'/search?search=' + encodeURIComponent ( transcript ) + '&searchTarget=search-index' ;
2026-08-31 18:48:19 +03:00
}, 500 );
}
}
};
activeVoiceRec . onerror = function ( ev ) {
if ( statusText ) statusText . textContent = 'Ses Algı lanamadı ' ;
if ( transcriptEl ) transcriptEl . textContent = 'Tekrar denemek için mikrofona dokunun.' ;
stopVoiceInsideModal ();
};
activeVoiceRec . onend = function () {
stopVoiceInsideModal ();
};
activeVoiceRec . start ();
} catch ( err ) {
stopVoiceInsideModal ();
}
}
function stopVoiceInsideModal () {
isVoiceListening = false ;
if ( activeVoiceRec ) {
try {
activeVoiceRec . stop ();
} catch ( e ) {}
activeVoiceRec = null ;
}
var modal = document . getElementById ( 'yaktube-full-search-modal' );
if ( modal ) {
var voiceSec = modal . querySelector ( '#yaktube-modal-voice-section' );
if ( voiceSec ) voiceSec . style . display = 'none' ;
}
}
function transformToYouTubeSearchBar ( searchInp , searchContainer ) {
if ( searchContainer . hasAttribute ( 'data-yaktube-trigger-styled' )) return ;
searchContainer . setAttribute ( 'data-yaktube-trigger-styled' , 'true' );
// Hide original native inputs
searchInp . style . display = 'none' ;
searchInp . setAttribute ( 'tabindex' , '-1' );
searchInp . setAttribute ( 'aria-hidden' , 'true' );
var nativeInputs = searchContainer . querySelectorAll ( 'input, button' );
nativeInputs . forEach ( function ( el ) {
if (
! el . classList . contains ( 'yaktube-search-trigger-pill' ) &&
! el . classList . contains ( 'yaktube-voice-search-btn' )
) {
el . style . display = 'none' ;
el . setAttribute ( 'tabindex' , '-1' );
el . setAttribute ( 'aria-hidden' , 'true' );
}
});
// 1. Create Outer Wrapper
var outerWrap = searchContainer . querySelector ( '.yaktube-search-outer-wrap' );
if ( ! outerWrap ) {
outerWrap = document . createElement ( 'div' );
outerWrap . className = 'yaktube-search-outer-wrap' ;
searchContainer . appendChild ( outerWrap );
}
// 2. Create Single Sleek Pill Trigger
var triggerPill = outerWrap . querySelector ( '.yaktube-search-trigger-pill' );
if ( ! triggerPill ) {
triggerPill = document . createElement ( 'button' );
triggerPill . type = 'button' ;
triggerPill . className = 'yaktube-search-trigger-pill' ;
triggerPill . setAttribute ( 'aria-label' , 'Ara' );
triggerPill . setAttribute ( 'title' , 'Arama Yap (/)' );
triggerPill . innerHTML =
'<span class="yaktube-search-trigger-icon">🔍</span>' +
'<span class="yaktube-search-trigger-text">Ara...</span>' +
'<span class="yaktube-search-trigger-shortcut">/</span>' ;
outerWrap . appendChild ( triggerPill );
triggerPill . addEventListener ( 'click' , function ( e ) {
e . preventDefault ();
openFullSearchModal ( '' , false );
});
}
// 3. YouTube Circular Voice Button (🎙️) Beside the Pill Box
var voiceBtn = outerWrap . querySelector ( '.yaktube-voice-search-btn' );
if ( ! voiceBtn ) {
voiceBtn = document . createElement ( 'button' );
voiceBtn . type = 'button' ;
voiceBtn . className = 'yaktube-voice-search-btn' ;
voiceBtn . setAttribute ( 'aria-label' , 'Sesle Ara (Shift+V)' );
voiceBtn . setAttribute ( 'title' , 'Sesle Ara (Shift+V)' );
voiceBtn . innerHTML = '🎙️' ;
outerWrap . appendChild ( voiceBtn );
voiceBtn . addEventListener ( 'click' , function ( e ) {
e . preventDefault ();
e . stopPropagation ();
openFullSearchModal ( '' , true );
});
}
}
function injectSearchEnhancements () {
var searchContainers = document . querySelectorAll (
'my-search-typeahead, .search-container, .header-search, my-header .search'
);
searchContainers . forEach ( function ( container ) {
var searchInp = container . querySelector ( 'input[type="search"], input[type="text"], input' );
if ( searchInp ) {
transformToYouTubeSearchBar ( searchInp , container );
}
});
// Clean up repetitive card titles and strange relative date buttons
document . querySelectorAll ( 'my-video-miniature, .video-miniature' ). forEach ( function ( card ) {
var thumbLink = card . querySelector ( 'a.thumbnail-link, .video-thumbnail a, a.thumbnail' );
if ( thumbLink && ! thumbLink . hasAttribute ( 'aria-hidden' )) {
thumbLink . setAttribute ( 'aria-hidden' , 'true' );
thumbLink . setAttribute ( 'tabindex' , '-1' );
}
var dateBtn = card . querySelector ( '.video-date-line button, .video-date button, button[title*="tarih"]' );
if ( dateBtn && ! dateBtn . hasAttribute ( 'data-cleaned' )) {
dateBtn . setAttribute ( 'data-cleaned' , 'true' );
var dateText = dateBtn . textContent . trim ();
dateBtn . setAttribute ( 'aria-label' , dateText );
}
});
}
// Global Keyboard Shortcuts
document . addEventListener ( 'keydown' , function ( e ) {
var activeTag = document . activeElement ? document . activeElement . tagName . toLowerCase () : '' ;
var isInputActive = activeTag === 'input' || activeTag === 'textarea' ;
// Shift + V: Open YouTube Voice Search Modal
if ( e . shiftKey && ( e . key === 'V' || e . key === 'v' ) && ! isInputActive ) {
2026-08-29 23:02:50 +03:00
e . preventDefault ();
2026-08-31 18:48:19 +03:00
openFullSearchModal ( '' , true );
}
// Escape: Close Full Search Modal
else if ( e . key === 'Escape' || e . keyCode === 27 ) {
var modal = document . getElementById ( 'yaktube-full-search-modal' );
if ( modal && modal . classList . contains ( 'yaktube-modal-open' )) {
e . preventDefault ();
closeFullSearchModal ();
}
}
// / or S: Open Search Modal
else if (( e . key === '/' || e . key === 's' || e . key === 'S' ) && ! isInputActive && ! e . ctrlKey && ! e . metaKey ) {
e . preventDefault ();
openFullSearchModal ( '' , false );
2026-08-29 23:02:50 +03:00
}
});
2026-09-04 07:32:05 +03:00
// Automatically trigger cloud search history pull on startup if logged in
try {
if ( isAccountSyncEnabled ()) {
syncCloudSearchHistory ();
}
} catch ( e ) {}
2026-08-31 18:48:19 +03:00
// 7.5. Reactive Hybrid Related Videos Engine (Watch Page)
var currentActiveVideoKey = null ;
var isFetchingRelated = false ;
var relatedShowAll = false ;
var cachedRelatedResults = [];
var cachedRelatedQuery = '' ;
function renderRelatedCards ( container , results , query ) {
var count = results ? results . length : 0 ;
if ( count === 0 ) {
container . style . display = 'none' ;
return ;
}
container . style . display = 'block' ;
var limit = relatedShowAll ? count : Math . min ( count , 12 );
var displayed = results . slice ( 0 , limit );
var cardsHtml = '' ;
displayed . forEach ( function ( item ) {
var isLocal = !! item . isDownloaded ;
var statusBadge = isLocal
? '<span class="yaktube-badge" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); margin-bottom: 4px; display: inline-block; font-size:10px; padding:2px 8px;">✅ ' +
getInstanceName () +
"'da İzle</span>"
: '<span class="yaktube-badge" style="background: linear-gradient(135deg, #ff8f37 0%, #ff5e3a 100%); margin-bottom: 4px; display: inline-block; font-size:10px; padding:2px 8px;">⚡ Canlı Keşif</span>' ;
var actionButton = isLocal
? '<a href="' +
item . localVideo . url +
'" class="yaktube-play-btn yaktube-related-play-link" style="font-size:11px; padding:6px 10px;" aria-label="' +
item . title . replace ( /"/g , '"' ) +
2026-09-05 15:54:10 +03:00
' videosunu ' +
escapeHtml ( getInstanceName ()) +
'\'dan hemen izle">▶️ Hemen İzle</a>'
2026-08-31 18:48:19 +03:00
: '<button class="yaktube-import-btn yaktube-related-import" data-url="' +
item . url +
'" data-title="' +
item . title . replace ( /"/g , '"' ) +
'" style="font-size:11px; padding:6px 10px;" aria-label="' +
item . title . replace ( /"/g , '"' ) +
' videosunu ' +
getInstanceName () +
'\'a indir ve izle">📥 İndir ve İzle</button>' ;
var cardLabel =
item . title +
( isLocal ? ' (' + getInstanceName () + ' kütüphanenizde mevcut)' : ' (Canlı YouTube keşfi)' ) +
', ' +
item . channel +
' kanalı , Süre: ' +
item . durationFormatted ;
cardsHtml +=
'<div class="yaktube-card" role="article" tabindex="0" style="margin-bottom:10px;" aria-label="' +
cardLabel . replace ( /"/g , '"' ) +
'">' +
'<div class="yaktube-thumb-wrap">' +
'<img class="yaktube-thumb-img" src="' +
item . thumbnail +
'" alt="' +
item . title . replace ( /"/g , '"' ) +
'" loading="lazy" />' +
'<span class="yaktube-duration-badge" aria-label="Süre">' +
item . durationFormatted +
'</span>' +
'</div>' +
'<div class="yaktube-info">' +
'<div>' +
statusBadge +
'<h4 class="yaktube-video-name" title="' +
item . title +
'" style="margin:0; font-size:12px; font-weight:600; line-height:1.35;">' +
item . title +
'</h4>' +
'<div class="yaktube-channel-name" style="font-size:10px;">' +
item . channel +
'</div>' +
'</div>' +
actionButton +
'</div>' +
'</div>' ;
2026-08-29 23:02:50 +03:00
});
2026-08-31 18:48:19 +03:00
var loadMoreBtn = '' ;
if ( count > 12 ) {
if ( ! relatedShowAll ) {
loadMoreBtn =
'<button id="yaktube-related-toggle-more" class="yaktube-shortcuts-btn" style="width:100%; margin-top:12px; padding:10px; justify-content:center; font-weight:700; background:rgba(255,143,55,0.15); border-color:#ff8f37; color:#fff;" aria-label="Daha fazla önerilen video göster">' +
'➕ Daha Fazla Öneri Göster (' +
( count - 12 ) +
' video daha)' +
'</button>' ;
} else {
loadMoreBtn =
'<button id="yaktube-related-toggle-more" class="yaktube-shortcuts-btn" style="width:100%; margin-top:12px; padding:8px; justify-content:center; font-size:11px;" aria-label="Daha az öneri göster">' +
'🔼 Daha Az Göster' +
'</button>' ;
}
}
var headerCountText = count > 12 && ! relatedShowAll ? displayed . length + ' / ' + count : count ;
container . innerHTML =
'<div class="yaktube-header">' +
'<div class="yaktube-header-left">' +
'<span class="yaktube-badge">🔥 ' +
getInstanceName () +
' Akı llı Önerilenler</span>' +
'<h3 class="yaktube-title-h3" style="margin:0; font-size:1.05rem;">"' +
query +
'" ile İlgili Öneriler (' +
headerCountText +
')</h3>' +
'</div>' +
'<button id="yaktube-related-refresh-btn" class="yaktube-shortcuts-btn" aria-label="Önerileri Yenile">' +
'🔄 Yenile' +
'</button>' +
'</div>' +
'<div class="yaktube-grid" style="grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:12px;">' +
cardsHtml +
'</div>' +
loadMoreBtn ;
var toggleBtn = document . getElementById ( 'yaktube-related-toggle-more' );
if ( toggleBtn ) {
toggleBtn . addEventListener ( 'click' , function () {
relatedShowAll = ! relatedShowAll ;
renderRelatedCards ( container , cachedRelatedResults , cachedRelatedQuery );
announce (
relatedShowAll ? 'Tüm ' + cachedRelatedResults . length + ' önerilen video açıldı .' : 'Öneriler daraltı ldı .'
);
});
}
var refreshBtn = document . getElementById ( 'yaktube-related-refresh-btn' );
if ( refreshBtn ) {
refreshBtn . addEventListener ( 'click' , function () {
currentActiveVideoKey = null ;
checkAndInjectRelatedVideos ( true );
});
}
container . querySelectorAll ( '.yaktube-related-import' ). forEach ( function ( btn ) {
btn . addEventListener ( 'click' , function ( e ) {
var videoUrl = e . currentTarget . getAttribute ( 'data-url' );
var videoTitle = e . currentTarget . getAttribute ( 'data-title' );
handleImport ( videoUrl , e . currentTarget , videoTitle );
});
});
container . querySelectorAll ( '.yaktube-related-play-link' ). forEach ( function ( link ) {
link . addEventListener ( 'click' , function () {
currentActiveVideoKey = null ;
});
});
}
function checkAndInjectRelatedVideos ( forceReload ) {
var isWatchPage =
window . location . pathname . includes ( '/videos/watch/' ) || window . location . pathname . startsWith ( '/w/' );
if ( ! isWatchPage ) {
var existing = document . getElementById ( 'yaktube-hybrid-related-container' );
if ( existing ) existing . remove ();
currentActiveVideoKey = null ;
return ;
}
var titleElem =
document . querySelector ( 'h1.video-name' ) ||
document . querySelector ( '.video-info h1' ) ||
document . querySelector ( '.video-watch-heading h1' ) ||
document . querySelector ( 'h1' ) ||
document . querySelector ( '.title' );
var rawTitle = titleElem ? ( titleElem . textContent || titleElem . innerText || '' ). trim () : '' ;
if ( ! rawTitle || rawTitle . length < 3 ) {
return ;
}
var currentPath = window . location . pathname ;
var thisVideoKey = currentPath + '::' + rawTitle . toLowerCase ();
if (
! forceReload &&
thisVideoKey === currentActiveVideoKey &&
document . getElementById ( 'yaktube-hybrid-related-container' )
) {
return ;
}
if ( isFetchingRelated ) return ;
isFetchingRelated = true ;
var cleanTopic = rawTitle
. replace ( /YakTube/gi , '' )
. replace ( /[\(\[].*?[\)\]]/g , '' )
. replace ( /official video|video klip|klip|hd|4k|lyric video|audio|remix|feat\.?|ft\.?/gi , '' )
. trim ();
var words = cleanTopic . split ( /\s+/ ). filter ( function ( w ) {
return w . length > 2 ;
});
var searchQuery = words . slice ( 0 , 3 ). join ( ' ' ) || cleanTopic ;
var container = document . getElementById ( 'yaktube-hybrid-related-container' );
if ( ! container ) {
container = document . createElement ( 'div' );
container . id = 'yaktube-hybrid-related-container' ;
container . className = 'yaktube-ondemand-section' ;
container . setAttribute ( 'role' , 'region' );
container . setAttribute ( 'aria-label' , 'YakTube Akı llı Önerilen Videolar' );
}
var recVideosMount = document . querySelector ( 'my-recommended-videos, .other-videos' );
var commentsMount = document . querySelector ( '#yaktube-watch-comments-panel, my-video-comments, .video-comments' );
var videoBottomMount = document . querySelector ( '.video-bottom, .video-info' );
var watchMount = document . querySelector ( 'my-video-watch' ) || document . querySelector ( 'main' );
if ( recVideosMount ) {
if ( container . parentElement !== recVideosMount ) {
recVideosMount . prepend ( container );
}
} else if ( commentsMount ) {
if ( container . previousElementSibling !== commentsMount ) {
commentsMount . insertAdjacentElement ( 'afterend' , container );
}
} else if ( videoBottomMount ) {
if ( container . previousElementSibling !== videoBottomMount ) {
videoBottomMount . insertAdjacentElement ( 'afterend' , container );
}
} else if ( watchMount ) {
if ( container . parentElement !== watchMount ) {
watchMount . appendChild ( container );
}
} else {
isFetchingRelated = false ;
return ;
}
container . innerHTML =
'<div class="yaktube-header">' +
'<div class="yaktube-header-left">' +
'<span class="yaktube-badge">🔥 ' +
getInstanceName () +
' Akı llı Önerilenler</span>' +
'<h3 class="yaktube-title-h3" style="margin:0; font-size:1.05rem;">"' +
searchQuery +
'" için Öneriler Taranı yor...</h3>' +
'</div>' +
'</div>' +
'<div style="padding: 18px; text-align: center; color: #ff8f37;">' +
'<div class="yaktube-spinner" style="width: 30px; height: 30px; border-width: 3px;" aria-hidden="true"></div>' +
'<div style="margin-top: 10px; font-size: 13px;">YouTube ve Fediverse\'deki en alakalı videolar getiriliyor...</div>' +
'</div>' ;
announce ( searchQuery + ' için ilgili video önerileri yükleniyor.' );
fetch ( '/api-custom/youtube-search?q=' + encodeURIComponent ( searchQuery ))
. then ( function ( res ) {
return res . json ();
})
. then ( function ( data ) {
var rawResults = data . results || [];
var normalizedCurrentTitle = rawTitle . toLowerCase (). trim ();
var uniqueResults = [];
var seenIds = {};
rawResults . forEach ( function ( item ) {
if ( item && item . id && ! seenIds [ item . id ] && item . title ) {
var itemTitle = item . title . toLowerCase (). trim ();
if ( itemTitle !== normalizedCurrentTitle && ! itemTitle . includes ( normalizedCurrentTitle )) {
seenIds [ item . id ] = true ;
uniqueResults . push ( item );
}
}
});
cachedRelatedResults = uniqueResults ;
cachedRelatedQuery = searchQuery ;
currentActiveVideoKey = thisVideoKey ;
relatedShowAll = false ;
renderRelatedCards ( container , uniqueResults , searchQuery );
var initialCount = Math . min ( uniqueResults . length , 12 );
if ( uniqueResults . length === 0 ) {
announce ( searchQuery + ' için ilgili öneri bulunamadı .' );
} else {
announce (
searchQuery +
' için ' +
initialCount +
' öneri gösteriliyor' +
( uniqueResults . length > 12 ? ', toplam ' + uniqueResults . length + ' video bulundu' : '' ) +
'.'
);
}
})
. catch ( function ( err ) {
console . error ( '[YakTube] Related fetch error:' , err );
container . style . display = 'none' ;
})
. finally ( function () {
isFetchingRelated = false ;
});
}
var lastWatchUuid = null ;
function checkAndInjectWatchPageComments () {
var pathname = window . location . pathname ;
var match = pathname . match ( /\/videos\/watch\/([a-zA-Z0-9_-]+)/ ) || pathname . match ( /\/w\/([a-zA-Z0-9_-]+)/ );
if ( ! match ) {
lastWatchUuid = null ;
var existingWatchComments = document . getElementById ( 'yaktube-watch-comments-panel' );
if ( existingWatchComments ) existingWatchComments . remove ();
return ;
}
var uuid = match [ 1 ];
var existingPanel = document . getElementById ( 'yaktube-watch-comments-panel' );
if ( uuid === lastWatchUuid && existingPanel ) {
var nativeComments = document . querySelector ( 'my-video-comments, .video-comments, .comments-block, #comments' );
if ( nativeComments && existingPanel . previousElementSibling !== nativeComments ) {
nativeComments . insertAdjacentElement ( 'afterend' , existingPanel );
}
return ;
}
lastWatchUuid = uuid ;
var mountTarget =
document . querySelector ( 'my-video-comments, .video-comments, my-video-watch, .video-bottom' ) || document . body ;
if ( ! mountTarget ) return ;
fetch ( '/api-custom/video-origin/' + encodeURIComponent ( uuid ))
. then ( function ( r ) {
return r . json ();
})
. then ( function ( originData ) {
if ( ! originData || ! originData . ok || ! originData . hasYouTubeOrigin || ! originData . youtubeId ) {
var old = document . getElementById ( 'yaktube-watch-comments-panel' );
if ( old ) old . remove ();
return ;
}
var youtubeId = originData . youtubeId ;
var existing = document . getElementById ( 'yaktube-watch-comments-panel' );
if ( existing ) existing . remove ();
var panel = document . createElement ( 'div' );
panel . id = 'yaktube-watch-comments-panel' ;
panel . className = 'yaktube-comments-panel' ;
panel . setAttribute ( 'role' , 'region' );
panel . setAttribute ( 'aria-label' , 'YouTube Orijinal Yorumlar\u0131' );
panel . style . cssText =
'margin-top: 24px; margin-bottom: 24px; background: #0f172a; border-radius: 12px; padding: 20px; border: 1px solid rgba(255,255,255,0.08); box-shadow: 0 8px 24px rgba(0,0,0,0.4);' ;
panel . innerHTML =
'<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px;">' +
'<div>' +
'<h2 style="margin: 0; font-size: 16px; font-weight: 700; color: #fff; display: flex; align-items: center; gap: 8px;">' +
"\ud83d\udcac YouTube'daki Orijinal Yorumlar" +
'</h2>' +
'<div style="color: #94a3b8; font-size: 12px; margin-top: 4px;">Bu video YouTube\'dan aktar\u0131lm\u0131\u015ft\u0131r. Orijinal topluluk yorumlar\u0131n\u0131 a\u015fa\u011f\u0131dan anl\u0131k olarak y\u00fckleyebilirsiniz.</div>' +
'</div>' +
'<button id="yaktube-watch-toggle-comments-btn" style="background: #ff8f37; color: #000; border: none; padding: 8px 18px; border-radius: 8px; font-weight: 700; cursor: pointer; font-size: 13px; display: inline-flex; align-items: center; gap: 6px; box-shadow: 0 4px 12px rgba(255,143,55,0.3);" aria-label="YouTube yorumlar\u0131n\u0131 y\u00fckle ve g\u00f6ster">' +
'\ud83d\udcac Yorumlar\u0131 G\u00f6ster (20 Yorum)' +
'</button>' +
'</div>' +
'<div id="yaktube-watch-comments-list" style="display: none; margin-top: 16px; flex-direction: column; gap: 12px;"></div>' +
'<div id="yaktube-watch-more-comments-wrap" style="text-align: center; margin-top: 18px; display: none;">' +
'<button id="yaktube-watch-more-comments-btn" style="background: #1e293b; color: #ff8f37; border: 1px solid #ff8f37; padding: 10px 24px; border-radius: 8px; font-weight: 700; cursor: pointer; font-size: 13px; display: inline-flex; align-items: center; gap: 6px; transition: all 0.2s ease;" aria-label="Daha fazla YouTube yorumu y\u00fckle">' +
'\u2795 Daha Fazla Yorum G\u00f6ster (+20 Yorum)' +
'</button>' +
'</div>' ;
var nativeComments = document . querySelector ( 'my-video-comments, .video-comments, .comments-block, #comments' );
if ( nativeComments && nativeComments . parentElement ) {
nativeComments . insertAdjacentElement ( 'afterend' , panel );
} else {
var watchCol = document . querySelector ( '.video-info, .video-bottom, my-video-watch, .main-col' );
if ( watchCol ) {
watchCol . appendChild ( panel );
} else {
mountTarget . appendChild ( panel );
}
}
var watchCommentToken = null ;
var listContainer = document . getElementById ( 'yaktube-watch-comments-list' );
var toggleBtn = document . getElementById ( 'yaktube-watch-toggle-comments-btn' );
var moreWrap = document . getElementById ( 'yaktube-watch-more-comments-wrap' );
var moreBtn = document . getElementById ( 'yaktube-watch-more-comments-btn' );
function fetchWatchComments ( token ) {
if ( ! token ) {
toggleBtn . disabled = true ;
toggleBtn . textContent = '⏳ Yorumlar Y\u00fckleniyor...' ;
} else {
moreBtn . disabled = true ;
moreBtn . textContent = '⏳ Y\u00fckleniyor...' ;
}
var fUrl =
'/api-custom/youtube-comments?id=' +
encodeURIComponent ( youtubeId ) +
( token ? '&token=' + encodeURIComponent ( token ) : '' );
fetch ( fUrl )
. then ( function ( r ) {
return r . json ();
})
. then ( function ( res ) {
if ( ! token ) {
listContainer . innerHTML = '' ;
listContainer . style . display = 'flex' ;
toggleBtn . textContent = '🔄 Yorumlar\u0131 Yenile' ;
toggleBtn . disabled = false ;
} else {
moreBtn . textContent = '➕ Daha Fazla Yorum G\u00f6ster (+20 Yorum)' ;
moreBtn . disabled = false ;
}
var cList = res . comments || [];
if ( cList . length === 0 && ! token ) {
listContainer . innerHTML =
'<div style="color: #94a3b8; font-size: 13px; text-align: center; padding: 12px;">Bu videoda hen\u00fcz YouTube yorumu bulunmuyor.</div>' ;
moreWrap . style . display = 'none' ;
announce ( 'Bu videoda henüz YouTube yorumu bulunmuyor.' );
return ;
}
cList . forEach ( function ( c ) {
var card = document . createElement ( 'div' );
card . className = 'yaktube-comment-item' ;
card . setAttribute ( 'role' , 'article' );
card . setAttribute ( 'tabindex' , '0' );
card . setAttribute (
'aria-label' ,
c . author + ' isimli kullan\u0131c\u0131n\u0131n yorumu: ' + c . content
);
card . style . cssText =
'background: #1e293b; padding: 14px 16px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.05); display: flex; gap: 12px; align-items: flex-start;' ;
var avatarLetter = ( c . author || 'U' ). replace ( '@' , '' ). charAt ( 0 ). toUpperCase ();
var avatarHtml =
'<div style="width: 38px; height: 38px; border-radius: 50%; background: #334155; color: #ff8f37; font-weight: 700; display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0;">' +
avatarLetter +
'</div>' ;
var safeContent = c . content
. replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /\n/g , '<br>' );
card . innerHTML =
avatarHtml +
'<div style="flex-grow: 1; min-width: 0;">' +
'<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap;">' +
'<h3 class="yaktube-comment-author-heading" style="margin: 0; font-size: 13px; font-weight: 700; color: #f1f5f9; display: inline-block;">' +
c . author +
'</h3>' +
'<span style="color: #64748b; font-size: 11px;">' +
c . published +
'</span>' +
( c . likeCount && c . likeCount !== '0'
? '<span style="background: rgba(255,255,255,0.08); color: #94a3b8; font-size: 11px; padding: 2px 6px; border-radius: 4px; display: inline-flex; align-items: center; gap: 4px;">👍 ' +
c . likeCount +
'</span>'
: '' ) +
'</div>' +
'<div style="color: #cbd5e1; font-size: 13px; line-height: 1.5; word-break: break-word;">' +
safeContent +
'</div>' +
'</div>' ;
listContainer . appendChild ( card );
});
watchCommentToken = res . nextToken ;
if ( watchCommentToken ) {
moreWrap . style . display = 'block' ;
} else {
moreWrap . style . display = 'none' ;
}
announce ( cList . length + ' adet YouTube yorumu y\u00fcklendi.' );
})
. catch ( function ( err ) {
if ( ! token ) {
toggleBtn . disabled = false ;
toggleBtn . textContent = '⚠️ Tekrar Dene' ;
} else {
moreBtn . disabled = false ;
moreBtn . textContent = '⚠️ Tekrar Dene' ;
}
alert ( 'Yorumlar y\u00fcklenirken hata olu\u015ftu: ' + err . message );
});
}
if ( toggleBtn ) {
toggleBtn . addEventListener ( 'click' , function () {
fetchWatchComments ( null );
});
}
if ( moreBtn ) {
moreBtn . addEventListener ( 'click' , function () {
if ( watchCommentToken ) fetchWatchComments ( watchCommentToken );
});
}
})
. catch ( function () {});
}
// 6. PWA Mobile App Install Engine & Standalone Detection
var isInsideYakTubeApp =
( navigator . userAgent && navigator . userAgent . indexOf ( 'YakTubeMobileApp' ) !== - 1 ) ||
( typeof window . matchMedia === 'function' &&
window . matchMedia ( '(display-mode: standalone)' ) &&
window . matchMedia ( '(display-mode: standalone)' ). matches ) ||
( window . navigator && window . navigator . standalone === true );
var isMobileDevice = /Android|iPhone|iPad|iPod|Mobile|Tablet/i . test ( navigator . userAgent );
var deferredInstallPrompt = null ;
window . addEventListener ( 'beforeinstallprompt' , function ( e ) {
if ( isInsideYakTubeApp ) return ;
e . preventDefault ();
deferredInstallPrompt = e ;
injectPWAInstallButton ();
2026-08-29 23:02:50 +03:00
});
2026-08-31 18:48:19 +03:00
function injectPWAInstallButton () {
if ( isInsideYakTubeApp ) {
var existBtn = document . getElementById ( 'yaktube-pwa-install-btn' );
if ( existBtn ) {
existBtn . setAttribute ( 'aria-hidden' , 'true' );
existBtn . remove ();
}
return ;
}
if ( ! deferredInstallPrompt || document . getElementById ( 'yaktube-pwa-install-btn' )) return ;
var headerButtons =
document . querySelector ( 'my-header .buttons-container' ) ||
document . querySelector ( '.buttons-container' ) ||
document . querySelector ( 'my-header' );
if ( ! headerButtons ) return ;
var installBtn = document . createElement ( 'button' );
installBtn . id = 'yaktube-pwa-install-btn' ;
installBtn . className = 'yaktube-pwa-btn' ;
installBtn . setAttribute ( 'type' , 'button' );
2026-09-09 07:06:23 +03:00
installBtn . setAttribute ( 'aria-label' , getInstanceName () + ' Mobil Uygulaması nı Yükle' );
2026-08-31 18:48:19 +03:00
installBtn . style . cssText =
'background: linear-gradient(135deg, #ff8f37, #f97316); color: #000; border: none; padding: 6px 14px; border-radius: 20px; font-weight: 700; font-size: 12px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; margin-right: 8px; box-shadow: 0 4px 12px rgba(255,143,55,0.4);' ;
installBtn . innerHTML = '📲 Uygulamayı Yükle' ;
installBtn . addEventListener ( 'click' , function () {
if ( deferredInstallPrompt ) {
deferredInstallPrompt . prompt ();
deferredInstallPrompt . userChoice . then ( function ( choiceResult ) {
if ( choiceResult . outcome === 'accepted' ) {
installBtn . remove ();
2026-09-09 07:06:23 +03:00
announce ( getInstanceName () + ' uygulaması cihazı nı za yükleniyor.' );
2026-08-31 18:48:19 +03:00
}
deferredInstallPrompt = null ;
});
}
});
headerButtons . prepend ( installBtn );
}
2026-09-09 07:06:23 +03:00
function isOfficialYakTubeServer () {
var host = ( window . location . hostname || '' ). toLowerCase ();
// Sadece ve sadece resmi YakTube alan adı nda veya yerel test ortamı nda çalışsı n
// Diğer PeerTube sunucuları nda yaktube.apk dosyası olmayacağı için ASLA gösterilmez
return host === 'yaktube.yakhub.com.tr' || host === '127.0.0.1' || host === 'localhost' ;
2026-08-31 18:48:19 +03:00
}
function injectApkDownloadButton () {
2026-09-09 07:06:23 +03:00
if ( ! isOfficialYakTubeServer ()) {
var existBtnOther = document . getElementById ( 'yaktube-apk-download-btn' );
if ( existBtnOther ) existBtnOther . remove ();
return ;
}
2026-08-31 18:48:19 +03:00
// 1. If inside APK: NEVER show APK button
if ( isInsideYakTubeApp ) {
var existBtnApp = document . getElementById ( 'yaktube-apk-download-btn' );
if ( existBtnApp ) {
existBtnApp . setAttribute ( 'aria-hidden' , 'true' );
existBtnApp . remove ();
}
return ;
}
// 2. If on Desktop: NEVER show in top header (keep header clean)
if ( ! isMobileDevice ) {
var existBtnDesktop = document . getElementById ( 'yaktube-apk-download-btn' );
if ( existBtnDesktop ) {
existBtnDesktop . setAttribute ( 'aria-hidden' , 'true' );
existBtnDesktop . remove ();
}
return ;
}
// 3. If on Mobile Web: Show in header
if ( document . getElementById ( 'yaktube-apk-download-btn' )) return ;
var headerButtons =
document . querySelector ( 'my-header .buttons-container' ) ||
document . querySelector ( 'my-header .header-right' ) ||
document . querySelector ( '.buttons-container' ) ||
document . querySelector ( 'my-header' );
if ( ! headerButtons ) return ;
var apkBtn = document . createElement ( 'a' );
apkBtn . id = 'yaktube-apk-download-btn' ;
apkBtn . href = '/yaktube.apk' ;
apkBtn . className = 'yaktube-apk-btn' ;
apkBtn . setAttribute ( 'download' , 'YakTube_Mobile_v1.0.apk' );
apkBtn . setAttribute ( 'aria-label' , 'YakTube Android APK İndir (4.5 MB)' );
apkBtn . style . cssText =
'background: #1e293b; color: #ff8f37; border: 1px solid #ff8f37; padding: 6px 12px; border-radius: 20px; font-weight: 700; font-size: 11px; text-decoration: none; display: inline-flex; align-items: center; gap: 4px; margin-right: 6px; transition: all 0.2s; box-shadow: 0 4px 12px rgba(0,0,0,0.3);' ;
apkBtn . innerHTML = '🤖 APK İndir (4.5 MB)' ;
headerButtons . prepend ( apkBtn );
}
function injectSidebarApkLink () {
2026-09-09 07:06:23 +03:00
if ( ! isOfficialYakTubeServer ()) {
var existLinkOther = document . getElementById ( 'yaktube-sidebar-apk-link' );
if ( existLinkOther ) existLinkOther . remove ();
return ;
}
2026-08-31 18:48:19 +03:00
// If inside APK: NEVER show in sidebar
if ( isInsideYakTubeApp ) {
var existLink = document . getElementById ( 'yaktube-sidebar-apk-link' );
if ( existLink ) {
existLink . setAttribute ( 'aria-hidden' , 'true' );
existLink . remove ();
}
return ;
}
if ( document . getElementById ( 'yaktube-sidebar-apk-link' )) return ;
var menuContainer = document . querySelector ( 'my-menu, .menu, .main-menu, .nav-list' );
if ( ! menuContainer ) return ;
var sidebarLink = document . createElement ( 'a' );
sidebarLink . id = 'yaktube-sidebar-apk-link' ;
sidebarLink . href = '/yaktube.apk' ;
sidebarLink . className = 'menu-link item-link' ;
sidebarLink . setAttribute ( 'download' , 'YakTube_Mobile_v1.0.apk' );
sidebarLink . setAttribute ( 'aria-label' , 'YakTube Android Uygulaması nı İndir (4.5 MB)' );
sidebarLink . style . cssText =
'display: flex; align-items: center; gap: 10px; padding: 10px 16px; margin: 8px 12px; background: rgba(255, 143, 55, 0.1); color: #ff8f37; border: 1px solid rgba(255, 143, 55, 0.3); border-radius: 8px; font-weight: 700; font-size: 13px; text-decoration: none;' ;
sidebarLink . innerHTML = '<span>🤖</span> <span>YakTube Android APK (4.5 MB)</span>' ;
menuContainer . appendChild ( sidebarLink );
}
2026-09-19 05:14:11 +03:00
// 9. Background Play (Screen-Off / Minimize) & MediaSession Lock-Screen Controls
2026-08-31 18:48:19 +03:00
var videoState = {
hasPlayed : false ,
2026-08-31 20:20:55 +03:00
explicitlyPausedByClick : false ,
lastPlayTime : 0
2026-08-31 18:48:19 +03:00
};
2026-08-29 23:02:50 +03:00
2026-08-31 18:48:19 +03:00
function attachVideoListeners ( video ) {
if ( ! video || video . hasAttribute ( 'data-yaktube-bgplay-hooked' )) return ;
video . setAttribute ( 'data-yaktube-bgplay-hooked' , 'true' );
2026-08-29 21:47:30 +03:00
2026-09-19 05:14:11 +03:00
try {
initTouchSeekGesture ( video );
} catch ( e ) {}
2026-08-31 18:48:19 +03:00
video . addEventListener ( 'play' , function () {
videoState . hasPlayed = true ;
2026-08-31 20:20:55 +03:00
videoState . explicitlyPausedByClick = false ;
videoState . lastPlayTime = Date . now ();
2026-08-31 18:48:19 +03:00
updateMediaSessionMetadata ( video );
});
2026-08-29 23:01:08 +03:00
2026-08-31 18:48:19 +03:00
video . addEventListener ( 'ended' , function () {
videoState . hasPlayed = false ;
2026-08-31 20:20:55 +03:00
videoState . explicitlyPausedByClick = true ;
2026-08-31 18:48:19 +03:00
});
video . addEventListener ( 'timeupdate' , function () {
updateMediaSessionPosition ( video );
});
}
2026-08-31 20:20:55 +03:00
// Hook manual play/pause buttons
document . addEventListener (
'click' ,
function ( e ) {
var target = e . target ;
if ( ! target ) return ;
var btn = target . closest (
'.vjs-play-control, button[title*="Duraklat"], button[aria-label*="Duraklat"], #yaktube-bar-play-btn'
);
if ( btn ) {
var video = getVideoElement ();
if ( video && ! video . paused ) {
videoState . explicitlyPausedByClick = true ;
}
}
},
true
);
2026-08-31 18:48:19 +03:00
2026-08-31 20:20:55 +03:00
function handleVisibilityChange () {
2026-08-31 18:48:19 +03:00
var video = getVideoElement ();
if ( ! video ) return ;
2026-08-31 20:20:55 +03:00
// When screen turns off / tab is hidden:
if ( document . hidden || document . webkitHidden ) {
if ( videoState . hasPlayed && ! videoState . explicitlyPausedByClick ) {
// Unconditionally keep audio stream active
setTimeout ( function () {
if ( video . paused && ! videoState . explicitlyPausedByClick ) {
video . play (). catch ( function () {});
}
}, 100 );
setTimeout ( function () {
if ( video . paused && ! videoState . explicitlyPausedByClick ) {
video . play (). catch ( function () {});
}
}, 300 );
2026-08-31 18:48:19 +03:00
}
2026-08-31 20:20:55 +03:00
}
2026-08-29 23:02:50 +03:00
}
2026-08-29 23:01:08 +03:00
2026-08-31 18:48:19 +03:00
document . addEventListener ( 'visibilitychange' , handleVisibilityChange , false );
document . addEventListener ( 'webkitvisibilitychange' , handleVisibilityChange , false );
2026-08-31 20:20:55 +03:00
window . addEventListener ( 'pagehide' , handleVisibilityChange , false );
2026-08-29 21:47:30 +03:00
2026-08-31 18:48:19 +03:00
// MediaSession API Integration (Lock Screen, Bluetooth, Smart Watches)
function updateMediaSessionMetadata ( video ) {
if ( ! ( 'mediaSession' in navigator )) return ;
2026-08-29 23:02:50 +03:00
2026-08-31 18:48:19 +03:00
var titleEl = document . querySelector ( 'h1.video-title, .video-name, my-video-watch h1, .title' );
var title = titleEl ? titleEl . textContent . trim () : 'YakTube Video' ;
var authorEl = document . querySelector ( '.actor-name, .channel-name, .video-channel-name, a.actor-link' );
var artist = authorEl ? authorEl . textContent . trim () : 'YakTube' ;
var artwork = [];
var thumbEl = document . querySelector ( 'meta[property="og:image"]' );
if ( thumbEl && thumbEl . content ) {
artwork . push ({ src : thumbEl . content , sizes : '512x512' , type : 'image/jpeg' });
}
2026-08-29 23:09:14 +03:00
try {
2026-08-31 18:48:19 +03:00
navigator . mediaSession . metadata = new MediaMetadata ({
title : title ,
artist : artist ,
album : 'YakTube' ,
artwork : artwork
2026-08-29 12:42:36 +03:00
});
2026-08-31 18:48:19 +03:00
navigator . mediaSession . setActionHandler ( 'play' , function () {
video . play ();
announce ( 'Video oynatı lı yor.' );
2026-08-29 12:42:36 +03:00
});
2026-08-31 18:48:19 +03:00
navigator . mediaSession . setActionHandler ( 'pause' , function () {
videoState . pausedByUser = true ;
video . pause ();
announce ( 'Video duraklatı ldı .' );
});
navigator . mediaSession . setActionHandler ( 'seekbackward' , function ( details ) {
var skipTime = details . seekOffset || 10 ;
video . currentTime = Math . max ( 0 , video . currentTime - skipTime );
announce ( skipTime + ' saniye geri sarı ldı .' );
});
navigator . mediaSession . setActionHandler ( 'seekforward' , function ( details ) {
var skipTime = details . seekOffset || 10 ;
video . currentTime = Math . min ( video . duration || 99999 , video . currentTime + skipTime );
announce ( skipTime + ' saniye ileri sarı ldı .' );
});
navigator . mediaSession . setActionHandler ( 'stop' , function () {
videoState . pausedByUser = true ;
video . pause ();
});
} catch ( e ) {}
2026-08-29 12:42:36 +03:00
}
2026-08-31 18:48:19 +03:00
function updateMediaSessionPosition ( video ) {
if ( ! ( 'mediaSession' in navigator ) || ! navigator . mediaSession . setPositionState ) return ;
if ( isNaN ( video . duration ) || isNaN ( video . currentTime ) || video . duration <= 0 ) return ;
try {
navigator . mediaSession . setPositionState ({
duration : video . duration ,
playbackRate : video . playbackRate || 1 ,
position : Math . min ( video . currentTime , video . duration )
});
} catch ( e ) {}
2026-08-29 12:42:36 +03:00
}
2026-09-19 05:14:11 +03:00
// 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%)
2026-08-31 18:48:19 +03:00
setInterval ( function () {
2026-09-19 05:14:11 +03:00
scheduleUnifiedCycle ( false , 0 );
}, 3500 );
// Initial trigger
runUnifiedCycle ( true );
2026-08-31 18:48:19 +03:00
})();
} catch ( err ) {
console . error ( '[YakTube] Safe catch error:' , err );
}