From 5c3c22999fbfcb6a043ddb03994c2c34ded9da28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enes=20Yak=C4=B1=C5=9Ft=C4=B1r?= Date: Wed, 23 Sep 2026 06:30:38 +0300 Subject: [PATCH] feat(a11y): release v1.5.4 - video player W3C WAI-ARIA enhancements, role=application removal, top skip link and full localization --- client.js | 431 ++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- style.css | 127 +++++++++++++++ 3 files changed, 553 insertions(+), 7 deletions(-) diff --git a/client.js b/client.js index ab4b967..4556fe6 100644 --- a/client.js +++ b/client.js @@ -55,6 +55,24 @@ try { } catch (e) {} console.log('[YakTube] YakNet SSO, Custom Branding, A11y & On-Demand Engine Active'); + // Global Suppression of role="application" on Video Elements (W3C WAI-ARIA Screen Reader Compliance) + try { + if ( + typeof HTMLVideoElement !== 'undefined' && + HTMLVideoElement.prototype && + !HTMLVideoElement.prototype._yaktubeA11yHooked + ) { + HTMLVideoElement.prototype._yaktubeA11yHooked = true; + var origVideoSetAttribute = HTMLVideoElement.prototype.setAttribute; + HTMLVideoElement.prototype.setAttribute = function (name, val) { + if (String(name).toLowerCase() === 'role' && String(val).toLowerCase() === 'application') { + return; // 100% suppress role="application" on video tags + } + return origVideoSetAttribute.apply(this, arguments); + }; + } + } catch (e) {} + // Ensure Fediverse Global Search (SepiaSearch) is default in PeertubeServerConfig try { if ( @@ -671,6 +689,7 @@ try { // 3. Intelligent Branding & A11y DOM Auto-Fixer function autoFixDOM() { + enhanceVideoPlayerAccessibility(); injectAccessiblePlayerBar(); // Check if on 403 / unauthorized page and provide admin login button @@ -997,7 +1016,9 @@ try { '' + '' + '' + + '' + '' + + '' + '' + '' + '' + @@ -1667,6 +1688,385 @@ try { return false; } + // ========================================================================== + // YAKTUBE ACCESSIBLE VIDEO PLAYER & WATCH PAGE ENGINE (W3C WAI-ARIA) + // ========================================================================== + function focusVideoPlayer() { + var playerWrapper = document.getElementById('videojs-wrapper') || document.querySelector('.video-js'); + if (playerWrapper) { + playerWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' }); + playerWrapper.focus(); + var titleEl = document.querySelector( + 'h1.video-info-name, .video-info-name h1, my-video-watch h1, .video-title, .title' + ); + var videoTitle = titleEl ? titleEl.textContent.trim() : 'Video'; + var video = getVideoElement(); + var status = video ? (video.paused ? 'Duraklatıldı' : 'Oynatılıyor') : ''; + announce( + 'Video oynatıcıya odaklanıldı: ' + + videoTitle + + (status ? ' (' + status + ')' : '') + + '. Oynatmak için Boşluk veya K, sarmak için J veya L tuşlarını kullanabilirsiniz.' + ); + } + } + window.focusVideoPlayer = focusVideoPlayer; + + function sanitizeWatchPageLabels(videoTitle) { + // 1. Next Video button in Video.js control bar + var nextBtns = document.querySelectorAll( + '.vjs-next-video-control, .vjs-next-control, button[title*="Next video"], button[aria-label*="Next video"]' + ); + nextBtns.forEach(function (btn) { + btn.setAttribute('aria-label', 'Sonraki Video (Shift+N)'); + btn.setAttribute('title', 'Sonraki Video (Shift+N)'); + var ctrlText = btn.querySelector('.vjs-control-text'); + if (ctrlText && ctrlText.textContent.includes('Next video')) { + ctrlText.textContent = 'Sonraki Video (Shift+N)'; + } + }); + + // 2. Share modal button + var shareBtns = document.querySelectorAll( + 'button[aria-label*="share this video"], button[title*="share this video"], .action-button[aria-label*="share this video"]' + ); + shareBtns.forEach(function (btn) { + btn.setAttribute('aria-label', 'Videoyu Paylaş'); + btn.setAttribute('title', 'Videoyu Paylaş'); + }); + + // 3. Download modal button + var downloadBtns = document.querySelectorAll( + 'button[aria-label*="download this video"], button[title*="download this video"], .action-button[aria-label*="download this video"]' + ); + downloadBtns.forEach(function (btn) { + btn.setAttribute('aria-label', 'Videoyu İndir'); + btn.setAttribute('title', 'Videoyu İndir'); + }); + + // 4. Subscription dropdown / button + var subDropdowns = document.querySelectorAll( + 'button[aria-label*="subscription"], .action-dropdown[aria-label*="subscription"], my-subscribe-button button' + ); + subDropdowns.forEach(function (btn) { + var cur = (btn.getAttribute('aria-label') || '').toLowerCase(); + if (cur.includes('subscription') || cur.includes('dropdown')) { + btn.setAttribute('aria-label', 'Abonelik Seçenekleri'); + btn.setAttribute('title', 'Abonelik Seçenekleri'); + } + }); + + // 5. Account management link + var accountLinks = document.querySelectorAll( + 'a[aria-label*="manage your account"], a[title*="manage your account"]' + ); + accountLinks.forEach(function (a) { + a.setAttribute('aria-label', 'Hesap Yönetim Sayfası'); + a.setAttribute('title', 'Hesap Yönetim Sayfası'); + }); + + // 6. Date Toggle Button (Clean up "Bu tarih birimini ... olarak değistir") + var dateToggles = document.querySelectorAll('my-date-toggle, .date-toggle, [role="button"].date-toggle'); + dateToggles.forEach(function (el) { + var rawText = (el.textContent || '').trim(); + var cleanDate = rawText.replace(/^[•\s]+|[•\s]+$/g, '').trim(); + if (cleanDate) { + var dateLabel = 'Yayınlanma Tarihi: ' + cleanDate + ' (Tarih biçimini değiştirmek için tıklayın)'; + if (el.getAttribute('aria-label') !== dateLabel) { + el.setAttribute('aria-label', dateLabel); + } + if (el.getAttribute('title') !== dateLabel) { + el.setAttribute('title', dateLabel); + } + } + }); + + // 7. Video Rate Buttons (Like & Dislike distinct separation) + var rateContainer = document.querySelector('my-video-rate, .video-actions-rates'); + if (rateContainer) { + var buttons = rateContainer.querySelectorAll('button'); + buttons.forEach(function (btn) { + var label = ( + btn.getAttribute('aria-label') || + btn.getAttribute('title') || + btn.textContent || + '' + ).toLowerCase(); + var icon = btn.querySelector('my-global-icon, i, svg'); + var iconName = icon ? icon.className || icon.getAttribute('name') || icon.getAttribute('iconName') || '' : ''; + + var isDislike = + label.includes('beğenme') || + label.includes('dislike') || + iconName.includes('thumbs-down') || + iconName.includes('thumb-down'); + var isLike = + !isDislike && + (label.includes('beğen') || + label.includes('like') || + iconName.includes('thumbs-up') || + iconName.includes('thumb-up')); + + var isPressed = + btn.classList.contains('active') || + btn.classList.contains('liked') || + btn.getAttribute('aria-pressed') === 'true'; + + if (isDislike) { + btn.setAttribute('aria-label', isPressed ? 'Videoyu Beğenme (Seçildi)' : 'Videoyu Beğenme'); + btn.setAttribute('title', 'Videoyu Beğenme'); + } else if (isLike) { + btn.setAttribute('aria-label', isPressed ? 'Videoyu Beğen (Seçildi)' : 'Videoyu Beğen'); + btn.setAttribute('title', 'Videoyu Beğen'); + } + }); + } + + // 8. Channel Avatar Link (Turn vague single letter "y" into descriptive channel link) + var channelSection = document.querySelector('.video-info-channel, my-account-on-channel-avatar'); + if (channelSection) { + var channelNameEl = document.querySelector('.video-info-channel-left-links a, a.single-link, .channel-name'); + var channelName = channelNameEl ? channelNameEl.textContent.trim() : ''; + var avatarAnchor = channelSection.querySelector('my-account-on-channel-avatar a, .actor-avatar a, .avatar a'); + if (avatarAnchor) { + var cLabel = channelName ? channelName + ' Kanalı ve Profil Resmi' : 'Kanal Profil Resmi'; + avatarAnchor.setAttribute('aria-label', cLabel); + avatarAnchor.setAttribute('title', cLabel); + var avatarImg = avatarAnchor.querySelector('img'); + if (avatarImg) { + avatarImg.setAttribute('alt', cLabel); + } + } + } + + // 9. Actions Dropdown ("Eylem" / "More" button) + var actionsDropdownBtn = document.querySelector( + 'my-video-actions-dropdown button, .action-dropdown button, my-action-buttons my-video-actions-dropdown' + ); + if (actionsDropdownBtn) { + actionsDropdownBtn.setAttribute('aria-label', 'Diğer Video Seçenekleri ve İşlemler'); + actionsDropdownBtn.setAttribute('title', 'Diğer Video Seçenekleri ve İşlemler'); + } + } + + function enhanceVideoPlayerAccessibility() { + var isWatchPage = window.location.pathname.includes('/videos/watch/'); + var skipLink = document.getElementById('yaktube-skip-to-player'); + + if (!isWatchPage) { + if (skipLink) skipLink.remove(); + return; + } + + // 1. Remove harmful role="application" from video and containers + var videos = document.querySelectorAll('video, .vjs-tech'); + videos.forEach(function (v) { + if (v.getAttribute('role') === 'application' || v.hasAttribute('role')) { + v.removeAttribute('role'); + } + }); + + var appElements = document.querySelectorAll( + '#video-wrapper [role="application"], #videojs-wrapper [role="application"], .video-js [role="application"]' + ); + appElements.forEach(function (el) { + el.removeAttribute('role'); + }); + + // 2. Fetch current video title for accessible announcements + var titleEl = document.querySelector( + 'h1.video-info-name, .video-info-name h1, my-video-watch h1, .video-title, .title' + ); + var videoTitle = titleEl ? titleEl.textContent.trim() : 'Video'; + + // 3. Setup Top-Level Skip Link: "🎬 Video Oynatıcıya Atla (Alt+P)" + if (!skipLink) { + skipLink = document.createElement('a'); + skipLink.id = 'yaktube-skip-to-player'; + skipLink.className = 'yaktube-skip-link'; + skipLink.href = '#videojs-wrapper'; + skipLink.textContent = '🎬 Video Oynatıcıya Atla (Alt+P)'; + skipLink.setAttribute('aria-label', 'Video Oynatıcıya Atla (Alt+P)'); + skipLink.addEventListener('click', function (e) { + e.preventDefault(); + focusVideoPlayer(); + }); + document.body.insertBefore(skipLink, document.body.firstChild); + } + + // 4. Configure Player Container (#videojs-wrapper or .video-js) + var playerWrapper = document.getElementById('videojs-wrapper') || document.querySelector('.video-js'); + if (playerWrapper) { + if (!playerWrapper.hasAttribute('data-yaktube-a11y-enhanced')) { + playerWrapper.setAttribute('data-yaktube-a11y-enhanced', 'true'); + playerWrapper.setAttribute('role', 'region'); + playerWrapper.setAttribute('aria-roledescription', 'Video Oynatıcı'); + playerWrapper.setAttribute('tabindex', '0'); + + // Focus event: announce player status + playerWrapper.addEventListener('focus', function () { + var v = getVideoElement(); + var status = v ? (v.paused ? 'Duraklatıldı' : 'Oynatılıyor') : ''; + var curTitleEl = document.querySelector( + 'h1.video-info-name, .video-info-name h1, my-video-watch h1, .video-title, .title' + ); + var curTitle = curTitleEl ? curTitleEl.textContent.trim() : videoTitle; + announce( + 'Video oynatıcı seçildi: ' + + curTitle + + (status ? ' (' + status + ')' : '') + + '. Oynatmak için Boşluk veya K, sarmak için J veya L, ses için ok tuşlarını kullanabilirsiniz.' + ); + }); + + // Keydown handler when player container is focused + playerWrapper.addEventListener('keydown', function (e) { + var video = getVideoElement(); + if (!video) return; + + // Space or K: Toggle Play/Pause + if (e.key === ' ' || e.key === 'k' || e.key === 'K') { + e.preventDefault(); + e.stopPropagation(); + if (video.paused) { + video.play(); + announce('Video oynatılıyor.'); + showQuickToast('▶️ Oynatılıyor'); + } else { + video.pause(); + announce('Video duraklatıldı.'); + showQuickToast('⏸️ Duraklatıldı'); + } + return; + } + + // Left Arrow or J: Rewind 10s + if (e.key === 'ArrowLeft' || e.key === 'j' || e.key === 'J') { + e.preventDefault(); + e.stopPropagation(); + video.currentTime = Math.max(0, video.currentTime - 10); + announce('10 saniye geri sarıldı. Konum: ' + formatTime(video.currentTime)); + showQuickToast('⏪ -10sn'); + return; + } + + // Right Arrow or L: Forward 10s + if (e.key === 'ArrowRight' || e.key === 'l' || e.key === 'L') { + e.preventDefault(); + e.stopPropagation(); + video.currentTime = Math.min(video.duration || 999999, video.currentTime + 10); + announce('10 saniye ileri sarıldı. Konum: ' + formatTime(video.currentTime)); + showQuickToast('⏩ +10sn'); + return; + } + + // Up Arrow: Volume +5% + if (e.key === 'ArrowUp') { + e.preventDefault(); + e.stopPropagation(); + video.volume = Math.min(1, Math.round((video.volume + 0.05) * 100) / 100); + var pctUp = Math.round(video.volume * 100); + announce('Ses seviyesi yüzde ' + pctUp); + showQuickToast('🔊 Ses: %' + pctUp); + return; + } + + // Down Arrow: Volume -5% + if (e.key === 'ArrowDown') { + e.preventDefault(); + e.stopPropagation(); + video.volume = Math.max(0, Math.round((video.volume - 0.05) * 100) / 100); + var pctDown = Math.round(video.volume * 100); + announce('Ses seviyesi yüzde ' + pctDown); + showQuickToast('🔉 Ses: %' + pctDown); + return; + } + + // M: Mute toggle + if (e.key === 'm' || e.key === 'M') { + e.preventDefault(); + e.stopPropagation(); + video.muted = !video.muted; + announce(video.muted ? 'Ses kapatıldı (Sessiz).' : 'Ses açıldı.'); + showQuickToast(video.muted ? '🔇 Sessiz' : '🔊 Ses Açık'); + return; + } + + // F: Fullscreen toggle + if (e.key === 'f' || e.key === 'F') { + e.preventDefault(); + e.stopPropagation(); + var pTarget = document.querySelector('#video-wrapper') || playerWrapper; + if (!document.fullscreenElement) { + if (pTarget.requestFullscreen) pTarget.requestFullscreen(); + announce('Tam ekran moduna geçildi.'); + showQuickToast('⛶ Tam Ekran'); + } else { + if (document.exitFullscreen) document.exitFullscreen(); + announce('Tam ekrandan çıkıldı.'); + showQuickToast('⛶ Normal Ekran'); + } + return; + } + + // T: Theater mode toggle + if (e.key === 't' || e.key === 'T') { + e.preventDefault(); + e.stopPropagation(); + 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ı.'); + showQuickToast(isTheater ? '🎭 Sinema Modu' : '🎬 Normal Mod'); + return; + } + + // 0-9: Seek percent + if (/^[0-9]$/.test(e.key) && video.duration) { + e.preventDefault(); + e.stopPropagation(); + var pct = parseInt(e.key, 10) * 10; + video.currentTime = (video.duration * pct) / 100; + announce('Yüzde ' + pct + ' konumuna sarıldı. Konum: ' + formatTime(video.currentTime)); + showQuickToast('📍 %' + pct); + return; + } + + // Shift+N: Next Video + if (e.shiftKey && (e.key === 'n' || e.key === 'N')) { + e.preventDefault(); + e.stopPropagation(); + var nextBtn = document.querySelector('.vjs-next-video-control, .vjs-next-control'); + if (nextBtn) { + nextBtn.click(); + announce('Sonraki videoya geçiliyor...'); + } + return; + } + + // Alt+H or ?: Shortcuts modal + if ((e.altKey && (e.key === 'h' || e.key === 'H')) || e.key === '?') { + e.preventDefault(); + e.stopPropagation(); + toggleShortcutsModal(); + return; + } + }); + } + + // Dynamic ARIA label updating with current title + var curAriaLabel = + 'Video Oynatıcı: ' + + videoTitle + + '. Oynatmak için Boşluk veya K, sarmak için J veya L, ses seviyesi için Yukarı veya Aşağı ok tuşları, kısayol listesi için Alt+H tuşlarını kullanabilirsiniz.'; + if (playerWrapper.getAttribute('aria-label') !== curAriaLabel) { + playerWrapper.setAttribute('aria-label', curAriaLabel); + } + } + + // 5. Sanitize Unlocalized & Confusing Watch Page Controls + sanitizeWatchPageLabels(videoTitle); + } + // 3.1. Injects Accessible Player Action Bar under the Watch Video function injectAccessiblePlayerBar() { if (!window.location.pathname.includes('/videos/watch/')) return; @@ -1699,13 +2099,22 @@ try { '' + ''; - 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); + var playerWrap = document.querySelector( + '#video-wrapper, #videojs-wrapper, .player-container, .video-player-container, .player-wrapper, my-video-watch .player' + ); + if (playerWrap && playerWrap.parentNode) { + if (playerWrap.nextSibling) { + playerWrap.parentNode.insertBefore(bar, playerWrap.nextSibling); + } else { + playerWrap.parentNode.appendChild(bar); + } } else { - targetContainer.prepend(bar); + var vBottom = document.querySelector('.video-bottom, .video-info'); + if (vBottom && vBottom.parentNode) { + vBottom.parentNode.insertBefore(bar, vBottom); + } else { + targetContainer.prepend(bar); + } } // Attach button actions @@ -1801,6 +2210,16 @@ try { return; } + // Jump to Video Player: Alt+P or Shift+P + if ( + ((e.altKey && (e.key === 'p' || e.key === 'P')) || (e.shiftKey && (e.key === 'p' || e.key === 'P'))) && + !isInputFocused() + ) { + e.preventDefault(); + focusVideoPlayer(); + return; + } + // Admin Login: Alt+A if (e.altKey && (e.key === 'a' || e.key === 'A') && !isInputFocused()) { e.preventDefault(); diff --git a/package.json b/package.json index 13401a9..44be317 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "peertube-plugin-yaktube-ondemand", - "version": "1.5.3", + "version": "1.5.4", "description": "Accessible On-Demand YouTube Import, Live Streams, Smart Recommendations, Sleep Timer, Touch Gestures, Voice Assistant & A11y Engine for PeerTube", "main": "main.js", "library": "./main.js", diff --git a/style.css b/style.css index b04fddb..0226651 100644 --- a/style.css +++ b/style.css @@ -1354,3 +1354,130 @@ body:has(#yaktube-ondemand-container) my-error-page { box-shadow: 0 6px 20px rgba(255, 143, 55, 0.4) !important; outline: none !important; } + +/* ========================================================================== + YAKTUBE ACCESSIBLE PLAYER & WATCH PAGE STYLES (W3C WAI-ARIA) + ========================================================================== */ + +/* 1. Accessible Skip Link (Top of Page) */ +.yaktube-skip-link { + position: fixed !important; + top: -120px !important; + left: 16px !important; + background: linear-gradient(135deg, #ff8f37 0%, #ff5e3a 100%) !important; + color: #ffffff !important; + padding: 12px 22px !important; + border-radius: 12px !important; + font-weight: 800 !important; + font-size: 14px !important; + text-decoration: none !important; + box-shadow: 0 8px 30px rgba(255, 143, 55, 0.5) !important; + z-index: 999999 !important; + transition: top 0.2s cubic-bezier(0.16, 1, 0.3, 1) !important; + display: inline-flex !important; + align-items: center !important; + gap: 8px !important; +} + +.yaktube-skip-link:focus, +.yaktube-skip-link:focus-visible { + top: 16px !important; + outline: 3px solid #ffffff !important; + outline-offset: 3px !important; +} + +/* 2. Accessible Video Player Container Focus States */ +#videojs-wrapper:focus, +#videojs-wrapper:focus-visible, +.video-js:focus-visible { + outline: 3px solid #ff8f37 !important; + outline-offset: 4px !important; + box-shadow: 0 0 25px rgba(255, 143, 55, 0.65) !important; + border-radius: 8px !important; +} + +/* 3. Accessible Player Action Toolbar */ +.yaktube-player-action-bar { + display: flex !important; + align-items: center !important; + flex-wrap: wrap !important; + gap: 8px !important; + margin: 12px 0 18px 0 !important; + padding: 10px 16px !important; + background: rgba(24, 24, 32, 0.95) !important; + border: 1px solid rgba(255, 143, 55, 0.3) !important; + border-radius: 12px !important; + backdrop-filter: blur(10px) !important; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4) !important; + width: 100% !important; + box-sizing: border-box !important; +} + +.yaktube-player-bar-label { + font-size: 13px !important; + font-weight: 700 !important; + color: #ff8f37 !important; + margin-right: 6px !important; + user-select: none !important; +} + +.yaktube-player-action-btn { + background: rgba(255, 255, 255, 0.08) !important; + color: #eeeeee !important; + border: 1px solid rgba(255, 255, 255, 0.15) !important; + padding: 7px 14px !important; + border-radius: 8px !important; + font-size: 12.5px !important; + font-weight: 600 !important; + cursor: pointer !important; + transition: all 0.2s ease !important; + display: inline-flex !important; + align-items: center !important; + gap: 5px !important; +} + +.yaktube-player-action-btn:hover, +.yaktube-player-action-btn:focus-visible { + background: rgba(255, 143, 55, 0.25) !important; + border-color: #ff8f37 !important; + color: #ffffff !important; + transform: translateY(-1px) !important; + outline: 2px solid #ff8f37 !important; + outline-offset: 2px !important; +} + +/* 4. Rate Buttons Distinct Separation (Like & Dislike) */ +my-video-rate .video-actions, +my-video-rate .video-actions-rates, +my-video-rate { + display: inline-flex !important; + align-items: center !important; + gap: 8px !important; +} + +my-video-rate button.action-button { + margin-right: 6px !important; + border-radius: 8px !important; + transition: all 0.2s ease !important; +} + +my-video-rate button.action-button:focus-visible { + outline: 2px solid #ff8f37 !important; + outline-offset: 2px !important; +} + +/* 5. Date Toggle Button Clarity */ +my-date-toggle .date-toggle { + cursor: pointer !important; + border-radius: 6px !important; + padding: 2px 6px !important; + transition: background 0.2s ease !important; +} + +my-date-toggle .date-toggle:hover, +my-date-toggle .date-toggle:focus-visible { + background: rgba(255, 143, 55, 0.15) !important; + color: #ff8f37 !important; + outline: 2px solid #ff8f37 !important; + outline-offset: 1px !important; +}
Kısayol (Evrensel / Tek Tuş)İşlev
Alt + P veya Shift + P🎬 Video Oynatıcıya Odaklan (Atla)
Alt + K veya SpaceOynat / Duraklat
Shift + NSonraki Videoya Geç
Alt + J veya J10 saniye geri sar
Alt + L veya L10 saniye ileri sar
Alt + Yukarı / AşağıSesi %5 artır / azalt