diff --git a/client.js b/client.js index 8340ffd..690b0d5 100644 --- a/client.js +++ b/client.js @@ -52,6 +52,13 @@ try { } catch (e) {} console.log('[YakTube] YakNet SSO, Custom Branding, A11y & On-Demand Engine Active'); + // Ensure Fediverse Global Search (SepiaSearch) is default in PeertubeServerConfig + try { + if (window.PeertubeServerConfig && window.PeertubeServerConfig.search && window.PeertubeServerConfig.search.searchIndex) { + window.PeertubeServerConfig.search.searchIndex.isDefaultSearch = true; + } + } catch (e) {} + // Dynamic Instance Name Helper function getInstanceName() { if ( @@ -1317,8 +1324,114 @@ try { // 5. On-Demand Search & Import with Duplicate Awareness var lastQuery = null; + var lastSearchTarget = null; var isSearching = false; + function escapeHtml(str) { + if (!str) return ''; + return String(str) + .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 = + '
' + + '
' + + '
' + + '🌐 Fediverse Ağı' + + '' + + (isFediverse ? 'SepiaSearch Küresel Fediverse Arama Aktif' : escapeHtml(getInstanceName()) + ' Yerel Arama Aktif') + + '' + + '
' + + '

🌐 Fediverse & Küresel Ağ Arama Sonuçları

' + + '

Merkeziyetsiz federe ağdaki (SepiaSearch ve bağımsız PeerTube sunucuları) video sonuçları aşağıda listelenmektedir.

' + + '
' + + '
' + + '' + + '' + + '
' + + '
'; + + 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'); + } + }); + } + } + function showModal(title, desc) { var overlay = document.getElementById('yaktube-import-modal'); if (!overlay) { @@ -1710,13 +1823,35 @@ try { if (!window.location.pathname.includes('/search') || !searchParam || searchParam.trim().length < 2) { var existing = document.getElementById('yaktube-ondemand-container'); if (existing) existing.remove(); + var existingBridge = document.getElementById('yaktube-fediverse-bridge'); + if (existingBridge) existingBridge.remove(); lastQuery = null; + lastSearchTarget = null; return; } var query = searchParam.trim(); - if (query === lastQuery) return; + + // 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; + } lastQuery = query; + lastSearchTarget = currentSearchTarget; var container = document.getElementById('yaktube-ondemand-container'); if (!container) { @@ -1724,7 +1859,7 @@ try { container.id = 'yaktube-ondemand-container'; container.className = 'yaktube-ondemand-section'; container.setAttribute('role', 'region'); - container.setAttribute('aria-label', 'Canl\u0131 YouTube Arama Sonu\u00e7lar\u0131'); + container.setAttribute('aria-label', 'Canlı YouTube ve YakTube Arama Sonuçları'); var mainContent = document.querySelector('my-search, my-search-results, router-outlet, main, .main-col') || document.body; @@ -1738,18 +1873,18 @@ try { container.innerHTML = '
' + '
' + - '\u26a1 Canl\u0131 YouTube & Web Arama' + + '⚡ Canlı YouTube & Web Arama' + '

"' + - query + - '" i\u00e7in YouTube Sonu\u00e7lar\u0131 Taran\u0131yor...

' + + escapeHtml(query) + + '" için Sonuçlar Taranıyor...' + '
' + - '' + '
' + '
' + '' + - '
YouTube\'daki en pop\u00fcler e\u015fle\u015fmeler taran\u0131yor...
' + + '
YouTube ve ' + escapeHtml(getInstanceName()) + ' yerel kütüphanesindeki eşleşmeler taranıyor...
' + '
'; var shortcutsBtn = document.getElementById('yaktube-open-shortcuts'); @@ -1757,167 +1892,262 @@ try { shortcutsBtn.addEventListener('click', toggleShortcutsModal); } - announce(query + ' i\u00e7in canl\u0131 YouTube aramas\u0131 ba\u015flat\u0131ld\u0131.'); + ensureFediverseBridge(query, currentSearchTarget); + announce(query + ' için arama başlatıldı.'); isSearching = true; - fetch('/api-custom/youtube-search?q=' + encodeURIComponent(query)) - .then(function (res) { - return res.json(); - }) - .then(function (data) { - var rawResults = data.results || []; - var uniqueResults = []; + Promise.all([ + fetch('/api-custom/youtube-search?q=' + encodeURIComponent(query)) + .then(function (res) { + return res.json(); + }) + .catch(function (err) { + console.warn('[YakTube] YouTube search fetch error:', err); + return { results: [] }; + }), + fetch('/api/v1/search/videos?search=' + encodeURIComponent(query) + '&isLocal=true&count=12') + .then(function (res) { + return res.json(); + }) + .catch(function (err) { + 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 = []; var seenIds = {}; - rawResults.forEach(function (item) { + rawYtResults.forEach(function (item) { if (item && item.id && !seenIds[item.id] && item.title) { seenIds[item.id] = true; - uniqueResults.push(item); + uniqueYtResults.push(item); } }); - var count = uniqueResults.length; - if (count === 0) { + var ytCount = uniqueYtResults.length; + var localResults = localData.data || []; + var localTotal = typeof localData.total === 'number' ? localData.total : localResults.length; + + if (ytCount === 0 && localResults.length === 0) { container.innerHTML = '
' + '
' + '⚡ Canlı YouTube & Web Arama' + '

"' + - query + - '" için YouTube\'da Sonuç Bulunamadı

' + + escapeHtml(query) + + '" için Sonuç Bulunamadı' + '
' + + '' + '
'; - announce(query + " için YouTube\'da eşleşen video bulunamadı."); + announce(query + ' için YouTube veya yerel sunucuda eşleşen video bulunamadı.'); } else { - var countText = count === 1 ? '1 Video Bulundu' : count + ' Video Bulundu'; - announce(query + ' araması için ' + count + ' video listelendi.'); - var visibleCount = Math.min(12, count); + var visibleYtCount = Math.min(12, ytCount); - function render() { - var visibleResults = uniqueResults.slice(0, visibleCount); - var cardsHtml = ''; - visibleResults.forEach(function (item) { - var isLocal = !!item.isDownloaded; - var isLive = !!item.isLive; + function renderAll() { + // 1. YouTube Section + var ytHeaderHtml = ''; + var ytCardsHtml = ''; + var ytLoadMoreHtml = ''; - var statusBadge = ''; - if (isLive) { - statusBadge = - '🔴 CANLI YAYIN'; - } else if (isLocal) { - statusBadge = - '✅ ' + - getInstanceName() + - "'da Mevcut"; - } - - var actionButton = ''; - if (isLive) { - actionButton = - ''; - } else if (isLocal) { - actionButton = - '▶️ ' + - getInstanceName() + - "'dan İzle"; - } else { - actionButton = - '"; - } - - var durationText = isLive ? 'CANLI' : item.durationFormatted; - var cardLabel = - item.title + - (isLive ? ' (Canlı Yayın)' : isLocal ? ' (' + getInstanceName() + ' kütüphanenizde mevcut)' : '') + - ', ' + - item.channel + - ' kanalı, Süre: ' + - durationText; - cardsHtml += - '
' + - '
' + - '' +
-                  item.title.replace(/' + - '' + - durationText + - '' + + if (ytCount > 0) { + var countText = ytCount === 1 ? '1 Video Bulundu' : ytCount + ' Video Bulundu'; + ytHeaderHtml = + '
' + + '
' + + '⚡ Canlı YouTube & Web Arama' + + '

"' + + escapeHtml(query) + + '" için ' + + countText + + '

' + '
' + - '
' + - '
' + - statusBadge + - '

' + - item.title + - '

' + - '
' + - item.channel + - '
' + - '
' + - actionButton + - '
' + - '
'; - }); - - var loadMoreHtml = ''; - if (visibleCount < count) { - loadMoreHtml = - '
' + - '' + '
'; + + var visibleResults = uniqueYtResults.slice(0, visibleYtCount); + visibleResults.forEach(function (item) { + var isLocal = !!item.isDownloaded; + var isLive = !!item.isLive; + + var statusBadge = ''; + if (isLive) { + statusBadge = + '🔴 CANLI YAYIN'; + } else if (isLocal) { + statusBadge = + '✅ ' + + escapeHtml(getInstanceName()) + + '\'da Mevcut'; + } + + var actionButton = ''; + if (isLive) { + actionButton = + ''; + } else if (isLocal && item.localVideo && item.localVideo.url) { + actionButton = + '▶️ ' + + escapeHtml(getInstanceName()) + + '\'dan İzle'; + } else { + actionButton = + ''; + } + + var durationText = isLive ? 'CANLI' : (item.durationFormatted || formatClockTime(item.duration)); + var cardLabel = + item.title + + (isLive ? ' (Canlı Yayın)' : isLocal ? ' (' + getInstanceName() + ' kütüphanenizde mevcut)' : '') + + ', ' + + item.channel + + ' kanalı, Süre: ' + + durationText; + + ytCardsHtml += + '
' + + '
' + + '' +
+                    escapeHtml(item.title) +
+                    '' + + '' + + escapeHtml(durationText) + + '' + + '
' + + '
' + + '
' + + statusBadge + + '

' + + escapeHtml(item.title) + + '

' + + '
' + + escapeHtml(item.channel) + + '
' + + '
' + + actionButton + + '
' + + '
'; + }); + + if (visibleYtCount < ytCount) { + ytLoadMoreHtml = + '
' + + '' + + '
'; + } + } + + // 2. Local Videos Shelf + var localShelfHtml = ''; + if (localResults.length > 0) { + var localCardsHtml = ''; + localResults.forEach(function (lItem) { + var lThumb = lItem.thumbnailPath || (lItem.thumbnails && lItem.thumbnails[0] ? lItem.thumbnails[0].fileUrl : '/client/assets/images/default-thumbnail.jpg'); + var lDuration = formatClockTime(lItem.duration); + 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)); + var lViews = (lItem.views || 0) + ' görüntüleme'; + var lLabel = lItem.name + ', ' + lChannel + ', YakTube yerel sunucusunda mevcut, Süre: ' + lDuration; + + localCardsHtml += + ''; + }); + + localShelfHtml = + '
' + + '
' + + '
' + + '✅ Yerel Kütüphane' + + '

"' + + escapeHtml(query) + + '" için ' + escapeHtml(getInstanceName()) + ' Sunucusunda ' + localTotal + ' Video Bulundu

' + + '
' + + 'Doğrudan sunucumuzda barındırılan, yüksek hızlı ve anında izlenebilen videolar' + + '
' + + '
' + + localCardsHtml + + '
' + + '
'; } container.innerHTML = - '
' + - '
' + - '⚡ Canlı YouTube & Web Arama' + - '

"' + - query + - '" için ' + - countText + - '

' + - '
' + - '' + - '
' + - '
' + - cardsHtml + - '
' + - loadMoreHtml; + ytHeaderHtml + + (ytCount > 0 ? '
' + ytCardsHtml + '
' : '') + + ytLoadMoreHtml + + localShelfHtml; var sBtn = document.getElementById('yaktube-open-shortcuts'); if (sBtn) { @@ -1927,9 +2157,9 @@ try { var loadMoreBtn = document.getElementById('yaktube-load-more-btn'); if (loadMoreBtn) { loadMoreBtn.addEventListener('click', function () { - visibleCount = Math.min(visibleCount + 6, count); - render(); - announce(visibleCount + ' adet video gösteriliyor.'); + visibleYtCount = Math.min(visibleYtCount + 6, ytCount); + renderAll(); + announce(visibleYtCount + ' adet video gösteriliyor.'); }); } @@ -1950,7 +2180,17 @@ try { }); } - render(); + 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); } }) .catch(function (err) { @@ -1958,14 +2198,15 @@ try { container.innerHTML = '
' + '
' + - '\u26a1 Canl\u0131 YouTube & Web Arama' + - '

Arama s\u0131ras\u0131nda bir ba\u011flant\u0131 sorunu olu\u015ftu.

' + + '⚡ Canlı YouTube & Web Arama' + + '

Arama sırasında bir bağlantı sorunu oluştu.

' + '
' + '
'; - announce('Arama s\u0131ras\u0131nda bir ba\u011flant\u0131 sorunu olu\u015ftu.'); + announce('Arama sırasında bir bağlantı sorunu oluştu.'); }) .finally(function () { isSearching = false; + ensureFediverseBridge(query, currentSearchTarget); }); } @@ -2353,7 +2594,7 @@ try { saveRecentSearch(q); announce('"' + q + '" için arama yapılıyor...', true); closeFullSearchModal(); - window.location.href = '/search?search=' + encodeURIComponent(q); + window.location.href = '/search?search=' + encodeURIComponent(q) + '&searchTarget=search-index'; } } window.__yaktube_execute_search__ = executeSearch; @@ -2485,7 +2726,7 @@ try { announce('"' + transcript + '" için arama başlatılıyor...', true); setTimeout(function () { closeFullSearchModal(); - window.location.href = '/search?search=' + encodeURIComponent(transcript); + window.location.href = '/search?search=' + encodeURIComponent(transcript) + '&searchTarget=search-index'; }, 500); } } diff --git a/package.json b/package.json index 1a61662..45753ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "peertube-plugin-yaktube-ondemand", - "version": "1.4.28", + "version": "1.4.29", "description": "Accessible On-Demand YouTube Import, Live Streams, Smart Recommendations and A11y Engine for PeerTube", "main": "main.js", "library": "./main.js", diff --git a/style.css b/style.css index 5cbda54..7c25035 100644 --- a/style.css +++ b/style.css @@ -960,3 +960,140 @@ body:has(#yaktube-ondemand-container) my-error-page { height: 0 !important; overflow: hidden !important; } + +/* -------------------------------------------------------------------------- */ +/* YakTube Local Video Shelf & Highlights (v1.4.29) */ +/* -------------------------------------------------------------------------- */ +.yaktube-local-shelf { + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid rgba(16, 185, 129, 0.25); +} + +.yaktube-local-shelf-sub { + font-size: 12px; + color: #a7f3d0; + margin-top: 4px; + display: block; +} + +.yaktube-local-card { + border-color: rgba(16, 185, 129, 0.35) !important; +} + +.yaktube-local-card:hover, +.yaktube-local-card:focus-within { + border-color: #10b981 !important; + box-shadow: 0 10px 22px rgba(16, 185, 129, 0.22) !important; +} + +/* -------------------------------------------------------------------------- */ +/* YakTube Fediverse Bridge & Scope Selector (v1.4.29) */ +/* -------------------------------------------------------------------------- */ +.yaktube-fediverse-bridge { + margin: 28px 0 16px 0; + padding: 16px 20px; + background: linear-gradient(135deg, rgba(30, 27, 75, 0.85) 0%, rgba(15, 23, 42, 0.9) 100%); + border: 1px solid rgba(99, 102, 241, 0.4); + border-radius: 14px; + backdrop-filter: blur(12px); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35); + color: #ffffff; +} + +.yaktube-fediverse-bridge-content { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + flex-wrap: wrap; + gap: 16px; +} + +.yaktube-fediverse-bridge-left { + flex: 1 1 320px; +} + +.yaktube-fediverse-bridge-badge-wrap { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; +} + +.yaktube-fediverse-badge { + background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); + color: #fff; + font-size: 11px; + font-weight: 700; + padding: 3px 9px; + border-radius: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.yaktube-fediverse-status { + font-size: 12px; + font-weight: 600; + color: #a5b4fc; +} + +.yaktube-fediverse-title { + margin: 0; + font-size: 1.15rem; + font-weight: 700; + color: #ffffff; + display: flex; + align-items: center; + gap: 8px; +} + +.yaktube-fediverse-sub { + margin: 4px 0 0 0; + font-size: 12px; + color: #94a3b8; + line-height: 1.4; +} + +.yaktube-scope-selector { + display: inline-flex; + align-items: center; + background: rgba(15, 23, 42, 0.75); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 10px; + padding: 4px; + gap: 4px; +} + +.yaktube-scope-btn { + background: transparent; + border: 1px solid transparent; + color: #94a3b8; + padding: 8px 16px; + font-size: 13px; + font-weight: 600; + border-radius: 8px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + transition: all 0.2s ease; +} + +.yaktube-scope-btn:hover { + color: #ffffff; + background: rgba(255, 255, 255, 0.08); +} + +.yaktube-scope-btn.active { + background: linear-gradient(135deg, #ff8f37 0%, #ff5e3a 100%); + color: #ffffff; + border-color: rgba(255, 143, 55, 0.4); + box-shadow: 0 2px 8px rgba(255, 143, 55, 0.35); + font-weight: 700; +} + +.yaktube-scope-btn:focus-visible { + outline: 2px solid #ff8f37; + outline-offset: 2px; +}