NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Nova YouTube // @namespace https://github.com/raingart/Nova-YouTube-extension/ // @version 0.44.0 // @description Powerful control on YouTube // @description:zh-CN 最好的玉棒 youtube // @author raingart <raingart+scriptaddons@protonmail.com> // @license Apache-2.0 // @icon https://raw.github.com/raingart/Nova-YouTube-extension/master/icons/48.png // @homepageURL https://github.com/raingart/Nova-YouTube-extension // @supportURL https://github.com/raingart/Nova-YouTube-extension/issues // @contributionURL https://www.patreon.com/raingart // @contributionURL https://www.buymeacoffee.com/raingart // @contributionURL https://www.paypal.com/donate/?hosted_button_id=B44WLWHZ8AGU2 // @domain youtube.com // @include http*://www.youtube.com/* // @include http*://m.youtube.com/* // @include http*://*.youtube-nocookie.com/embed/* // @include http*://youtube.googleapis.com/embed/* // @include http*://raingart.github.io/options.html* // @exclude http*://*.youtube.com/*.xml* // @exclude http*://*.youtube.com/error* // @exclude http*://music.youtube.com/* // @exclude http*://accounts.youtube.com/* // @exclude http*://studio.youtube.com/* // @exclude http*://*.youtube.com/redirect?* // @grant GM_getResourceText // @grant GM_getResourceURL // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_openInTab // @grant unsafeWindow // @run-at document-start // @compatible chrome >=80 Violentmonkey,Tampermonkey // @compatible firefox >=74 Tampermonkey // ==/UserScript== /*jshint esversion: 6 */ if (typeof GM_info === 'undefined') { alert('Direct Chromium is not supported now'); } if (!('MutationObserver' in window)) { errorAlert('MutationObserver not supported'); } try { document?.body; } catch (error) { errorAlert('Your browser does not support chaining operator'); } switch (GM_info.scriptHandler) { case 'Tampermonkey': case 'Violentmonkey': case 'ScriptCat': break; case 'FireMonkey': errorAlert(GM_info.scriptHandler + ' incomplete support', false); break; case 'Greasemonkey': errorAlert(GM_info.scriptHandler + ' is not supported'); break; case 'Stay': errorAlert(GM_info.scriptHandler + ' is not tested!\nPlease inform the author about the working status'); break; default: if (typeof GM_getValue !== 'function') { errorAlert('Your ' + GM_info.scriptHandler + ' does not support/no access the API being used. Contact the developer') } break; } function errorAlert(text = '', stop_execute = true) { alert(GM_info.script.name + ' Error!\n' + text); if (stop_execute) { throw GM_info.script.name + ' crashed!\n' + text; } } window.nova_plugins = []; window.nova_plugins.push({ id: 'comments-sort', title: 'Comments sort', 'title:zh': '评论排序', 'title:ja': 'コメントの並べ替え', 'title:ko': '댓글 정렬', 'title:id': 'Mengurutkan komentar', 'title:es': 'Clasificación de comentarios', 'title:pt': 'classificação de comentários', 'title:fr': 'Tri des commentaires', 'title:it': 'Ordinamento dei commenti', 'title:de': 'Kommentare sortieren', 'title:pl': 'Sortowanie komentarzy', 'title:ua': 'Сортування коментарів', run_on_pages: 'watch, -mobile', opt_api_key_warn: true, section: 'comments', desc: 'add modal', 'desc:ua': 'Додати спосіб подання', _runtime: user_settings => { const MAX_COMMENTS = 500, MODAL_NAME_SELECTOR_ID = 'nova-modal-comments', MODAL_CONTENT_SELECTOR_ID = 'modal-content', NOVA_REPLYS_SELECTOR_ID = 'nova-replys'; insertButton(); function insertButton() { NOVA.waitSelector( user_settings['comments-popup'] ? '#masthead-container' : '#comments ytd-comments-header-renderer #title' ) .then(menu => { const btn = document.createElement('span'); btn.setAttribute('data-open-modal', MODAL_NAME_SELECTOR_ID); btn.title = 'Nova Comments'; btn.textContent = '►'; btn.addEventListener('click', () => { if (!document.body.querySelector(`#${MODAL_CONTENT_SELECTOR_ID} table`)) { getComments(); } btn.dispatchEvent(new CustomEvent(MODAL_NAME_SELECTOR_ID, { bubbles: true, detail: 'test' })); }); Object.assign(btn.style, user_settings['comments-popup'] ? { position: 'fixed', right: '0', top: 'var(--ytd-masthead-height)', visibility: 'visible', 'z-index': 1 + Math.max( NOVA.css.getValue('.ytp-chrome-top', 'z-index'), 60), 'font-size': '18px', } : { 'font-size': '24px', 'text-decoration': 'none', padding: '0 10px', background: 'transparent', border: 'none', }, { color: 'orange', cursor: 'pointer', }); user_settings['comments-popup'] ? menu.append(btn) : menu.prepend(btn); insertModal(); connectSortable(); NOVA.runOnPageInitOrTransition(() => { if (NOVA.currentPage == 'watch') { document.getElementById(MODAL_CONTENT_SELECTOR_ID).innerHTML = '<pre>Loading data...</pre>'; } }); }); } let commentList = [] function getComments(next_page_token) { const params = { 'videoId': NOVA.queryURL.get('v') || movie_player.getVideoData().video_id, 'part': 'snippet,replies', 'maxResults': 100, 'order': 'relevance', }; if (next_page_token) { params['pageToken'] = next_page_token; } NOVA.request.API({ request: 'commentThreads', params: params, api_key: user_settings['user-api-key'], }) .then(res => { if (res?.error) { if (res.reason) { document.getElementById(MODAL_NAME_SELECTOR_ID) .dispatchEvent(new CustomEvent(MODAL_NAME_SELECTOR_ID, { bubbles: true, detail: 'test' })); return alert(`Error [${res.code}]: ${res.reason}`); } else { return document.getElementById(MODAL_CONTENT_SELECTOR_ID).innerHTML = `<pre>Error [${res.code}]: ${res.reason}</pre> <pre>${res.error}</pre>`; } } res?.items?.forEach(item => { if (comment = item.snippet?.topLevelComment?.snippet) { commentList.push( Object.assign( { 'totalReplyCount': item.snippet.totalReplyCount }, { 'id': item.id }, comment, item.replies, ) ); } else { console.warn('API is change', item); } }); if (!user_settings['user-api-key'] && commentList.length > MAX_COMMENTS) { alert('Use your personal API key to overcome the 500 comments limit'); genTable(); } else if (res?.nextPageToken) { document.getElementById(MODAL_CONTENT_SELECTOR_ID).innerHTML = `<pre>Loading: ${commentList.length}</pre>`; getComments(res?.nextPageToken); } else { genTable(); } }); } function genTable() { if (!commentList.length) { return document.getElementById(MODAL_CONTENT_SELECTOR_ID).innerHTML = `<pre>Comments empty</pre>`; } const ul = document.createElement('tbody'); commentList .sort((a, b) => b.likeCount - a.likeCount) .forEach(comment => { try { const countWords = (str = '') => str.trim().split(/\s+/).length, clearOfEmoji = str => str .replace(/[\u2011-\u26FF]/g, ' ') .replace(/[^<>=\p{L}\p{N}\p{P}\p{Z}{\^\$}]/gu, ' ') .replace(/([=:;/.()]{2,}|\))$/g, ' ') .replace(/\s{2,}/g, ' ') .replace(/(<br>){3,}/g, '<br><br>') .trim(); if (user_settings.comments_sort_clear_emoji) { comment.textDisplay = clearOfEmoji(comment.textDisplay); if (comment.textDisplay.length < 3) return; if (+user_settings.comments_sort_min_words && countWords(comment.textDisplay) <= +user_settings.comments_sort_min_words ) { return; } } const replyInputName = `${NOVA_REPLYS_SELECTOR_ID}-${comment.id}`, li = document.createElement('tr'); li.className = 'item'; li.innerHTML = `<td>${comment.likeCount}</td> <td sorttable_customkey="${comment.totalReplyCount}"> ${comment.comments?.length ? `<a href="https://www.youtube.com/watch?v=${comment.videoId}&lc=${comment.id}" target="_blank" title="Open comment link">${comment.comments.length}</a> <label for="${replyInputName}"></label>` : ''}</td> <td sorttable_customkey="${new Date(comment.updatedAt).getTime()}">${NOVA.timeFormatTo.ago(new Date(comment.updatedAt))}</td> <td> <a href="${comment.authorChannelUrl}" target="_blank" title="${comment.authorDisplayName}"> <img src="${comment.authorProfileImageUrl}" alt="${comment.authorDisplayName}" /> </a> </td> <td sorttable_customkey="${comment.textOriginal.length}"> <span class="text-overflow-dynamic-ellipsis">${comment.textDisplay}</span> ${appendReplies()} </td>`; ul.append(li); if (+comment.totalReplyCount) { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = checkbox.name = replyInputName; checkbox.addEventListener('change', ({ target }) => { document.body.querySelector(`table[${NOVA_REPLYS_SELECTOR_ID}="${target.name}"]`) .classList.toggle('nova-hide'); }); li.querySelector('td label[for]').append(checkbox); } function appendReplies() { if (!+comment.totalReplyCount) return ''; const table = document.createElement('table'); table.className = 'nova-hide'; table.setAttribute(NOVA_REPLYS_SELECTOR_ID, replyInputName); comment.comments ?.forEach(reply => { if (user_settings.comments_sort_clear_emoji) { reply.snippet.textDisplay = clearOfEmoji(reply.snippet.textDisplay); if (+user_settings.comments_sort_min_words && countWords(reply.snippet.textDisplay) <= +user_settings.comments_sort_min_words ) { return; } } const li = document.createElement('tr'); li.innerHTML = `<td> <a href="${reply.snippet.authorChannelUrl}" target="_blank" title="${reply.snippet.authorDisplayName}"> <img src="${reply.snippet.authorProfileImageUrl}" alt="${reply.snippet.authorDisplayName}" /> </a> </td> <td> <span class="text-overflow-dynamic-ellipsis"> <div class="nova-reply-time-text">${reply.snippet.likeCount ? `${reply.snippet.likeCount} likes` : ''}</div> <div>${reply.snippet.textDisplay}</div> </span> </td>`; table.append(li); }); return table.outerHTML; } } catch (error) { console.error('Error comment generate:\n', error.stack + '\n', comment); } }); const MODAL_CONTENT_FILTER_SELECTOR_ID = 'nova-search-comment'; document.getElementById(MODAL_CONTENT_SELECTOR_ID).innerHTML = `<table class="sortable" border="0" cellspacing="0" cellpadding="0"> <thead id="${MODAL_CONTENT_FILTER_SELECTOR_ID}"> <tr> <th class="sorttable_numeric">likes</th> <th class="sorttable_numeric">replys</th> <th class="sorttable_numeric">date</th> <th class="sorttable_nosort">avatar</th> <th class="sorttable_numeric">comments (${commentList.length})</th> </tr> </thead> <!-- $ {ul.innerHTML} --> </table>`; document.getElementById(MODAL_CONTENT_FILTER_SELECTOR_ID).after(ul); sorttable.makeSortable(document.body.querySelector('.sortable')); insertFilterInput(MODAL_CONTENT_FILTER_SELECTOR_ID); NOVA.css.push( `.nova-hide { display: none; } table[${NOVA_REPLYS_SELECTOR_ID}] { border: 1px solid #444; width: auto !important; } table[${NOVA_REPLYS_SELECTOR_ID}] td { padding: auto 10px; } .nova-reply-time-text { font-size: .5em; font-style: italic; }`); } function insertFilterInput(parent_selector_id = required()) { if (typeof parent_selector_id !== 'string') { return console.error('typeof "parent_selector_id":', (typeof parent_selector_id)); } NOVA.css.push( `#${parent_selector_id} { position: relative; } #${parent_selector_id} input { position: absolute; top: 0; right: 0; } #${parent_selector_id} input[type=search]:focus, #${parent_selector_id} input[type=text]:focus { outline: 1px solid #00b7fc; } .nova-mark-text { background-color: #ff0; background-color: mark; }`); const searchInput = document.createElement('input'); searchInput.setAttribute('type', 'search'); searchInput.setAttribute('placeholder', 'Filter'); ['change', 'keyup'].forEach(evt => { searchInput .addEventListener(evt, function () { NOVA.searchFilterHTML({ 'keyword': this.value, 'filter_selectors': 'tr.item', 'highlight_selector': '.text-overflow-dynamic-ellipsis', 'highlight_class': 'nova-mark-text', }); }); searchInput .addEventListener('click', () => { searchInput.value = ''; searchInput.dispatchEvent(new Event('change')); }); }); document.getElementById(parent_selector_id).append(searchInput); }; function insertModal() { NOVA.css.push( `.modal { --animation-time: .2s; z-index: 9999; position: fixed; top: 0; left: 0; background: rgba(0, 0, 0, .8); display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; box-sizing: border-box; visibility: hidden; opacity: 0; } .modal.modal-visible { animation: microModalFadeIn var(--animation-time) cubic-bezier(0, 0, .2, 1); visibility: visible; opacity: 1; } @keyframes microModalFadeIn { from { opacity: 0; } to { opacity: 1; } } .modal-container { border-radius: 4px; background-color: silver; position: relative; display: flex; box-sizing: border-box; overflow-y: auto; max-width: 70%; max-height: 100vh; transform: scale(0.9); transition: all var(--animation-time) ease-out; } .modal.modal-visible .modal-container { transform: scale(1); } .modal-close { position: absolute; top: 0; right: 0; cursor: pointer; font-size: 2em; padding: 0 5px; transition: background-color var(--animation-time) ease-out; } .modal-close:before { content: "\\2715"; } .modal-close:hover { background-color: #ea3c3c; } .modal-content { padding: 2rem; }`); NOVA.css.push( `.modal {} .modal-container { background-color: var(--yt-spec-brand-background-primary); background-color: var(--yt-spec-menu-background); background-color: var(--yt-spec-raised-background); color: var(--yt-spec-text-primary); } .modal-content { font-size: 12px; }`); document.body .insertAdjacentHTML('beforeend', `<div id="${MODAL_NAME_SELECTOR_ID}" class="modal" data-modal> <div class="modal-container"> <div class="modal-close" data-close-modal></div> <div class="modal-content" id="${MODAL_CONTENT_SELECTOR_ID}"></div> </div> </div>`); const modalShowClass = 'modal-visible'; document.getElementById(MODAL_NAME_SELECTOR_ID) .addEventListener('click', ({ target }) => { target.dispatchEvent(new CustomEvent(MODAL_NAME_SELECTOR_ID, { bubbles: true, detail: 'test' })); }); document.addEventListener(MODAL_NAME_SELECTOR_ID, ({ target }) => { const attrModal = target.hasAttribute('data-modal'), attrOpen = target.getAttribute('data-open-modal'), attrClose = target.hasAttribute('data-close-modal'); if (attrModal) { target.classList.remove(modalShowClass); } else if (attrOpen && (modal = document.getElementById(attrOpen))) { modal.classList.add(modalShowClass); } else if (attrClose && (modal = target.closest('[data-modal]'))) { modal.classList.remove(modalShowClass); } }); } function connectSortable() { NOVA.css.push( `table.sortable table { width: 100%; } table.sortable thead {} table.sortable thead th { text-transform: uppercase; cursor: pointer; } thead, th, td { text-align: center; } table tbody { counter-reset: sortabletablescope; } `); NOVA.css.push( `#${MODAL_CONTENT_SELECTOR_ID} table {} #${MODAL_CONTENT_SELECTOR_ID} th { padding: 5px 3px; font-weight: 500; } #${MODAL_CONTENT_SELECTOR_ID} thead { background-color: var(--yt-spec-text-secondary); background-color: var(--yt-spec-outline); } #${MODAL_CONTENT_SELECTOR_ID} tbody { margin-top: 0px; } #${MODAL_CONTENT_SELECTOR_ID} tr:nth-child(even) { background-color: var(--yt-spec-menu-background); } #${MODAL_CONTENT_SELECTOR_ID} td .text-overflow-dynamic-ellipsis { display: block; max-height: 25vh; overflow-y: auto; text-align: left; font-size: 1.2em; line-height: 1.4; padding: 10px 5px; max-width: 1200px; } #${MODAL_CONTENT_SELECTOR_ID} td a { text-decoration: none; color: var(--yt-spec-call-to-action); }`); function dean_addEvent(t, e, r) { if (t.addEventListener) t.addEventListener(e, r, !1); else { r.$$guid || (r.$$guid = dean_addEvent.guid++), t.events || (t.events = {}); var o = t.events[e]; o || (o = t.events[e] = {}, t["on" + e] && (o[0] = t["on" + e])), o[r.$$guid] = r, t["on" + e] = handleEvent } } function handleEvent(t) { var e = !0; t = t || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event); var r = this.events[t.type]; for (var o in r) this.$$handleEvent = r[o], !1 === this.$$handleEvent(t) && (e = !1); return e } function fixEvent(t) { return t.preventDefault = fixEvent.preventDefault, t.stopPropagation = fixEvent.stopPropagation, t } sorttable = { makeSortable: function (t) { if (0 == t.getElementsByTagName("thead").length && (the = document.createElement("thead"), the.appendChild(t.rows[0]), t.insertBefore(the, t.firstChild)), null == t.tHead && (t.tHead = t.getElementsByTagName("thead")[0]), 1 == t.tHead.rows.length) { sortbottomrows = []; for (var e = 0; e < t.rows.length; e++)-1 != t.rows[e].className.search(/\bsortbottom\b/) && (sortbottomrows[sortbottomrows.length] = t.rows[e]); if (sortbottomrows) { null == t.tFoot && (tfo = document.createElement("tfoot"), t.appendChild(tfo)); for (e = 0; e < sortbottomrows.length; e++)tfo.appendChild(sortbottomrows[e]); delete sortbottomrows } headrow = t.tHead.rows[0].cells; for (e = 0; e < headrow.length; e++)headrow[e].className.match(/\bsorttable_nosort\b/) || (mtch = headrow[e].className.match(/\bsorttable_([a-z0-9]+)\b/), mtch && (override = mtch[1]), mtch && "function" == typeof sorttable["sort_" + override] ? headrow[e].sorttable_sortfunction = sorttable["sort_" + override] : headrow[e].sorttable_sortfunction = sorttable.guessType(t, e), headrow[e].sorttable_columnindex = e, headrow[e].sorttable_tbody = t.tBodies[0], dean_addEvent(headrow[e], "click", sorttable.innerSortFunction = function (t) { if (-1 != this.className.search(/\bsorttable_sorted\b/)) return sorttable.reverse(this.sorttable_tbody), this.className = this.className.replace("sorttable_sorted", "sorttable_sorted_reverse"), this.removeChild(document.getElementById("sorttable_sortfwdind")), sortrevind = document.createElement("span"), sortrevind.id = "sorttable_sortrevind", sortrevind.innerHTML = " ▴", void this.appendChild(sortrevind); if (-1 != this.className.search(/\bsorttable_sorted_reverse\b/)) return sorttable.reverse(this.sorttable_tbody), this.className = this.className.replace("sorttable_sorted_reverse", "sorttable_sorted"), this.removeChild(document.getElementById("sorttable_sortrevind")), sortfwdind = document.createElement("span"), sortfwdind.id = "sorttable_sortfwdind", sortfwdind.innerHTML = " ▾", void this.appendChild(sortfwdind); theadrow = this.parentNode, forEach(theadrow.childNodes, (function (t) { 1 == t.nodeType && (t.className = t.className.replace("sorttable_sorted_reverse", ""), t.className = t.className.replace("sorttable_sorted", "")) })), sortfwdind = document.getElementById("sorttable_sortfwdind"), sortfwdind && sortfwdind.parentNode.removeChild(sortfwdind), sortrevind = document.getElementById("sorttable_sortrevind"), sortrevind && sortrevind.parentNode.removeChild(sortrevind), this.className += " sorttable_sorted", sortfwdind = document.createElement("span"), sortfwdind.id = "sorttable_sortfwdind", sortfwdind.innerHTML = " ▾", this.appendChild(sortfwdind), row_array = [], col = this.sorttable_columnindex, rows = this.sorttable_tbody.rows; for (var e = 0; e < rows.length; e++)row_array[row_array.length] = [sorttable.getInnerText(rows[e].cells[col]), rows[e]]; row_array.sort(this.sorttable_sortfunction).reverse(), tb = this.sorttable_tbody; for (e = 0; e < row_array.length; e++)tb.appendChild(row_array[e][1]); delete row_array })) } }, guessType: function (t, e) { sortfn = sorttable.sort_alpha; for (var r = 0; r < t.tBodies[0].rows.length; r++)if (text = sorttable.getInnerText(t.tBodies[0].rows[r].cells[e]), "" != text && text.match(/^-?[£$¤]?[\d,.]+%?$/)) return sorttable.sort_numeric; return sortfn }, getInnerText: function (t) { if (!t) return ""; if (hasInputs = "function" == typeof t.getElementsByTagName && t.getElementsByTagName("input").length, null != t.getAttribute("sorttable_customkey")) return t.getAttribute("sorttable_customkey"); if (void 0 !== t.textContent && !hasInputs) return t.textContent.replace(/^\s+|\s+$/g, ""); if (void 0 !== t.innerText && !hasInputs) return t.innerText.replace(/^\s+|\s+$/g, ""); if (void 0 !== t.text && !hasInputs) return t.text.replace(/^\s+|\s+$/g, ""); switch (t.nodeType) { case 3: if ("input" == t.nodeName.toLowerCase()) return t.value.replace(/^\s+|\s+$/g, ""); case 4: return t.nodeValue.replace(/^\s+|\s+$/g, ""); case 1: case 11: for (var e = "", r = 0; r < t.childNodes.length; r++)e += sorttable.getInnerText(t.childNodes[r]); return e.replace(/^\s+|\s+$/g, ""); default: return "" } }, reverse: function (t) { newrows = []; for (var e = 0; e < t.rows.length; e++)newrows[newrows.length] = t.rows[e]; for (e = newrows.length - 1; e >= 0; e--)t.appendChild(newrows[e]); delete newrows }, sort_numeric: function (t, e) { return aa = parseFloat(t[0].replace(/[^0-9.-]/g, "")), isNaN(aa) && (aa = 0), bb = parseFloat(e[0].replace(/[^0-9.-]/g, "")), isNaN(bb) && (bb = 0), aa - bb }, sort_alpha: function (t, e) { return t[0].localeCompare(e[0]) } }, dean_addEvent.guid = 1, fixEvent.preventDefault = function () { this.returnValue = !1 }, fixEvent.stopPropagation = function () { this.cancelBubble = !0 }, Function.prototype.forEach = function (t, e, r) { for (var o in t) void 0 === this.prototype[o] && e.call(r, t[o], o, t) }, String.forEach = function (t, e, r) { Array.forEach(t.split(""), (function (o, n) { e.call(r, o, n, t) })) }; var forEach = function (t, e, r) { if (t) { var o = Object; if (t instanceof Function) o = Function; else { if (t.forEach instanceof Function) return void t.forEach(e, r); "string" == typeof t ? o = String : "number" == typeof t.length && (o = Array) } o.forEach(t, e, r) } }; } }, options: { comments_sort_clear_emoji: { _tagName: 'input', label: 'Clear of emoji', type: 'checkbox', }, comments_sort_min_words: { _tagName: 'input', label: 'Min words count', type: 'number', title: '0 - disable', placeholder: '1-10', min: 1, max: 10, value: 2, 'data-dependent': { 'comments_sort_clear_emoji': true }, }, }, }); window.nova_plugins.push({ id: 'comments-popup', title: 'Comments section in popup', 'title:zh': '弹出窗口中的评论部分', 'title:ja': 'ポップアップのコメントセクション', 'title:ko': '팝업의 댓글 섹션', 'title:id': 'Bagian komentar di popup', 'title:es': 'Sección de comentarios en ventana emergente', 'title:pt': 'Seção de comentários no pop-up', 'title:fr': 'Section des commentaires dans la fenêtre contextuelle', 'title:it': 'Sezione commenti nel popup', 'title:de': 'Kommentarbereich im Popup', 'title:pl': 'Sekcja komentarzy w osobnym oknie', 'title:ua': 'Розділ коментарів у спливаючому вікні', run_on_pages: 'watch, -mobile', section: 'comments', _runtime: user_settings => { if (user_settings['comments_visibility_mode'] == 'disable') return; const COMMENTS_SELECTOR = 'html:not(:fullscreen) #page-manager #comments:not([hidden]):not(:empty)', counterAttrName = 'data-counter'; NOVA.runOnPageInitOrTransition(() => { if (NOVA.currentPage == 'watch') { NOVA.waitSelector('ytd-comments-header-renderer #title #count', { destroy_if_url_changes: true }) .then(count => { document.body.querySelector(COMMENTS_SELECTOR) ?.setAttribute(counterAttrName, NOVA.prettyRoundInt(parseInt(count.textContent.replace(/,/g, ''))) ); }); } }); NOVA.waitSelector('#masthead-container') .then(masthead => { NOVA.css.push( `${COMMENTS_SELECTOR}, ${COMMENTS_SELECTOR}:before { position: fixed; top: ${masthead.offsetHeight || 56}px; right: 0; z-index: ${1 + Math.max(getComputedStyle(masthead || movie_player)['z-index'], 601)}; } ${COMMENTS_SELECTOR}:not(:hover):before { content: attr(${counterAttrName}) " comments ▼"; cursor: pointer; visibility: visible; right: 3em; padding: 0 6px 2px; line-height: normal; font-family: Roboto, Arial, sans-serif; font-size: 11px; color: #eee; background: rgba(0,0,0,0.3); } ${COMMENTS_SELECTOR} { ${(user_settings.comments_popup_width === 100) ? 'margin: 0 1%;' : ''} padding: 0 15px; background-color: var(--yt-spec-brand-background-primary); background-color: var(--yt-spec-menu-background); background-color: var(--yt-spec-raised-background); color: var(--yt-spec-text-primary);; border: 1px solid #333; max-width: ${user_settings.comments_popup_width || 40}%; } ${COMMENTS_SELECTOR}:not(:hover) { visibility: collapse; } ${COMMENTS_SELECTOR}:hover { visibility: visible !important; } ${COMMENTS_SELECTOR} > #sections > #contents { overflow-y: auto; max-height: 88vh; padding-top: 1em; } #expander.ytd-comment-renderer { overflow-x: hidden; } ${COMMENTS_SELECTOR} #sections { min-width: 500px; } ${COMMENTS_SELECTOR} #contents::-webkit-scrollbar { height: 8px; width: 10px; } ${COMMENTS_SELECTOR} #contents::-webkit-scrollbar-button { height: 0; width: 0; } ${COMMENTS_SELECTOR} #contents::-webkit-scrollbar-corner { background: transparent; } ${COMMENTS_SELECTOR} #contents::-webkit-scrollbar-thumb { background: #e1e1e1; border: 0; border-radius: 0; } ${COMMENTS_SELECTOR} #contents::-webkit-scrollbar-track { background: #666; border: 0; border-radius: 0; } ${COMMENTS_SELECTOR} #contents::-webkit-scrollbar-track:hover { background: #666; } ytd-comments-header-renderer { margin: 10px 0 !important; }`); if (user_settings.comments_popup_hide_textarea) { NOVA.css.push( `${COMMENTS_SELECTOR} > #sections > #contents { overflow-y: auto; max-height: 88vh; border-top: 1px solid #333; padding-top: 1em; } ${COMMENTS_SELECTOR} #header #simple-box { display: none; } ytd-comments-header-renderer #title { margin: 0 !important; }`); } }); }, options: { comments_popup_width: { _tagName: 'input', label: 'Width', 'label:ua': 'Ширина', type: 'number', title: '% of the screen width', placeholder: '%', step: 5, min: 10, max: 100, value: 40, }, comments_popup_hide_textarea: { _tagName: 'input', label: 'Hide textarea', 'label:ua': 'Приховати поле вводу', type: 'checkbox', }, } }); window.nova_plugins.push({ id: 'comments-expand', title: 'Expand comments', 'title:zh': '展开评论', 'title:ja': 'コメントを展開', 'title:ko': '댓글 펼치기', 'title:id': 'Perluas komentar', 'title:es': 'Expandir comentarios', 'title:pt': 'Expandir comentários', 'title:fr': 'Développer les commentaires', 'title:it': 'Espandi i commenti', 'title:de': 'Kommentare erweitern', 'title:pl': 'Rozwiń komentarze', 'title:ua': 'Розгорнути коментарі', run_on_pages: 'watch, -mobile', section: 'comments', _runtime: user_settings => { NOVA.css.push( `#expander.ytd-comment-renderer { overflow-x: hidden; }`); NOVA.watchElements({ selectors: ['#comment #expander[collapsed] #more:not([hidden])'], attr_mark: 'nova-comment-expanded', callback: btn => { const moreExpand = () => btn.click(); const comment = btn.closest('#expander[collapsed]'); switch (user_settings.comments_expand_mode) { case 'onhover': comment.addEventListener('mouseenter', moreExpand, { capture: true, once: true }); break; case 'always': moreExpand(); break; } }, }); NOVA.watchElements({ selectors: ['#replies #more-replies button'], attr_mark: 'nova-replies-expanded', callback: btn => { const moreExpand = () => btn.click(); switch (user_settings.comments_view_reply) { case 'onhover': btn.addEventListener('mouseenter', moreExpand, { capture: true, once: true }); break; case 'always': moreExpand(); break; } }, }); if (NOVA.queryURL.has('lc')) { NOVA.waitSelector('#comment #linked-comment-badge + #body #expander[collapsed] #more:not([hidden])') .then(btn => btn.click()); NOVA.waitSelector('ytd-comment-thread-renderer:has(#linked-comment-badge) #replies #more-replies button') .then(btn => btn.click()); } }, options: { comments_expand_mode: { _tagName: 'select', label: 'Expand comment', 'label:zh': '展开评论', 'label:ja': 'コメントを展開', 'label:ko': '댓글 펼치기', 'label:id': 'Perluas komentar', 'label:es': 'Expandir comentarios', 'label:pt': 'Expandir comentário', 'label:fr': 'Développer les commentaires', 'label:it': 'Espandi commento', 'label:de': 'Kommentar erweitern', 'label:pl': 'Rozwiń komentarz', 'label:ua': 'Розгорнути коментар', options: [ { label: 'always', value: 'always', selected: true, 'label:zh': '每次', 'label:ja': 'いつも', 'label:ko': '언제나', 'label:es': 'siempre', 'label:pt': 'sempre', 'label:fr': 'toujours', 'label:de': 'stets', 'label:pl': 'zawsze', 'label:ua': 'завжди', }, { label: 'on hover', value: 'onhover', 'label:zh': '悬停时', 'label:ja': 'ホバー時に', 'label:ko': '호버에', 'label:id': 'saat melayang', 'label:es': 'en vuelo estacionario', 'label:pt': 'pairando', 'label:fr': 'en vol stationnaire', 'label:it': 'quando in bilico', 'label:de': 'auf schweben', 'label:pl': 'przy najechaniu', 'label:ua': 'при наведенні', }, { label: 'disable', value: false, 'label:ua': 'вимк.', }, ], }, comments_view_reply: { _tagName: 'select', label: 'Expand reply', 'label:zh': '展开回复', 'label:ja': '返信を展開', 'label:ko': '답장 펼치기', 'label:id': 'Perluas balasan', 'label:es': 'Expandir respuesta', 'label:pt': 'Expandir a resposta', 'label:fr': 'Développer la réponse', 'label:it': 'Espandi risposta', 'label:de': 'Antwort erweitern', 'label:pl': 'Rozwiń odpowiedź', 'label:ua': 'Розгорнути відповідь', options: [ { label: 'always', value: 'always', 'label:zh': '每次', 'label:ja': 'いつも', 'label:ko': '언제나', 'label:id': 'selalu', 'label:es': 'siempre', 'label:pt': 'sempre', 'label:fr': 'toujours', 'label:it': 'sempre', 'label:de': 'stets', 'label:pl': 'zawsze', 'label:ua': 'завжди', }, { label: 'on hover', value: 'onhover', selected: true, 'label:zh': '悬停时', 'label:ja': 'ホバー時に', 'label:ko': '호버에', 'label:id': 'saat melayang', 'label:es': 'en vuelo estacionario', 'label:pt': 'pairando', 'label:fr': 'en vol stationnaire', 'label:it': 'quando in bilico', 'label:de': 'auf schweben', 'label:pl': 'przy najechaniu', 'label:ua': 'при наведенні', }, { label: 'disable', value: false, }, ], }, } }); window.nova_plugins.push({ id: 'square-avatars', title: 'Square avatars', 'title:zh': '方形头像', 'title:ja': '正方形のアバター', 'title:ko': '정사각형 아바타', 'title:id': 'Avatar persegi', 'title:es': 'Avatares cuadrados', 'title:pt': 'Avatares quadrados', 'title:fr': 'Avatars carrés', 'title:it': 'Avatar quadrati', 'title:de': 'Quadratische Avatare', 'title:pl': 'Kwadratowe awatary', 'title:ua': 'Квадратні аватарки', run_on_pages: '*, -live_chat', section: 'comments', desc: 'Make user images squared', 'desc:zh': '方形用户形象', 'desc:ja': 'ユーザー画像を二乗する', 'desc:ko': '사용자 이미지를 정사각형으로 만들기', 'desc:id': 'Buat gambar pengguna menjadi persegi', 'desc:pt': 'Torne as imagens do usuário quadradas', 'desc:fr': 'Rendre les images utilisateur au carré', 'desc:it': 'Rendi le immagini degli utenti quadrate', 'desc:de': 'Machen Sie Benutzerbilder quadriert', 'desc:pl': 'Awatary użytkowniów będą kwadratowe', 'desc:ua': 'Зробіть зображення користувачів квадратними', _runtime: user_settings => { NOVA.css.push( [ 'yt-img-shadow', '.ytp-title-channel-logo', '#player .ytp-title-channel', 'ytm-profile-icon', '#ytd-player.ytd-watch-flexy', 'a.ytd-thumbnail', ] .join(',\n') + ` { border-radius: 0 !important; } html { --yt-button-border-radius: 0; }`); NOVA.waitUntil(() => { if (window.yt && (obj = yt?.config_?.EXPERIMENT_FLAGS) && Object.keys(obj).length) { yt.config_.EXPERIMENT_FLAGS.web_rounded_thumbnails = false; return true; } }); }, }); window.nova_plugins.push({ id: 'comments-visibility', title: 'Collapse comments section', 'title:zh': '收起评论区', 'title:ja': 'コメント欄を折りたたむ', 'title:ko': '댓글 섹션 축소', 'title:id': 'Ciutkan bagian komentar', 'title:es': 'Ocultar sección de comentarios', 'title:pt': 'Recolher seção de comentários', 'title:fr': 'Réduire la section des commentaires', 'title:it': 'Comprimi la sezione commenti', 'title:de': 'Kommentarbereich minimieren', 'title:pl': 'Zwiń sekcję komentarzy', 'title:ua': 'Згорнути розділ коментарів', run_on_pages: 'watch, -mobile', restart_on_location_change: true, section: 'comments', _runtime: user_settings => { NOVA.collapseElement({ selector: '#comments', label: 'comments', remove: (user_settings.comments_visibility_mode == 'disable') ? true : false, }); }, options: { comments_visibility_mode: { _tagName: 'select', label: 'Mode', 'label:zh': '模式', 'label:ja': 'モード', 'label:ko': '방법', 'label:es': 'Modo', 'label:pt': 'Modo', 'label:it': 'Modalità', 'label:de': 'Modus', 'label:pl': 'Tryb', 'label:ua': 'Режим', options: [ { label: 'collapse', value: 'hide', selected: true, 'label:pl': 'zwiń', 'label:ua': 'сховати', }, { label: 'remove', value: 'disable', 'label:pl': 'usuń', 'label:ua': 'усунути', }, ], }, } }); window.nova_plugins.push({ id: 'metadata-hide', title: 'Hide metadata', 'title:ua': 'Приховати метадані', run_on_pages: 'watch', section: 'details', desc: 'Cover link to games, movies, etc.', 'desc:ua': 'Посилання на ігри, фільми тощо.', _runtime: user_settings => { NOVA.css.push( `ytd-watch-metadata > ytd-metadata-row-container-renderer { display: none; }`); }, }); window.nova_plugins.push({ id: 'description-expand', title: 'Expand description', 'title:zh': '展开说明', 'title:ja': '説明を展開', 'title:ko': '설명 펼치기', 'title:id': 'Perluas deskripsi', 'title:es': 'Ampliar descripción', 'title:pt': 'Expandir descrição', 'title:fr': 'Développer la description', 'title:it': 'Espandi la descrizione', 'title:de': 'Beschreibung erweitern', 'title:pl': 'Rozwiń opis', 'title:ua': 'Розширити опис', run_on_pages: 'watch, -mobile', section: 'details', desc: 'on hover', 'data-conflict': 'description-popup, comments-sidebar-position-exchange', _runtime: user_settings => { if (user_settings['description-popup']) return; if (user_settings['comments-sidebar-position-exchange']) return; const SELECTOR_BTN = '[description-collapsed] #description #expand'; switch (user_settings.description_expand_mode) { case 'onhover': NOVA.waitSelector(SELECTOR_BTN) .then(btn => btn.addEventListener('mouseenter', btn.click)); break; case 'always': document.addEventListener('yt-page-data-updated', () => { document.body.querySelector(SELECTOR_BTN)?.click(); }); break; } }, options: { description_expand_mode: { _tagName: 'select', label: 'Mode', 'label:zh': '模式', 'label:ja': 'モード', 'label:ko': '방법', 'label:es': 'Modo', 'label:pt': 'Modo', 'label:it': 'Modalità', 'label:de': 'Modus', 'label:pl': 'Tryb', 'label:ua': 'Режим', options: [ { label: 'always', value: 'always', selected: true, 'label:zh': '每次', 'label:ja': 'いつも', 'label:ko': '언제나', 'label:id': 'selalu', 'label:es': 'siempre', 'label:pt': 'sempre', 'label:fr': 'toujours', 'label:it': 'sempre', 'label:de': 'stets', 'label:pl': 'zawsze', 'label:ua': 'завжди', }, { label: 'on hover', value: 'onhover', 'label:zh': '悬停时', 'label:ja': 'ホバー時に', 'label:ko': '호버에', 'label:id': 'saat melayang', 'label:es': 'en vuelo estacionario', 'label:pt': 'pairando', 'label:fr': 'en vol stationnaire', 'label:it': 'quando in bilico', 'label:de': 'auf schweben', 'label:pl': 'po najechaniu', 'label:ua': 'при наведенні', }, ], }, } }); window.nova_plugins.push({ id: 'description-timestamps-scroll', title: 'No scroll to top when clicking timestamps', 'title:zh': '没有在时间戳上滚动到播放器', 'title:ja': 'タイムスタンプでプレーヤーにスクロールしない', 'title:ko': '타임스탬프에서 플레이어로 스크롤하지 않음', 'title:id': 'Tidak ada gulir ke pemain pada stempel waktu', 'title:es': 'Sin desplazamiento al jugador en marcas de tiempo', 'title:pt': 'Sem rolar para o jogador em timestamps', 'title:fr': 'Pas de défilement vers le joueur sur les horodatages', 'title:it': 'Nessun passaggio al giocatore sui timestamp', 'title:de': 'Kein Scrollen zum Player bei Zeitstempeln', 'title:pl': 'Brak przejścia do odtwarzacza na znacznikach czasu', 'title:ua': 'Немає прокрутки до відтворювача на часових мітках', run_on_pages: 'watch, -mobile', section: 'details', desc: 'Disable scrolling to player when clicking on timestamps', 'desc:pl': 'Wyłącza przewijanie do odtwarzacza podczas klikania znaczników czasu', 'desc:ua': 'Вимикає прокрутку до відтворювача при натисканні на часову мітку', _runtime: user_settings => { document.addEventListener('click', evt => { if (!evt.isTrusted || !evt.target.matches('a[href*="&t="]')) return; if (sec = parseInt(NOVA.queryURL.get('t', evt.target.href))) { evt.preventDefault(); evt.stopPropagation(); evt.stopImmediatePropagation(); movie_player.seekTo(sec); } }, { capture: true }); }, }); window.nova_plugins.push({ id: 'redirect-disable', title: 'Clear links from redirect', 'title:zh': '清除重定向中的链接', 'title:ja': 'リダイレクトからリンクをクリアする', 'title:ko': '리디렉션에서 링크 지우기', 'title:id': 'Hapus tautan dari pengalihan', 'title:es': 'Borrar enlaces de redireccionamientos', 'title:pt': 'Limpar links de redirecionamentos', 'title:fr': 'Effacer les liens des redirections', 'title:it': 'Cancella i collegamenti dal reindirizzamento', 'title:de': 'Links aus Weiterleitungen löschen', 'title:pl': 'Wyczyść linki z przekierowań', 'title:ua': 'Очистити посилання від перенаправлення', run_on_pages: 'watch, channel', section: 'details', desc: 'Direct external links', 'desc:zh': '直接链接到外部站点', 'desc:ja': '外部サイトへの直接リンク', 'desc:ko': '직접 외부 링크', 'desc:id': 'Tautan eksternal langsung', 'desc:es': 'Enlaces externos directos', 'desc:pt': 'Links externos diretos', 'desc:fr': 'Liens externes directs', 'desc:it': 'Collegamenti esterni diretti', 'desc:de': 'Direkte externe Links', 'desc:pl': 'Bezpośrednie łącza zewnętrzne', 'desc:ua': 'Прямі зовнішні посилання', _runtime: user_settings => { document.addEventListener('click', evt => evt.isTrusted && patchLink(evt.target), { capture: true }); document.addEventListener('auxclick', evt => evt.isTrusted && evt.button === 1 && patchLink(evt.target), { capture: true }); function patchLink(target = required()) { const linkSelector = 'a[href*="/redirect?"]'; if (!target.matches(linkSelector)) { if (!(target = target.closest(linkSelector))) return; } if (q = NOVA.queryURL.get('q', target.href)) { target.href = decodeURIComponent(q); } } }, }); window.nova_plugins.push({ id: 'details-buttons', title: 'Buttons', run_on_pages: 'watch, -mobile', section: 'details', _runtime: user_settings => { if (user_settings.details_buttons_hide?.includes('subscribe')) { stylesList.push('#columns #subscribe-button'); } if (user_settings.details_buttons_hide?.includes('all')) { return NOVA.css.push( `ytd-watch-metadata #actions button { display: none !important; }`); } let styles = ''; if (user_settings.details_button_no_labels) { styles += `ytd-watch-metadata #actions button [class*=text] { display: none; } ytd-watch-metadata #actions button .yt-spec-button-shape-next__icon { margin: 0 !important; } ytd-watch-metadata #actions ytd-segmented-like-dislike-button-renderer ~ * button, ytd-watch-metadata #actions #top-level-buttons-computed ~ * button.yt-spec-button-shape-next--size-m { padding: 0 7px; }`; } if (+user_settings.details_button_no_labels_opacity) { styles += `#subscribe-button:not(:hover), ytd-watch-metadata #actions #menu:not(:hover) { transition: opacity .2s ease-in-out; opacity: ${user_settings.details_button_no_labels_opacity || .1}; }`; } if (styles) { NOVA.css.push(styles); } if (user_settings.details_buttons_hide?.length) { const buttonSelectors = [ 'ytd-watch-metadata #menu ytd-button-renderer', 'ytd-watch-metadata #menu button', 'ytd-popup-container ytd-menu-service-item-renderer', ]; let stylesList = []; if (user_settings.details_buttons_hide.includes('join')) { stylesList.push('#sponsor-button'); } if (user_settings.details_buttons_hide.includes('like_dislike')) { stylesList.push('ytd-watch-metadata #menu ytd-segmented-like-dislike-button-renderer'); } if (user_settings.details_buttons_hide.includes('dislike')) { stylesList.push('ytd-watch-metadata #menu #segmented-dislike-button, .yt-spec-button-shape-next--segmented-start::after'); NOVA.css.push( `ytd-watch-metadata #menu ytd-segmented-like-dislike-button-renderer button { border-radius: 100%; width: 40px; border: 0; }`); } if (user_settings.details_buttons_hide.includes('download')) { stylesList.push('ytd-watch-metadata #menu ytd-download-button-renderer'); } if (user_settings.details_buttons_hide.includes('share')) { stylesList.push(buttonSelectors.map(e => `\n${e}:has(path[d^="M15 5.63 20.66"])`)); } if (user_settings.details_buttons_hide.includes('thanks')) { stylesList.push(buttonSelectors.map(e => `\n${e}:has(path[d^="M11 17h2v-1h1c.55"])`)); } if (user_settings.details_buttons_hide.includes('clip')) { stylesList.push(buttonSelectors.map(e => `\n${e}:has(path[d^="M8 7c0 .55-.45"])`)); } if (user_settings.details_buttons_hide.includes('save')) { stylesList.push(buttonSelectors.map(e => `\n${e}:has(path[d$="M2,16h8v-1H2V16z"])`)); } if (user_settings.details_buttons_hide.includes('report')) { stylesList.push(buttonSelectors.map(e => `\n${e}:has(path[d$="L14,3z"])`)); } if (user_settings.details_buttons_hide.includes('transcript')) { stylesList.push(buttonSelectors.map(e => `\n${e}:has(path[d^="M5,11h2v2H5V11z"])`)); } if (stylesList.length) { NOVA.css.push( stylesList.join(',\n') + ` { display: none !important; }`); } } }, options: { details_button_no_labels: { _tagName: 'input', label: 'Buttons without labels', 'label:zh': '没有标签的按钮', 'label:ja': 'ラベルのないボタン', 'label:ko': '라벨이 없는 버튼', 'label:id': 'Tombol tanpa label', 'label:es': 'Botones sin etiquetas', 'label:pt': 'Botões sem rótulos', 'label:fr': 'Boutons sans étiquettes', 'label:it': 'Bottoni senza etichette', 'label:de': 'Knöpfe ohne Beschriftung', 'label:pl': 'Guziki bez etykiet', 'label:ua': 'Кнопки без написів', type: 'checkbox', title: 'Requires support for css tag ":has()"', }, details_button_no_labels_opacity: { _tagName: 'input', label: 'Opacity', 'label:zh': '不透明度', 'label:ja': '不透明度', 'label:ko': '불투명', 'label:id': 'Kegelapan', 'label:es': 'Opacidad', 'label:pt': 'Opacidade', 'label:fr': 'Opacité', 'label:it': 'Opacità', 'label:de': 'Opazität', 'label:pl': 'Przejrzystość', 'label:ua': 'Прозорість', type: 'number', title: '0 - disable', placeholder: '0-1', step: .05, min: 0, max: 1, value: .1, }, details_buttons_hide: { _tagName: 'select', label: 'Hide items', title: '[Ctrl+Click] to select several', 'title:zh': '[Ctrl+Click] 选择多个', 'title:ja': '「Ctrl+Click」して、いくつかを選択します', 'title:ko': '[Ctrl+Click] 여러 선택', 'title:id': '[Ctrl+Klik] untuk memilih beberapa', 'title:es': '[Ctrl+Click] para seleccionar varias', 'title:pt': '[Ctrl+Click] para selecionar vários', 'title:fr': '[Ctrl+Click] pour sélectionner plusieurs', 'title:it': '[Ctrl+Clic] per selezionarne diversi', 'title:de': '[Ctrl+Click] um mehrere auszuwählen', 'title:pl': 'Ctrl+kliknięcie, aby zaznaczyć kilka', 'title:ua': '[Ctrl+Click] щоб обрати декілька', multiple: null, size: 8, options: [ { label: 'subscribe', value: 'subscribe', }, { label: 'all (below)', value: 'all', }, { label: 'join', value: 'join', }, { label: 'like/dislike', value: 'like_dislike', }, { label: 'dislike', value: 'dislike', }, { label: 'share', value: 'share', }, { label: 'clip', value: 'clip', }, { label: 'save', value: 'save', }, { label: 'download', value: 'download', }, { label: 'thanks', value: 'thanks', }, { label: 'report', value: 'report', }, { label: 'transcript', value: 'transcript', }, ], }, } }); window.nova_plugins.push({ id: 'description-popup', title: 'Description section in popup', 'title:zh': '弹出窗口中的描述部分', 'title:ja': 'ポップアップの説明セクション', 'title:ko': '팝업의 설명 섹션', 'title:id': 'Bagian deskripsi dalam popup', 'title:es': 'Sección de descripción en ventana emergente', 'title:pt': 'Seção de descrição no pop-up', 'title:fr': 'Section de description dans la fenêtre contextuelle', 'title:it': 'Sezione Descrizione nel popup', 'title:de': 'Beschreibungsabschnitt im Popup', 'title:pl': 'Opis w osobnym oknie', 'title:ua': 'Розділ опису у спливаючому вікні', run_on_pages: 'watch, -mobile', section: 'details', _runtime: user_settings => { const DESCRIPTION_SELECTOR = 'html:not(:fullscreen) ytd-watch-metadata #description.ytd-watch-metadata:not([hidden]):not(:empty)', DATE_SELECTOR_ID = 'nova-description-date'; NOVA.waitSelector('#masthead-container') .then(masthead => { NOVA.css.push( `${DESCRIPTION_SELECTOR}, ${DESCRIPTION_SELECTOR}:before { position: fixed; top: ${masthead.offsetHeight || 56}px; right: 0; z-index: ${1 + Math.max(getComputedStyle(masthead || movie_player)['z-index'], 601)}; } ${DESCRIPTION_SELECTOR}:not(:hover):before { content: "info ▼"; cursor: pointer; visibility: visible; right: 12.5em; padding: 0 8px 2px; line-height: normal; font-family: Roboto, Arial, sans-serif; font-size: 11px; color: #eee; background: rgba(0,0,0,0.3); } ${DESCRIPTION_SELECTOR} { margin: 0 1%; overflow-y: auto; max-height: 88vh; max-width: 55%; background-color: var(--yt-spec-brand-background-primary); background-color: var(--yt-spec-menu-background); background-color: var(--yt-spec-raised-background); color: var(--yt-spec-text-primary);; border: 1px solid #333; border-radius: 0 !important; } ${DESCRIPTION_SELECTOR}:not(:hover) { visibility: collapse; overflow: hidden; } ${DESCRIPTION_SELECTOR}:hover { visibility: visible !important; } ${DESCRIPTION_SELECTOR}::-webkit-scrollbar { height: 8px; width: 10px; } ${DESCRIPTION_SELECTOR}::-webkit-scrollbar-button { height: 0; width: 0; } ${DESCRIPTION_SELECTOR}::-webkit-scrollbar-corner { background: transparent; } ${DESCRIPTION_SELECTOR}::-webkit-scrollbar-thumb { background: #e1e1e1; border: 0; border-radius: 0; } ${DESCRIPTION_SELECTOR}::-webkit-scrollbar-track { background: #666; border: 0; border-radius: 0; } ${DESCRIPTION_SELECTOR}::-webkit-scrollbar-track:hover { background: #666; }`); }); NOVA.runOnPageInitOrTransition(() => (NOVA.currentPage == 'watch') && restoreDateLine()); NOVA.waitSelector(DESCRIPTION_SELECTOR) .then(descriptionEl => { descriptionEl.addEventListener('mouseenter', evt => { document.body.querySelector('#meta [collapsed] #more, [description-collapsed] #description #expand') ?.click(); }); }); let oldDateText; function restoreDateLine() { NOVA.waitSelector('#title h1') .then(container => { NOVA.waitSelector('ytd-watch-metadata #description.ytd-watch-metadata') .then(async textDateEl => { await NOVA.waitUntil(() => { if ((text = [...textDateEl.querySelectorAll('span.bold.yt-formatted-string:not(:empty)')] .map(e => e.textContent) ?.join('').trim() ) && text != oldDateText ) { oldDateText = text; insertToHTML({ 'text': oldDateText, 'container': container }); return true; } }, 1000); }); }); function insertToHTML({ text = '', container = required() }) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); (document.getElementById(DATE_SELECTOR_ID) || (function () { container.insertAdjacentHTML('afterend', `<span id="${DATE_SELECTOR_ID}" class="style-scope yt-formatted-string bold" style="font-size: 1.35rem; line-height: 2rem; font-weight:400;">${text}</span>`); return document.getElementById(DATE_SELECTOR_ID); })()) .textContent = text; } } }, }); window.nova_plugins.push({ id: 'save-to-playlist', title: 'Add sort/filter to "Save to playlist" menu', 'title:zh': '将排序/过滤器添加到“保存到播放列表”菜单', 'title:ja': '「プレイリストに保存」メニューにソート/フィルターを追加', 'title:ko': '"재생 목록에 저장" 메뉴에 정렬/필터 추가', 'title:id': 'Tambahkan sortir/filter ke menu "Simpan ke daftar putar".', 'title:es': 'Agregar ordenar/filtrar al menú "Guardar en lista de reproducción"', 'title:pt': 'Adicionar classificação/filtro ao menu "Salvar na lista de reprodução"', 'title:fr': 'Ajouter un tri/filtre au menu "Enregistrer dans la liste de lecture"', 'title:it': 'Aggiungi ordinamento/filtro al menu "Salva nella playlist".', 'title:de': 'Sortieren/Filtern zum Menü „In Wiedergabeliste speichern“ hinzufügen', 'title:pl': 'Dodaj sortowanie/filtr do menu „Zapisz na liście odtwarzania”.', 'title:ua': 'Додати сортування/фільтр до меню "Зберегти до плейлиста"', run_on_pages: 'home, feed, channel, results, watch, -mobile', section: 'details', _runtime: user_settings => { NOVA.waitSelector('tp-yt-paper-dialog #playlists') .then(playlists => { const container = playlists.closest('tp-yt-paper-dialog'); new IntersectionObserver(([entry]) => { const searchInput = container.querySelector('input[type=search]') if (entry.isIntersecting) { if (user_settings.save_to_playlist_sort) sortPlaylistsMenu(playlists); if (!searchInput) insertFilterInput(playlists); } else if (searchInput) { searchInput.value = ''; searchInput.dispatchEvent(new Event('change')); } }) .observe(container); }); function sortPlaylistsMenu(playlists = required()) { if (!(playlists instanceof HTMLElement)) return console.error('playlists not HTMLElement:', playlists); playlists.append( ...Array.from(playlists.childNodes) .sort(sortByLabel) ); function sortByLabel(a, b) { const getLabel = el => el.innerText.trim(); return stringLocaleCompare(getLabel(a), getLabel(b)); function stringLocaleCompare(a = required(), b = required()) { return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }); } } } function insertFilterInput(container = required()) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); const searchInput = document.createElement('input'); searchInput.setAttribute('type', 'search'); searchInput.setAttribute('placeholder', 'Playlist Filter'); Object.assign(searchInput.style, { padding: '.4em .6em', border: 0, outline: 0, width: '100%', 'margin-bottom': '1.5em', height: '2.5em', color: 'var(--ytd-searchbox-text-color)', 'background-color': 'var(--ytd-searchbox-background)', }); ['change', 'keyup'].forEach(evt => { searchInput .addEventListener(evt, function () { NOVA.searchFilterHTML({ 'keyword': this.value, 'filter_selectors': '#playlists #checkbox', 'highlight_selector': '#label', }); }); searchInput .addEventListener('click', () => { searchInput.value = ''; searchInput.dispatchEvent(new Event('change')); }); }); container.prepend(searchInput); }; }, options: { save_to_playlist_sort: { _tagName: 'input', label: 'Default sorting alphabetically', 'label:zh': '默认按字母顺序排序', 'label:ja': 'デフォルトのアルファベット順のソート', 'label:ko': '알파벳순 기본 정렬', 'label:id': 'Penyortiran default menurut abjad', 'label:es': 'Clasificación predeterminada alfabéticamente', 'label:pt': 'Classificação padrão em ordem alfabética', 'label:fr': 'Tri par défaut par ordre alphabétique', 'label:it': 'Ordinamento predefinito in ordine alfabetico', 'label:de': 'Standardsortierung alphabetisch', 'label:pl': 'Domyślne sortowanie alfabetyczne', 'label:ua': 'Сортування за замовчуванням за алфавітом', type: 'checkbox', }, } }); window.nova_plugins.push({ id: 'channel-videos-count', title: 'Show channel videos count', 'title:zh': '显示频道上的视频数量', 'title:ja': 'チャンネルの動画数を表示する', 'title:ko': '채널 동영상 수 표시', 'title:id': 'Tampilkan jumlah video saluran', 'title:es': 'Mostrar recuento de videos del canal', 'title:pt': 'Mostrar contagem de vídeos do canal', 'title:fr': 'Afficher le nombre de vidéos de la chaîne', 'title:it': 'Mostra il conteggio dei video del canale', 'title:de': 'Anzahl der Kanalvideos anzeigen', 'title:pl': 'Pokaż liczbę filmów na kanale', 'title:ua': 'Показати кількість відео на каналі', run_on_pages: 'watch, -mobile', restart_on_location_change: true, section: 'details', opt_api_key_warn: true, desc: 'Display uploaded videos on channel', 'desc:zh': '在频道上显示上传的视频', 'desc:ja': 'アップロードした動画をチャンネルに表示', 'desc:ko': '채널에 업로드된 동영상 표시', 'desc:id': 'Tampilkan video yang diunggah di saluran', 'desc:es': 'Mostrar videos subidos en el canal', 'desc:pt': 'Exibir vídeos enviados no canal', 'desc:fr': 'Afficher les vidéos mises en ligne sur la chaîne', 'desc:it': 'Visualizza i video caricati sul canale', 'desc:de': 'Hochgeladene Videos auf dem Kanal anzeigen', 'desc:pl': 'Wyświetla przesłane filmy na kanale', 'desc:ua': 'Показує завантажені відео на каналі', _runtime: user_settings => { const CACHE_PREFIX = 'nova-channel-videos-count:', SELECTOR_ID = 'nova-video-count'; NOVA.waitSelector('#upload-info #owner-sub-count, ytm-slim-owner-renderer .subhead', { destroy_if_url_changes: true }) .then(el => setVideoCount(el)); async function setVideoCount(container = required()) { await NOVA.delay(500); const channelId = NOVA.getChannelId(); if (!channelId) return console.error('setVideoCount channelId: empty', channelId); if (storage = sessionStorage.getItem(CACHE_PREFIX + channelId)) { insertToHTML({ 'text': storage, 'container': container }); } else { NOVA.request.API({ request: 'channels', params: { 'id': channelId, 'part': 'statistics' }, api_key: user_settings['user-api-key'], }) .then(res => { if (res?.error) return alert(`Error [${res.code}]: ${res.reason}\n` + res.error); res?.items?.forEach(item => { if (videoCount = NOVA.prettyRoundInt(item.statistics.videoCount)) { insertToHTML({ 'text': videoCount, 'container': container }); sessionStorage.setItem(CACHE_PREFIX + channelId, videoCount); } else console.warn('API is change', item); }); }); } function insertToHTML({ text = '', container = required() }) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); (document.getElementById(SELECTOR_ID) || (function () { container.insertAdjacentHTML('beforeend', `<span class="date style-scope ytd-video-secondary-info-renderer" style="margin-right:5px;"> • <span id="${SELECTOR_ID}">${text}</span> videos</span>`); return document.getElementById(SELECTOR_ID); })()) .textContent = text; container.title = `${text} videos`; } } }, }); window.nova_plugins.push({ id: 'video-date-format', title: 'Displaying date format', run_on_pages: 'watch, -mobile', section: 'details', opt_api_key_warn: true, 'data-conflict': 'description-popup', _runtime: user_settings => { if (user_settings['description-popup']) return const CACHE_PREFIX = 'nova-video-date:', DATE_SELECTOR_ID = 'nova-video-published-date'; NOVA.runOnPageInitOrTransition(() => { if (NOVA.currentPage == 'watch') { NOVA.waitSelector('#title h1', { destroy_if_url_changes: true }) .then(el => setVideoDate(el)); } }); function setVideoDate(container = required()) { const videoId = NOVA.queryURL.get('v') || movie_player.getVideoData().video_id; if (storage = sessionStorage.getItem(CACHE_PREFIX + videoId)) { insertToHTML({ 'text': storage, 'container': container }); } NOVA.request.API({ request: 'videos', params: { 'id': videoId, 'part': 'snippet,liveStreamingDetails' }, api_key: user_settings['user-api-key'], }) .then(res => { if (res?.error) return alert(`Error [${res.code}]: ${res.reason}\n` + res.error); res?.items?.forEach(item => { let innerHTML = ''; if (item.snippet.publishedAt) { innerHTML = NOVA.dateformat.apply(new Date(item.snippet.publishedAt), [user_settings.video_date_format]); } if (item.liveStreamingDetails) { const ACTIVE_LIVE_START = 'Active Livestream since ', ENDED_STREAM_START = `${movie_player.getVideoData().isLive ? 'Livestream' : 'Premiere'} from `, DATETIME_UNTIL_PATTERN = ' until '; if (item.liveStreamingDetails.actualStartTime && item.liveStreamingDetails.actualEndTime) { const timeStart = new Date(item.liveStreamingDetails.actualStartTime), timeEnd = new Date(item.liveStreamingDetails.actualEndTime); innerHTML = ENDED_STREAM_START + NOVA.dateformat.apply(timeStart, [user_settings.video_date_format]); innerHTML += DATETIME_UNTIL_PATTERN + NOVA.dateformat.apply(timeEnd, [ timeStart.getDay() === timeEnd.getDay() ? user_settings.video_date_format.split(' at ')[1] : user_settings.video_date_format ]); } else if (item.liveStreamingDetails.scheduledStartTime) { innerHTML = ACTIVE_LIVE_START + NOVA.dateformat.apply(new Date(item.liveStreamingDetails.scheduledStartTime), [user_settings.video_date_format]); } } if (innerHTML) { insertToHTML({ 'text': innerHTML, 'container': container }); sessionStorage.setItem(CACHE_PREFIX + videoId, innerHTML); } }); }); function insertToHTML({ text = '', container = required() }) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); (document.getElementById(DATE_SELECTOR_ID) || (function () { container.insertAdjacentHTML('afterend', `<span id="${DATE_SELECTOR_ID}" class="style-scope yt-formatted-string bold" style="font-size: 1.35rem; line-height: 2rem; font-weight:400;">${text}</span>`); return document.getElementById(DATE_SELECTOR_ID); })()) .textContent = text; } } }, options: { video_date_format: { _tagName: 'select', label: 'Date pattern', options: [ { label: 'D MMM Y', value: 'D MMM YYYY' }, { label: 'D MMM Y HH:mm:ss', value: 'D MMM YYYY at HH:mm:ss', selected: true }, { label: 'DDD DD/MM/YYYY', value: 'DDD DD/MM/YYYY HH:mm:ss' }, { label: 'DDDD DD/MM/YYYY', value: 'DDDD DD/MM/YYYY HH:mm:ss' }, { label: 'Y/MM/DD', value: 'YYYY/MM/DD' }, { label: 'Y-MM-D', value: 'YYYY-MM-D' }, { label: 'Y.MM.D', value: 'YYYY.MM.D' }, { label: 'MM/DD/Y', value: 'MM/DD/YYYY' }, { label: 'MM/DD/Y HH:mm:ss', value: 'MM/DD/YYYY at HH:mm:ss' }, { label: 'MM-D-Y', value: 'MM-D-YYYY' }, { label: 'MM-D-Y HH:mm:ss', value: 'MM-D-YYYY at HH:mm:ss' }, { label: 'MM.D.Y', value: 'MM.D.YYYY' }, { label: 'MM.D.Y HH:mm:ss', value: 'MM.D.YYYY at HH:mm:ss' }, ], }, } }); window.nova_plugins.push({ id: 'return-dislike', title: 'Show dislike count', run_on_pages: 'watch, -mobile', section: 'details', desc: 'via by returnyoutubedislike.com', _runtime: user_settings => { if (user_settings.details_button_no_labels || user_settings.details_buttons_hide?.includes('like_dislike') ) { return; } const CACHE_PREFIX = 'nova-dislikes-count:', SELECTOR_ID = 'nova-dislikes-count'; NOVA.runOnPageInitOrTransition(() => { if (NOVA.currentPage == 'watch') { NOVA.waitSelector('ytd-watch-metadata #menu #segmented-dislike-button button', { destroy_if_url_changes: true }) .then(el => setDislikeCount(el)); } }); async function setDislikeCount(container = required()) { const videoId = NOVA.queryURL.get('v') || movie_player.getVideoData().video_id; if (!videoId) return console.error('return-dislike videoId: empty', videoId); container.style.width = 'auto'; if (storage = sessionStorage.getItem(CACHE_PREFIX + videoId)) { insertToHTML({ 'text': storage, 'container': container }); } else if (dislikeCount = await getDislikeCount()) { insertToHTML({ 'text': dislikeCount, 'container': container }); } async function getDislikeCount() { const videoId = NOVA.queryURL.get('v') || movie_player.getVideoData().video_id; const fetchAPI = () => fetch(`https://returnyoutubedislikeapi.com/votes?videoId=${videoId}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } } ) .then(response => response.json()) .then(json => json.dislikes) .catch(error => { }); if (result = await fetchAPI()) { sessionStorage.setItem(CACHE_PREFIX + videoId, JSON.stringify(result)); return result; } } function insertToHTML({ text = '', container = required() }) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); (document.getElementById(SELECTOR_ID) || (function () { container.insertAdjacentHTML('beforeend', `<span id="${SELECTOR_ID}" style="text-overflow:ellipsis; overflow:visible; white-space:nowrap; padding-left:3px;">${text}</span>`); return document.getElementById(SELECTOR_ID); })()) .textContent = text; container.title = text; } } }, }); window.nova_plugins.push({ id: 'page-logo', title: 'YouTube logo link', 'title:zh': 'YouTube 徽标', 'title:ja': 'YouTubeロゴ', 'title:ko': '유튜브 로고', 'title:ua': 'YouTube лого', run_on_pages: '*, -embed, -mobile, -live_chat', section: 'header', _runtime: user_settings => { NOVA.waitSelector('#masthead a#logo', { destroy_if_url_changes: true }) .then(async a => { if (link = new URL(user_settings.page_logo_url_mode)?.href) { a.href = link; await NOVA.waitUntil(() => a.data?.commandMetadata?.webCommandMetadata?.url, 1500); a.data.commandMetadata.webCommandMetadata.url = link; } }); }, options: { page_logo_url_mode: { _tagName: 'input', label: 'URL', type: 'url', pattern: "https://.*", placeholder: 'https://youtube.com/...', value: 'https://youtube.com/feed/subscriptions', }, } }); window.nova_plugins.push({ id: 'search-query', title: 'Search filter', 'title:zh': '搜索过滤器', 'title:ja': '検索フィルター', 'title:ko': '검색 필터', 'title:id': 'Filter pencarian', 'title:es': 'Filtros de búsqueda', 'title:pt': 'Filtros de pesquisa', 'title:fr': 'Filtres de recherche', 'title:it': 'Filtri di ricerca', 'title:de': 'Suchfilter', 'title:pl': 'Filtry wyszukiwania', 'title:ua': 'Фільтр пошуку', run_on_pages: 'results', restart_on_location_change: true, section: 'header', _runtime: user_settings => { if (!NOVA.queryURL.has('sp') && (sp = user_settings.search_query_date || user_settings.search_query_sort) ) { location.href = NOVA.queryURL.set({ 'sp': sp }); } }, options: { search_query_sort: { _tagName: 'select', label: 'Sort by', 'label:zh': '排序方式', 'label:ja': '並び替え', 'label:ko': '정렬 기준', 'label:id': 'Sortir dengan', 'label:es': 'Ordenar por', 'label:pt': 'Ordenar por', 'label:fr': 'Trier par', 'label:it': 'Ordina per', 'label:de': 'Sortieren nach', 'label:pl': 'Sortuj według', 'label:ua': 'Сортувати за', options: [ { label: 'relevance', value: false, selected: true, 'label:ua': 'актуальність', }, { label: 'upload date', value: 'cai%253d', 'label:ua': 'дата завантаження', }, { label: 'view count', value: 'cam%253d', 'label:ua': 'кількість переглядів', }, { label: 'rating', value: 'cae%253d', 'label:ua': 'вподобайки', }, ], 'data-dependent': { 'search_query_date': false }, }, search_query_date: { _tagName: 'select', label: 'Upload date', 'label:zh': '上传日期', 'label:ja': 'アップロード日', 'label:ko': '업로드 날짜', 'label:id': 'Tanggal unggah', 'label:es': 'Fecha de carga', 'label:pt': 'data de upload', 'label:fr': 'Date de dépôt', 'label:it': 'data di caricamento', 'label:de': 'Datum des Hochladens', 'label:pl': 'Data przesłania', 'label:ua': 'Дата завантаження', options: [ { label: 'all time', value: false, selected: true, 'label:ua': 'за увесь час', }, { label: 'last hour', value: 'egiiaq%253d%253d', 'label:ua': 'за останню годину', }, { label: 'today', value: 'egiiag%253d%253d', 'label:ua': 'сьогодні', }, { label: 'this week', value: 'egiiaw%253d%253d', 'label:ua': 'цього тижня', }, { label: 'this month', value: 'egiiba%253d%253d', 'label:ua': 'цього місяця', }, { label: 'this year', value: 'egiibq%253d%253d', 'label:ua': 'цього року', }, ], 'data-dependent': { 'search_query_sort': false }, }, } }); window.nova_plugins.push({ id: 'header-compact', title: 'Header compact', 'title:zh': '标题紧凑', 'title:ja': 'ヘッダーコンパクト', 'title:ko': '헤더 컴팩트', 'title:id': 'Kompak tajuk', 'title:es': 'Encabezado compacto', 'title:pt': 'Cabeçalho compacto', 'title:fr': 'En-tête compact', 'title:it': 'Testata compatta', 'title:de': 'Header kompakt', 'title:pl': 'Kompaktowy nagłówek', 'title:ua': 'Компактна шапка сайту', run_on_pages: '*, -embed, -mobile, -live_chat', section: 'header', _runtime: user_settings => { const height = '36px'; NOVA.css.push( `#masthead #container.ytd-masthead { max-height: ${height} !important; } #masthead #background { max-height: ${height} !important; } #search-form, #search-icon-legacy { max-height: ${height} !important; } body, html:not(:fullscreen) #page-manager { --ytd-masthead-height: ${height}; } #chips-wrapper.ytd-feed-filter-chip-bar-renderer { --ytd-rich-grid-chips-bar-top: ${height}; }`); }, }); window.nova_plugins.push({ id: 'header-unfixed', title: 'Header unpinned', 'title:zh': '标题未固定', 'title:ja': 'ヘッダーは固定されていません', 'title:ko': '헤더가 고정되지 않음', 'title:id': 'Tajuk tidak diperbaiki', 'title:es': 'Encabezado sin arreglar', 'title:pt': 'Cabeçalho não corrigido', 'title:fr': 'En-tête non corrigé', 'title:it': 'Intestazione non fissata', 'title:de': 'Kopfleiste nicht fixiert', 'title:pl': 'Przewijany nagłówek', 'title:ua': 'Відкріпити шапку сайту', run_on_pages: '*, -embed, -mobile, -live_chat', section: 'header', desc: 'Prevent header from sticking', 'desc:zh': '防止头部粘连', 'desc:ja': 'ヘッダーがくっつくのを防ぎます', 'desc:ko': '헤더가 달라붙는 것을 방지', 'desc:id': 'Mencegah header menempel', 'desc:es': 'Evita que el cabezal se pegue', 'desc:pt': 'Impede que o cabeçalho grude', 'desc:fr': "Empêcher l'en-tête de coller", 'desc:it': "Impedisci che l'intestazione si attacchi", 'desc:de': 'Verhindert das Ankleben des Headers', 'desc:pl': 'Nagłówek będzie przewijany wraz ze stroną', 'desc:ua': 'Відкріпляє шапку при прокрутці сайту', _runtime: user_settings => { const CLASS_NAME_TOGGLE = 'nova-header-unfixed', SELECTOR = 'html.' + CLASS_NAME_TOGGLE; NOVA.css.push( `${SELECTOR} #masthead-container { position: absolute !important; } ${SELECTOR} #chips-wrapper { position: sticky !important; } ${SELECTOR} #header { margin-top: 0 !important; }`); document.documentElement.classList.add(CLASS_NAME_TOGGLE); if (user_settings.header_unfixed_hotkey) { const hotkey = user_settings.header_unfixed_hotkey || 'v'; document.addEventListener('keyup', evt => { if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return; if (evt.key === hotkey) { document.documentElement.classList.toggle(CLASS_NAME_TOGGLE); } }); } if (user_settings.header_unfixed_scroll) { createArrowButton(); document.addEventListener('yt-action', evt => { if (evt.detail?.actionName == 'yt-store-grafted-ve-action' ) { scrollAfter(); } }); function scrollAfter() { if ((masthead = document.getElementById('masthead')) && (topOffset = masthead.offsetHeight) && NOVA.isInViewport(masthead) ) { window.scrollTo({ top: topOffset }); } } function createArrowButton() { const scrollDownButton = document.createElement('button'); scrollDownButton.textContent = '▼'; scrollDownButton.title = 'Scroll down'; Object.assign(scrollDownButton.style, { cursor: 'pointer', background: 'transparent', color: 'deepskyblue', border: 'none', }); scrollDownButton.onclick = scrollAfter; if (endnode = document.getElementById('end')) { endnode.parentElement.insertBefore(scrollDownButton, endnode); } } } }, options: { header_unfixed_scroll: { _tagName: 'input', label: 'Scroll after header', 'label:zh': '在标题后滚动', 'label:ja': 'ヘッダーの後にスクロール', 'label:ko': '헤더 뒤 스크롤', 'label:id': 'Gulir setelah tajuk', 'label:es': 'Desplazarse después del encabezado', 'label:pt': 'Role após o cabeçalho', 'label:fr': "Faire défiler après l'en-tête", 'label:it': "Scorri dopo l'intestazione", 'label:de': 'Nach der Kopfzeile scrollen', 'label:pl': 'Przewiń nagłówek', 'label:ua': 'Прокручувати після шапки сайту', title: 'Makes sense on a small screen', 'title:zh': '在小屏幕上有意义', 'title:ja': '小さな画面で意味があります', 'title:ko': '작은 화면에서 이해하기', 'title:id': 'Masuk akal di layar kecil', 'title:es': 'Tiene sentido en una pantalla pequeña', 'title:pt': 'Faz sentido em uma tela pequena', 'title:fr': 'A du sens sur un petit écran', 'title:it': 'Ha senso su un piccolo schermo', 'title:de': 'Macht auf einem kleinen Bildschirm Sinn', 'title:pl': 'Przydatne na małym ekranie', 'title:ua': 'Ефективно на малому екрані', type: 'checkbox', }, header_unfixed_hotkey: { _tagName: 'select', label: 'Hotkey toggle', 'label:ua': 'Перемикання гарячою клавішею', options: [ { label: 'V', value: 'v', selected: true }, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'w', 'x', 'y', 'z', ']', '[', '+', '-', ',', '.', '/', '<', ';', '\\' ], }, }, }); window.nova_plugins.push({ id: 'subscriptions-home', title: 'Redirect from home page to subscriptions page', run_on_pages: 'home', restart_on_location_change: true, section: 'header', 'data-conflict': 'page-logo', _runtime: user_settings => { location.pathname = '/feed/subscriptions'; }, }); const NOVA = { waitSelector(selector = required(), limit_data) { if (typeof selector !== 'string') return console.error('wait > selector:', typeof selector); if (limit_data?.container && !(limit_data.container instanceof HTMLElement)) return console.error('wait > container not HTMLElement:', limit_data.container); if (selector.includes(':has(') && !CSS.supports('selector(:has(*))')) { return new Promise((resolve, reject) => { console.warn('CSS ":has()" unsupported'); reject('CSS ":has()" unsupported'); }); } return new Promise(resolve => { if (element = (limit_data?.container || document.body || document).querySelector(selector)) { return resolve(element); } const mutationObserver = new MutationObserver((mutationRecordsArray, observer) => { for (const record of mutationRecordsArray) { for (const node of record.addedNodes) { if (![1, 3, 8].includes(node.nodeType) || !(node instanceof HTMLElement)) continue; if (node.matches && node.matches(selector)) { observer.disconnect(); return resolve(node); } else if ( (parentEl = node.parentElement || node) && (parentEl instanceof HTMLElement) && (element = parentEl.querySelector(selector)) ) { observer.disconnect(); return resolve(element); } } } if (document?.readyState != 'loading' && (element = (limit_data?.container || document?.body || document).querySelector(selector)) ) { observer.disconnect(); return resolve(element); } }) mutationObserver .observe(limit_data?.container || document.body || document.documentElement || document, { childList: true, subtree: true, attributes: true, }); if (limit_data?.destroy_if_url_changes) { isURLChange(); window.addEventListener('transitionend', ({ target }) => { if (isURLChange()) { mutationObserver.disconnect(); } }); function isURLChange() { return (this.prevURL === location.href) ? false : this.prevURL = location.href; } } }); }, waitUntil(condition = required(), timeout = 100) { if (typeof condition !== 'function') return console.error('waitUntil > condition is not fn:', typeof condition); return new Promise((resolve) => { if (result = condition()) { resolve(result); } else { const waitCondition = setInterval(() => { if (result = condition()) { clearInterval(waitCondition); resolve(result); } }, timeout); } }); }, delay(ms = 100) { return new Promise(resolve => setTimeout(resolve, ms)); }, watchElements_list: {}, watchElements({ selectors = required(), attr_mark, callback = required() }) { if (!Array.isArray(selectors) && typeof selectors !== 'string') return console.error('watch > selector:', typeof selectors); if (typeof callback !== 'function') return console.error('watch > callback:', typeof callback); this.waitSelector((typeof selectors === 'string') ? selectors : selectors.join(',')) .then(video => { !Array.isArray(selectors) && (selectors = selectors.split(',').map(s => s.trim())); process(); this.watchElements_list[attr_mark] = setInterval(() => document.visibilityState == 'visible' && process(), 1000 * 1.5); function process() { selectors .forEach(selectorItem => { if (selectorItem.includes(':has(') && !CSS.supports('selector(:has(*))')) { return console.warn('CSS ":has()" unsupported'); } if (attr_mark) selectorItem += `:not([${attr_mark}])`; document.body.querySelectorAll(selectorItem) .forEach(el => { if (attr_mark) el.setAttribute(attr_mark, true); callback(el); }); }); } }); }, runOnPageInitOrTransition(callback) { if (!callback || typeof callback !== 'function') { return console.error('runOnPageInitOrTransition > callback not function:', ...arguments); } let prevURL = location.href; const isURLChange = () => (prevURL === location.href) ? false : prevURL = location.href; isURLChange() || callback(); document.addEventListener('yt-navigate-finish', () => isURLChange() && callback()); }, css: { push(css = required(), selector, set_important) { if (typeof css === 'object') { if (!selector) return console.error('injectStyle > empty json-selector:', ...arguments); injectCss(selector + json2css(css)); function json2css(obj) { let css = ''; Object.entries(obj) .forEach(([key, value]) => { css += key + ':' + value + (set_important ? ' !important' : '') + ';'; }); return `{ ${css} }`; } } else if (css && typeof css === 'string') { if (document.head) { injectCss(css); } else { window.addEventListener('load', () => injectCss(css), { capture: true, once: true }); } } else { console.error('addStyle > css:', typeof css); } function injectCss(source = required()) { let sheet; if (source.endsWith('.css')) { sheet = document.createElement('link'); sheet.rel = 'sheet'; sheet.href = source; } else { const sheetId = 'NOVA-style'; sheet = document.getElementById(sheetId) || (function () { const style = document.createElement('style'); style.type = 'text/css'; style.id = sheetId; return (document.head || document.documentElement).appendChild(style); })(); } sheet.textContent += '\n' + source .replace(/\n+\s{2,}/g, ' ') + '\n'; } }, getValue(selector = required(), prop_name = required()) { return (el = (selector instanceof HTMLElement) ? selector : document.body?.querySelector(selector)) ? getComputedStyle(el).getPropertyValue(prop_name) : null; }, }, prettyRoundInt(num) { num = +num; if (num === 0) return ''; if (num < 1000) return num; const sizes = ['', 'K', 'M', 'B']; const i = ~~(Math.log(Math.abs(num)) / Math.log(1000)); if (!sizes[i]) return num; return round(num / 1000 ** i, 1) + sizes[i]; function round(n, precision = 2) { const prec = 10 ** precision; return ~~(n * prec) / prec; } }, isInViewport(el = required()) { if (!(el instanceof HTMLElement)) return console.error('el is not HTMLElement type:', el); if (distance = el.getBoundingClientRect()) { return ( distance.top >= 0 && distance.left >= 0 && distance.bottom <= (window.innerHeight || document.documentElement.clientHeight) && distance.right <= (window.innerWidth || document.documentElement.clientWidth) ); } }, collapseElement({ selector = required(), label = required(), remove }) { const selector_id = `${label.match(/[a-z]+/gi).join('')}-prevent-load-btn`; this.waitSelector(selector.toString()) .then(el => { if (remove) el.remove(); else { if (document.getElementById(selector_id)) return; el.style.display = 'none'; const btn = document.createElement('a'); btn.textContent = `Load ${label}`; btn.id = selector_id; btn.className = 'more-button style-scope ytd-video-secondary-info-renderer'; Object.assign(btn.style, { cursor: 'pointer', 'text-align': 'center', 'text-transform': 'uppercase', display: 'block', color: 'var(--yt-spec-text-secondary)', }); btn.addEventListener('click', () => { btn.remove(); el.style.display = 'unset'; window.dispatchEvent(new Event('scroll')); }); el.before(btn); } }); }, aspectRatio: { sizeToFit({ srcWidth = 0, srcHeight = 0, maxWidth = window.innerWidth, maxHeight = window.innerHeight }) { const aspectRatio = Math.min(+maxWidth / +srcWidth, +maxHeight / +srcHeight); return { width: +srcWidth * aspectRatio, height: +srcHeight * aspectRatio, }; }, getAspectRatio({ width = required(), height = required() }) { const gcd = (a, b) => b ? gcd(b, a % b) : a, divisor = gcd(width, height); return width / divisor + ':' + height / divisor; }, chooseAspectRatio({ width = required(), height = required(), layout }) { const acceptedRatioList = { 'landscape': { '1:1': 1, '3:2': 1.5, '4:3': 1.33333333333, '5:4': 1.25, '5:3': 1.66666666667, '16:9': 1.77777777778, '16:10': 1.6, '17:9': 1.88888888889, '21:9': 2.33333333333, '24:10': 2.4, }, 'portrait': { '1:1': 1, '2:3': .66666666667, '3:4': .75, '3:5': .6, '4:5': .8, '9:16': .5625, '9:17': .5294117647, '9:21': .4285714286, '10:16': .625, }, }; return choiceRatioFromList(this.getAspectRatio(...arguments)) || acceptedRatioList['landscape']['16:9']; function choiceRatioFromList(ratio = required()) { const layout_ = layout || ((ratio < 1) ? 'portrait' : 'landscape'); return acceptedRatioList[layout_][ratio]; } }, calculateHeight: (width = required(), aspectRatio = (16 / 9)) => parseFloat((width / aspectRatio).toFixed(2)), calculateWidth: (height = required(), aspectRatio = (16 / 9)) => parseFloat((height * aspectRatio).toFixed(2)), }, triggerHUD(text) { if (!text) return; if (typeof this.fateBezel === 'number') clearTimeout(this.fateBezel); const bezelEl = document.body.querySelector('.ytp-bezel-text'); if (!bezelEl) return console.warn(`triggerHUD ${text}=>${bezelEl}`); const bezelContainer = bezelEl.parentElement.parentElement, CLASS_VALUE = 'ytp-text-root', SELECTOR = '.' + CLASS_VALUE; if (!this.bezel_css_inited) { this.bezel_css_inited = true; this.css.push( `${SELECTOR} { display: block !important; } ${SELECTOR} .ytp-bezel-text-wrapper { pointer-events: none; z-index: 40 !important; } ${SELECTOR} .ytp-bezel-text { display: inline-block !important; } ${SELECTOR} .ytp-bezel { display: none !important; }`); } bezelEl.textContent = text; bezelContainer.classList.add(CLASS_VALUE); let ms = 1200; if (text.endsWith('%') || text.endsWith('x')) { ms = 600 } this.fateBezel = setTimeout(() => { bezelContainer.classList.remove(CLASS_VALUE); bezelEl.textContent = ''; }, ms); }, getChapterList(video_duration = required()) { switch (NOVA.currentPage) { case 'embed': chapsCollect = getFromAPI(); return chapsCollect; break; case 'watch': if ((chapsCollect = getFromDescriptionText() || getFromDescriptionChaptersBlock()) && chapsCollect.length ) { return chapsCollect; } break; } function descriptionExpand() { document.querySelector('#meta [collapsed] #more, [description-collapsed] #description #expand')?.click(); } function getFromDescriptionText() { descriptionExpand(); const selectorTimestampLink = 'a[href*="&t="]'; let timestampsCollect = [], nowComment, prevSec = -1; [ ( document.body.querySelector('ytd-watch-flexy')?.playerData?.videoDetails?.shortDescription || document.body.querySelector('ytd-watch-metadata #description.ytd-watch-metadata')?.textContent ), //...[...document.body.querySelectorAll(`#comments #comment #comment-content:has(${selectorTimestampLink})`)] ...[...document.body.querySelectorAll(`#comments #comment #comment-content ${selectorTimestampLink} + *:last-child`)] .map(el => ({ 'source': 'comment', 'text': el.closest('#comment-content')?.textContent, })), ] .forEach(data => { if (timestampsCollect.length > 1) return; nowComment = Boolean(data?.source); (data?.text || data) ?.split('\n') .forEach(line => { line = line?.toString().trim(); if (line.length > 5 && line.length < 200 && (timestamp = /((\d?\d:){1,2}\d{2})/g.exec(line))) { timestamp = timestamp[0]; const sec = NOVA.timeFormatTo.hmsToSec(timestamp), timestampPos = line.indexOf(timestamp); if ( (nowComment ? true : (sec > prevSec && sec < +video_duration)) && (timestampPos < 5 || (timestampPos + timestamp.length) === line.length) ) { if (nowComment) prevSec = sec; timestampsCollect.push({ 'sec': sec, 'time': timestamp, 'title': line .replace(timestamp, '') .trim().replace(/^[:\-–—|●►]|(\[\])?[:\-–—.;|]$/g, '').trim() //.trim().replace(/^([:\-–—|]|(\d+[\.)]))|(\[\])?[:\-–—.;|]$/g, '') .trim() }); } } }); }); if (timestampsCollect.length == 1 && timestampsCollect[0].sec < (video_duration / 4)) { return timestampsCollect; } else if (timestampsCollect.length > 1) { if (nowComment) { timestampsCollect = timestampsCollect.sort((a, b) => a.sec - b.sec); } return timestampsCollect; } } function getFromDescriptionChaptersBlock() { descriptionExpand(); const selectorTimestampLink = 'a[href*="&t="]'; let timestampsCollect = []; let prevSec = -1; document.body.querySelectorAll(`#structured-description ${selectorTimestampLink}`) .forEach(chaperLink => { const sec = parseInt(NOVA.queryURL.get('t', chaperLink.href)); if (sec > prevSec) { prevSec = sec; timestampsCollect.push({ 'time': NOVA.timeFormatTo.HMS.digit(sec), 'sec': sec, 'title': chaperLink.textContent.trim().split('\n')[0].trim(), }); } }); if (timestampsCollect.length == 1 && timestampsCollect[0].sec < (video_duration / 4)) { return timestampsCollect; } else if (timestampsCollect.length > 1) { return timestampsCollect; } } function getFromAPI() { if (!window.ytPubsubPubsubInstance) { return console.warn('ytPubsubPubsubInstance is null:', ytPubsubPubsubInstance); } if ((ytPubsubPubsubInstance = ytPubsubPubsubInstance.i || ytPubsubPubsubInstance.j || ytPubsubPubsubInstance.subscriptions_ ) && Array.isArray(ytPubsubPubsubInstance) ) { const data = Object.values( ytPubsubPubsubInstance.find(a => a?.player)?.player.app ) .find(a => a?.videoData) ?.videoData.multiMarkersPlayerBarRenderer; if (data?.markersMap?.length) { return data.markersMap[0].value.chapters ?.map(c => { const sec = +c.chapterRenderer.timeRangeStartMillis / 1000; return { 'sec': sec, 'time': NOVA.timeFormatTo.HMS.digit(sec), 'title': c.chapterRenderer.title.simpleText || c.chapterRenderer.title.runs[0].text, }; }); } } } }, strToArray(str) { return str ?.split(/[\n,;]/) .map(e => e.toString().trim().toLowerCase()) .filter(e => e.length); }, searchFilterHTML({ keyword = required(), filter_selectors = required(), highlight_selector, highlight_class }) { keyword = keyword.toString().toLowerCase(); document.body.querySelectorAll(filter_selectors) .forEach(item => { const text = item.textContent, hasText = text?.toLowerCase().includes(keyword), highlight = el => { if (el.innerHTML.includes('<mark ')) { el.innerHTML = el.innerHTML .replace(/<\/?mark[^>]*>/g, ''); } item.style.display = hasText ? '' : 'none'; if (hasText && keyword) { highlightTerm({ 'target': el, 'keyword': keyword, 'highlightClass': highlight_class, }); } }; (highlight_selector ? item.querySelectorAll(highlight_selector) : [item]) .forEach(highlight); }); function highlightTerm({ target = required(), keyword = required(), highlightClass }) { const content = target.textContent, pattern = new RegExp('(>[^<.]*)?(' + keyword + ')([^<.]*)?', 'gi'), highlightStyle = highlightClass ? `class="${highlightClass}"` : 'style="background-color:#afafaf"', replaceWith = `$1<mark ${highlightStyle}>$2</mark>$3`, marked = content.replaceAll(pattern, replaceWith); return (target.innerHTML = marked) !== content; } }, isMusic() { return checkMusicType(); function checkMusicType() { const channelName = movie_player.getVideoData().author, titleStr = movie_player.getVideoData().title.toUpperCase(), titleWordsList = titleStr?.toUpperCase().match(/\w+/g), playerData = document.body.querySelector('ytd-watch-flexy')?.playerData; return [ titleStr, location.href, channelName, playerData?.microformat?.playerMicroformatRenderer.category, playerData?.title, ] .some(i => i?.toUpperCase().includes('MUSIC')) || document.body.querySelector('#upload-info #channel-name .badge-style-type-verified-artist') || (channelName && /(VEVO|Topic|Records|RECORDS|Recordings|AMV)$/.test(channelName)) || (channelName && /(MUSIC|ROCK|SOUNDS|SONGS)/.test(channelName.toUpperCase())) || titleWordsList?.length && ['🎵', '♫', 'SONG', 'SONGS', 'SOUNDTRACK', 'LYRIC', 'LYRICS', 'AMBIENT', 'MIX', 'VEVO', 'CLIP', 'KARAOKE', 'OPENING', 'COVER', 'COVERED', 'VOCAL', 'INSTRUMENTAL', 'ORCHESTRAL', 'DJ', 'DNB', 'BASS', 'BEAT', 'HITS', 'ALBUM', 'PLAYLIST', 'DUBSTEP', 'CHILL', 'RELAX', 'CLASSIC', 'CINEMATIC'] .some(i => titleWordsList.includes(i)) || ['OFFICIAL VIDEO', 'OFFICIAL AUDIO', 'FEAT.', 'FT.', 'LIVE RADIO', 'DANCE VER', 'HIP HOP', 'ROCK N ROLL', 'HOUR VER', 'HOURS VER', 'INTRO THEME'] .some(i => titleStr.includes(i)) || titleWordsList?.length && ['OP', 'ED', 'MV', 'OST', 'NCS', 'BGM', 'EDM', 'GMV', 'AMV', 'MMD', 'MAD'] .some(i => titleWordsList.includes(i)); } }, timeFormatTo: { hmsToSec(str) { let parts = str?.split(':'), t = 0; switch (parts?.length) { case 2: t = (parts[0] * 60); break; case 3: t = (parts[0] * 3600) + (parts[1] * 60); break; case 4: t = (parts[0] * 86400) + (parts[1] * 3600) + (parts[2] * 60); break; } return t + +parts.pop(); }, HMS: { parseTime(time_sec) { const ts = Math.abs(+time_sec); return { d: ~~(ts / 86400), h: ~~((ts % 86400) / 3600), m: ~~((ts % 3600) / 60), s: ~~(ts % 60), }; }, digit(time_sec = required()) { const { d, h, m, s } = this.parseTime(time_sec); return (d ? `${d}d ` : '') + (h ? (d ? h.toString().padStart(2, '0') : h) + ':' : '') + (h ? m.toString().padStart(2, '0') : m) + ':' + s.toString().padStart(2, '0'); }, abbr(time_sec = required()) { const { d, h, m, s } = this.parseTime(time_sec); return (d ? `${d}d ` : '') + (h ? (d ? h.toString().padStart(2, '0') : h) + 'h' : '') + (m ? (h ? m.toString().padStart(2, '0') : m) + 'm' : '') + (s ? (m ? s.toString().padStart(2, '0') : s) + 's' : ''); }, }, ago(date = required()) { if (!(date instanceof Date)) return console.error('"date" is not Date type:', date); const samples = [ { label: 'year', seconds: 31536000 }, { label: 'month', seconds: 2592000 }, { label: 'day', seconds: 86400 }, { label: 'hour', seconds: 3600 }, { label: 'minute', seconds: 60 }, { label: 'second', seconds: 1 } ]; const now = date.getTime(), seconds = ~~((Date.now() - Math.abs(now)) / 1000), interval = samples.find(i => i.seconds < seconds), time = ~~(seconds / interval.seconds); return `${(now < 0 ? '-' : '') + time} ${interval.label}${time !== 1 ? 's' : ''}`; }, }, dateformat(format = 'YYYY/MM/DD') { if (!(this instanceof Date)) return console.error('dateformat - is not Date type:', this); const twoDigit = n => n.toString().padStart(2, '0'), date = this.getDate(), year = this.getFullYear(), month = this.getMonth(), day = this.getDay(), hours = this.getHours(), minutes = this.getMinutes(), seconds = this.getSeconds(); return format .replace(/A|Z|S(SS)?|ss?|mm?|HH?|hh?|D{1,4}|M{1,4}|YY(YY)?|'([^']|'')*'/g, partPattern => { let out; switch (partPattern) { case 'YY': out = year.substr(2); break; case 'YYYY': out = year; break; case 'M': out = month; break; case 'MM': out = twoDigit(month); break; case 'MMM': out = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][month]; break; case 'MMMM': out = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][month]; break; case 'D': out = date; break; case 'DD': out = twoDigit(date); break; case 'DDD': out = ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat'][day]; break; case 'DDDD': out = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][day]; break; case 'h': out = (hours % 12) || 12; break; case 'H': out = hours; break; case 'HH': out = twoDigit(hours); break; case 'mm': out = twoDigit(minutes); break; case 's': out = seconds; break; case 'ss': out = twoDigit(seconds); break; case 'SS': out = twoDigit(seconds); break; case 'A': out = (hours < 12 ? 'AM' : 'PM'); break; case 'Z': out = ('+' + -this.getTimezoneOffset() / 60) .replace(/^\D?(\D)/, "$1") .replace(/^(.)(.)$/, "$10$2") + '00'; break; } return out; }); }, updateUrl: (new_url = required()) => window.history.replaceState(null, null, new_url), queryURL: { has: (query = required(), url_string) => new URL(url_string || location).searchParams.has(query.toString()), get: (query = required(), url_string) => new URL(url_string || location).searchParams.get(query.toString()), set(query_obj = {}, url_string) { if (typeof query_obj != 'object' || !Object.keys(query_obj).length) return console.error('query_obj:', query_obj) const url = new URL(url_string || location); Object.entries(query_obj).forEach(([key, value]) => url.searchParams.set(key, value)); return url.toString(); }, remove(query = required(), url_string) { const url = new URL(url_string || location); url.searchParams.delete(query.toString()); return url.toString(); }, }, request: (() => { const API_STORE_NAME = 'YOUTUBE_API_KEYS'; async function getKeys() { NOVA.log('request.API: fetch to youtube_api_keys.json'); return await fetch('https://gist.githubusercontent.com/raingart/ff6711fafbc46e5646d4d251a79d1118/raw/youtube_api_keys.json') .then(res => res.text()) .then(keys => { NOVA.log(`get and save keys in localStorage`, keys); localStorage.setItem(API_STORE_NAME, keys); return JSON.parse(keys); }) .catch(error => { localStorage.removeItem(API_STORE_NAME); throw error; }) .catch(reason => console.error('Error get keys:', reason)); } return { async API({ request = required(), params = required(), api_key }) { const YOUTUBE_API_KEYS = localStorage.hasOwnProperty(API_STORE_NAME) ? JSON.parse(localStorage.getItem(API_STORE_NAME)) : await getKeys(); if (!api_key && (!Array.isArray(YOUTUBE_API_KEYS) || !YOUTUBE_API_KEYS?.length)) { localStorage.hasOwnProperty(API_STORE_NAME) && localStorage.removeItem(API_STORE_NAME); return console.error('YOUTUBE_API_KEYS empty:', YOUTUBE_API_KEYS); } const referRandKey = arr => api_key || 'AIzaSy' + arr[~~(Math.random() * arr.length)]; const query = Object.keys(params) .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) .join('&'); const URL = `https://www.googleapis.com/youtube/v3/${request}?${query}&key=` + referRandKey(YOUTUBE_API_KEYS); return await fetch(URL) .then(response => response.json()) .then(json => { if (!json?.error && Object.keys(json).length) return json; console.warn('used key:', NOVA.queryURL.get('key', URL)); if (json?.error && Object.keys(json.error).length) { throw new Error(JSON.stringify(json?.error)); } }) .catch(error => { localStorage.removeItem(API_STORE_NAME); console.error(`Request API failed:${URL}\n${error}`); if (error?.message && (err = JSON.parse(error?.message))) { return { 'code': err.code, 'reason': err.errors?.length && err.errors[0].reason, 'error': err.message, }; } }); }, }; })(), getPlayerState(state) { return { '-1': 'UNSTARTED', 0: 'ENDED', 1: 'PLAYING', 2: 'PAUSED', 3: 'BUFFERING', 5: 'CUED' }[state || movie_player.getPlayerState()]; }, videoElement: (() => { const videoSelector = '#movie_player:not(.ad-showing) video'; document.addEventListener('canplay', ({ target }) => { target.matches(videoSelector) && (NOVA.videoElement = target); }, { capture: true, once: true }); document.addEventListener('play', ({ target }) => { target.matches(videoSelector) && (NOVA.videoElement = target); }, true); })(), isFullscreen: () => ( movie_player.classList.contains('ytp-fullscreen') || (movie_player.hasOwnProperty('isFullscreen') && movie_player.isFullscreen()) ), getChannelId(api_key) { const isChannelId = id => id && /UC([a-z0-9-_]{22})$/i.test(id); let result = [ document.querySelector('meta[itemprop="channelId"][content]')?.content, (document.body.querySelector('ytd-app')?.__data?.data?.response || document.body.querySelector('ytd-app')?.data?.response || window.ytInitialData ) ?.metadata?.channelMetadataRenderer?.externalId, document.querySelector('link[itemprop="url"][href]')?.href.split('/')[4], location.pathname.split('/')[2], document.body.querySelector('#video-owner a[href]')?.href.split('/')[4], document.body.querySelector('a.ytp-ce-channel-title[href]')?.href.split('/')[4], document.body.querySelector('ytd-watch-flexy')?.playerData?.videoDetails?.channelId, ((typeof ytcfg === 'object') && (obj = ytcfg.data_?.PLAYER_VARS?.embedded_player_response) && NOVA.seachInObjectBy.key({ 'obj': JSON.parse(obj), 'keys': 'channelId', })?.data), ] .find(i => isChannelId(i)); return result; }, storage_obj_manager: { STORAGE_NAME: 'nova-channels-state', async initStorage() { this.channelId = location.search.includes('list=') ? (NOVA.queryURL.get('list') || movie_player?.getPlaylistId()) : await NOVA.waitUntil(NOVA.getChannelId, 1000); }, read(return_all) { if (store = JSON.parse(localStorage.getItem(this.STORAGE_NAME))) { return return_all ? store : store[this.channelId]; } }, write(obj_save) { if ((storage = this.read('all') || {})) { if (Object.keys(obj_save).length) { storage = Object.assign(storage, { [this.channelId]: obj_save }); } else { delete storage[this.channelId]; } } localStorage.setItem(this.STORAGE_NAME, JSON.stringify(storage)); }, _getParam(key = required()) { if (storage = this.read()) { return storage[key]; } }, async getParam(key = required()) { if (!this.channelId) await this.initStorage(); return this._getParam(...arguments); }, save(obj_save) { if (storage = this.read()) { obj_save = Object.assign(storage, obj_save); } this.write(obj_save); }, remove(key) { if ((storage = this.read())) { delete storage[key]; this.write(storage); } }, }, seachInObjectBy: { key({ obj = required(), keys = required(), match_fn = data => data.constructor.name !== 'Object', multiple = false, path = '' }) { const setPath = d => (path ? path + '.' : '') + d; let hasKey, results = []; for (const prop in obj) { if (obj.hasOwnProperty(prop) && obj[prop]) { hasKey = keys.constructor.name === 'String' ? (keys === prop) : keys.indexOf(prop) > -1; if (hasKey && (!match_fn || match_fn(obj[prop]))) { if (multiple) { results.push({ 'path': setPath(prop), 'data': obj[prop], }); } else { return { 'path': setPath(prop), 'data': obj[prop], }; } } else { switch (obj[prop].constructor.name) { case 'Object': if (result = this.key({ 'obj': obj[prop], 'keys': keys, 'path': setPath(prop), 'match_fn': match_fn, })) { if (multiple) results.push(result); else return result; } break; case 'Array': for (let i = 0; i < obj[prop].length; i++) { if (result = this.key({ 'obj': obj[prop][i], 'keys': keys, 'path': path + `[${i}]`, 'match_fn': match_fn, })) { if (multiple) results.push(result); else return result; } } break; case 'Function': if (Object.keys(obj[prop]).length) { for (const j in obj[prop]) { if (typeof obj[prop][j] !== 'undefined') { if (result = this.key({ 'obj': obj[prop][j], 'keys': keys, 'path': setPath(prop) + '.' + j, 'match_fn': match_fn, })) { if (multiple) results.push(result); else return result; } } } } break; } } } } if (multiple) return results; }, }, log() { if (this.DEBUG && arguments.length) { console.groupCollapsed(...arguments); console.trace(); console.groupEnd(); } } } window.nova_plugins.push({ id: 'scroll-to-top', title: 'Add "Scroll to top" button', 'title:zh': '滚动到顶部按钮', 'title:ja': 'トップボタンまでスクロール', 'title:ko': '맨 위로 스크롤 버튼', 'title:id': 'Gulir ke tombol atas', 'title:es': 'Desplazarse al botón superior', 'title:pt': 'Role para o botão superior', 'title:fr': 'Faites défiler vers le haut', 'title:it': 'Scorri fino al pulsante in alto', 'title:de': 'Nach oben scrollen', 'title:pl': 'Przycisk przewijania do góry', 'title:ua': 'Прокрутити до гори', run_on_pages: '*, -embed, -mobile, -live_chat', section: 'other', desc: 'Displayed on long pages', 'desc:zh': '出现在长页面上', 'desc:ja': '長いページに表示されます', 'desc:ko': '긴 페이지에 표시됨', 'desc:id': 'Ditampilkan di halaman panjang', 'desc:es': 'Mostrado en páginas largas', 'desc:pt': 'Exibido em páginas longas', 'desc:fr': 'Affiché sur de longues pages', 'desc:it': 'Visualizzato su pagine lunghe', 'desc:de': 'Wird auf langen Seiten angezeigt', 'desc:pl': 'Wyświetlaj na długich stronach', 'desc:ua': 'Відображається на довгих сторінках', _runtime: user_settings => { document.addEventListener('scroll', insertButton, { capture: true, once: true }); function insertButton() { const SELECTOR_ID = 'nova-scrollTop-btn'; const btn = document.createElement('button'); btn.id = SELECTOR_ID; Object.assign(btn.style, { position: 'fixed', cursor: 'pointer', bottom: 0, left: '20%', visibility: 'hidden', opacity: .5, width: '40%', height: '40px', border: 'none', outline: 'none', 'z-index': 1, 'border-radius': '100% 100% 0 0', 'font-size': '16px', 'background-color': 'rgba(0,0,0,.3)', 'box-shadow': '0 16px 24px 2px rgba(0, 0, 0, .14), 0 6px 30px 5px rgba(0, 0, 0, .12), 0 8px 10px -5px rgba(0, 0, 0, .4)', }); btn.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: user_settings.scroll_to_top_smooth ? 'smooth' : 'instant', }); if (user_settings.scroll_to_top_autoplay && NOVA.currentPage == 'watch' && ['UNSTARTED', 'PAUSED'].includes(NOVA.getPlayerState()) ) { movie_player.playVideo(); } }); const arrow = document.createElement('span'); Object.assign(arrow.style, { border: 'solid white', 'border-width': '0 3px 3px 0', display: 'inline-block', padding: '4px', 'vertical-align': 'middle', transform: 'rotate(-135deg)', }); btn.append(arrow); document.body.append(btn); NOVA.css.push( `#${SELECTOR_ID}:hover { opacity: 1 !important; background-color: rgba(0,0,0,.6) !important; }`); const scrollTop_btn = document.getElementById(SELECTOR_ID); let sOld; window.addEventListener('scroll', () => { const sCurr = document.documentElement.scrollTop > (window.innerHeight / 2); if (sCurr == sOld) return; sOld = sCurr; scrollTop_btn.style.visibility = sCurr ? 'visible' : 'hidden'; }); } }, options: { scroll_to_top_smooth: { _tagName: 'input', label: 'Smooth', 'label:zh': '光滑的', 'label:ja': 'スムーズ', 'label:ko': '매끄러운', 'label:id': 'Mulus', 'label:es': 'Suave', 'label:pt': 'Suave', 'label:fr': 'Lisse', 'label:it': 'Scorrimento fluido', 'label:de': 'Glatt', 'label:pl': 'Płynnie', 'label:ua': 'Плавно', type: 'checkbox', }, scroll_to_top_autoplay: { _tagName: 'input', label: 'Unpause a video', 'label:zh': '视频取消暂停', 'label:ja': 'ビデオの一時停止解除', 'label:ko': '비디오 일시 중지 해제', 'label:id': 'Video batalkan Jeda', 'label:es': 'Reanudar video', 'label:pt': 'Retomar vídeo', 'label:fr': 'Annuler la pause de la vidéo', 'label:it': 'Annulla pausa video', 'label:de': 'Video wieder anhalten', 'label:pl': 'Wyłącz wstrzymanie odtwarzania filmu', 'label:ua': 'Продовжити програвання відео', type: 'checkbox', }, } }); window.nova_plugins.push({ id: 'thumbs-title-filter', title: 'Block thumbnails by title', 'title:zh': '按标题阻止缩略图', 'title:ja': 'タイトルでサムネイルをブロックする', 'title:ko': '제목으로 축소판 차단', 'title:id': 'Blokir gambar mini berdasarkan judul', 'title:es': 'Bloquear miniaturas por título', 'title:pt': 'Bloquear miniaturas por título', 'title:fr': 'Bloquer les vignettes par titre', 'title:it': 'Blocca le miniature per titolo', 'title:de': 'Thumbnails nach Titel blockieren', 'title:pl': 'Blokuj miniatury według tytułu', 'title:ua': 'Блокуйте мініатюри за назвою', run_on_pages: '*, -embed, -mobile, -live_chat', section: 'other', _runtime: user_settings => { const keywords = NOVA.strToArray(user_settings.thumb_filter_title_blocklist); const thumbsSelectors = [ 'ytd-rich-item-renderer', 'ytd-video-renderer', 'ytd-compact-video-renderer', 'ytm-compact-video-renderer', 'ytm-item-section-renderer' ] .join(','); if (NOVA.isMobile) { NOVA.watchElements({ selectors: ['#video-title:not(:empty)'], attr_mark: 'nova-thumb-title-filtered', callback: video_title => { keywords.forEach(keyword => { if (video_title.textContent.trim().toLowerCase().includes(keyword) && (thumb = channel_name.closest(thumbsSelectors)) ) { } }); } }); } else { document.addEventListener('yt-action', evt => { if ([ 'yt-append-continuation-items-action', 'ytd-update-grid-state-action', 'yt-service-request', 'ytd-rich-item-index-update-action', ] .includes(evt.detail?.actionName) ) { hideThumb(); } }); function hideThumb() { document.body.querySelectorAll('#video-title') .forEach(el => { keywords.forEach(keyword => { if (el.innerText.toLowerCase().includes(keyword) && (thumb = el.closest(thumbsSelectors)) ) { thumb.remove(); } }); }); } } }, options: { thumb_filter_title_blocklist: { _tagName: 'textarea', label: 'Words list', 'label:zh': '单词列表', 'label:ja': '単語リスト', 'label:ko': '단어 목록', 'label:id': 'Daftar kata', 'label:es': 'lista de palabras', 'label:pt': 'Lista de palavras', 'label:fr': 'Liste de mots', 'label:it': 'Elenco di parole', 'label:de': 'Wortliste', 'label:pl': 'Lista słów', 'label:ua': 'Список слів', title: 'separator: "," or ";" or "new line"', 'title:zh': '分隔器: "," 或 ";" 或 "新队"', 'title:ja': 'セパレータ: "," または ";" または "改行"', 'title:ko': '구분 기호: "," 또는 ";" 또는 "새 줄"', 'title:id': 'pemisah: "," atau ";" atau "baris baru"', 'title:es': 'separador: "," o ";" o "new line"', 'title:pt': 'separador: "," ou ";" ou "new line"', 'title:fr': 'séparateur : "," ou ";" ou "nouvelle ligne"', 'title:it': 'separatore: "," o ";" o "nuova linea"', 'title:de': 'separator: "," oder ";" oder "new line"', 'title:pl': 'separator: "," lub ";" lub "now linia"', 'title:ua': 'розділювач: "," або ";" або "новий рядок"', placeholder: 'text1\ntext2', required: true, }, } }); window.nova_plugins.push({ id: 'disable-video-cards', title: 'Clear pages of junk', 'title:ua': 'Приховайте сміття: анотації, кінцеві заставки тощо', run_on_pages: 'results, watch, embed, -mobile', section: 'other', desc: 'Remove the annoying stuff', 'desc:ua': 'Приховайте набридливий контент', _runtime: user_settings => { let selectorsList = [ '.ytp-paid-content-overlay', '.iv-branding', '#movie_player:not(:hover) > [class^="ytp-ce-"]', '.ytp-cards-teaser-text', 'ytm-paid-content-overlay-renderer', ]; switch (NOVA.currentPage) { case 'embed': selectorsList.push([ '.ytp-pause-overlay', '.ytp-info-panel-preview', ]); break; default: selectorsList.push([ 'ytd-search-pyv-renderer', '[class^="ytd-promoted-"]', 'ytd-video-renderer + ytd-shelf-renderer', '#clarify-box', 'ytd-watch-metadata ytd-info-panel-content-renderer', '.ytd-watch-flexy.attached-message', 'ytd-popup-container tp-yt-paper-dialog ytd-single-option-survey-renderer', '#donation-shelf ytd-donation-unavailable-renderer', '[class^="ytp-cultural-moment"]', '.sparkles-light-cta', 'ytd-feed-nudge-renderer', ]); NOVA.css.push( [ 'ytd-rich-item-renderer:has(ytd-ad-slot-renderer)', 'ytd-live-chat-frame#chat[collapsed]:has(iframe#chatframe[src="about:blank"])', ] .join(',\n') + `{ display: none !important;}`); } if (selectorsList.length) { NOVA.css.push( selectorsList.join(',\n') + ` { display: none !important; }`); } }, }); window.nova_plugins.push({ id: 'thumbnails-clear', title: 'Clear thumbnails', 'title:zh': '清除缩略图', 'title:ja': 'サムネイルをクリアする', 'title:ko': '썸네일 지우기', 'title:id': 'Hapus gambar mini', 'title:es': 'Miniaturas claras', 'title:pt': 'Limpar miniaturas', 'title:fr': 'Effacer les vignettes', 'title:it': 'Cancella miniature', 'title:de': 'Miniaturansichten löschen', 'title:pl': 'Wyczyść miniatury', 'title:ua': 'Очистити мініатюри', run_on_pages: 'home, feed, channel, watch', section: 'other', desc: 'Replaces the predefined clickbait thumbnails', 'desc:zh': '替换预定义的缩略图', 'desc:ja': '事前定義されたサムネイルを置き換えます', 'desc:ko': '미리 정의된 축소판을 대체합니다', 'desc:id': 'Menggantikan gambar mini yang telah ditentukan sebelumnya', 'desc:es': 'Reemplaza la miniatura predefinida', 'desc:pt': 'Substitui a miniatura predefinida', 'desc:it': 'Sostituisce la miniatura predefinita', 'desc:de': 'Ersetzt das vordefinierte Thumbnail', 'desc:pl': 'Zastępuje predefiniowaną miniaturkę', 'desc:ua': 'Замінює попередньо визначені мініатюри клікбейти', _runtime: user_settings => { const ATTR_MARK = 'nova-thumb-preview-cleared', thumbsSelectors = [ 'ytd-rich-item-renderer', 'ytd-video-renderer', 'ytm-compact-video-renderer', 'ytm-item-section-renderer' ]; let DISABLE_YT_IMG_DELAY_LOADING_default = false; NOVA.watchElements({ selectors: [ '#thumbnail:not(.ytd-playlist-thumbnail):not([class*=markers]):not([href*="/shorts/"]) img[src]:not([src*="_live.jpg"])', 'a:not([href*="/shorts/"]) img.video-thumbnail-img[src]:not([src*="_live.jpg"])' ], attr_mark: ATTR_MARK, callback: async img => { if (NOVA.currentPage == 'results') return; if (window.yt?.config_?.DISABLE_YT_IMG_DELAY_LOADING && DISABLE_YT_IMG_DELAY_LOADING_default !== window.yt?.config_?.DISABLE_YT_IMG_DELAY_LOADING ) { DISABLE_YT_IMG_DELAY_LOADING_default = window.yt?.config_?.DISABLE_YT_IMG_DELAY_LOADING; await NOVA.delay(100); document.body.querySelectorAll(`[${ATTR_MARK}]`).forEach(e => e.removeAttribute(ATTR_MARK)); } if ((thumb = img.closest(thumbsSelectors)) && thumb.querySelector( `#badges [class*="live-now"], #overlays [aria-label="PREMIERE"], #overlays [overlay-style="UPCOMING"]`) ) { return; } if (src = patchImg(img.src)) img.src = patchImg(src); }, }); if (user_settings.thumbnails_clear_overlay) { NOVA.css.push( `#hover-overlays { visibility: hidden !important; }`); } function patchImg(str) { if ((re = /(\w{2}default|hq\d+)./i) && re.test(str)) { return str.replace(re, (user_settings.thumbnails_clear_preview_timestamp || 'hq2') + '.'); } } }, options: { thumbnails_clear_preview_timestamp: { _tagName: 'select', label: 'Thumbnail timestamps moment', 'label:zh': '缩略图时间戳', 'label:ja': 'サムネイルのタイムスタンプ', 'label:ko': '썸네일 타임스탬프', 'label:id': 'Stempel waktu gambar mini', 'label:es': 'Marcas de tiempo en miniatura', 'label:pt': 'Carimbos de data e hora em miniatura', 'label:fr': 'Horodatages des vignettes', 'label:it': 'Timestamp in miniatura', 'label:de': 'Thumbnail-Zeitstempel', 'label:pl': 'Znaczniki czasowe miniatur', 'label:ua': 'Мітки часу мініатюр', title: 'Show thumbnail from video time position', 'title:zh': '从视频时间位置显示缩略图', 'title:ja': 'ビデオの時間位置からサムネイルを表示', 'title:ko': '비디오 시간 위치에서 썸네일 표시', 'title:id': 'Tampilkan thumbnail dari posisi waktu video', 'title:es': 'Mostrar miniatura de la posición de tiempo del video', 'title:pt': 'Mostrar miniatura da posição no tempo do vídeo', 'title:fr': 'Afficher la vignette à partir de la position temporelle de la vidéo', 'title:it': "Mostra la miniatura dalla posizione dell'ora del video", 'title:de': 'Miniaturansicht von der Videozeitposition anzeigen', 'title:pl': 'Pokaż miniaturkę z pozycji czasu wideo', 'title:ua': 'Показати мініатюру з часової позиції відео', options: [ { label: 'start', value: 'hq1', 'label:zh': '开始', 'label:ja': '始まり', 'label:ko': '시작', 'label:id': 'awal', 'label:es': 'comienzo', 'label:pt': 'começar', 'label:fr': 'le début', 'label:it': 'inizio', 'label:de': 'anfang', 'label:pl': 'początek', 'label:ua': 'початок', }, { label: 'middle', value: 'hq2', selected: true, 'label:zh': '中间', 'label:ja': '真ん中', 'label:ko': '~ 아니다', 'label:id': 'tengah', 'label:es': 'medio', 'label:pt': 'meio', 'label:fr': 'ne pas', 'label:it': 'mezzo', 'label:de': 'mitte', 'label:pl': 'środek', 'label:ua': 'середина', }, { label: 'end', value: 'hq3', 'label:zh': '结尾', 'label:ja': '終わり', 'label:ko': '끝', 'label:id': 'akhir', 'label:es': 'fin', 'label:pt': 'fim', 'label:fr': 'finir', 'label:it': 'fine', 'label:de': 'ende', 'label:pl': 'koniec', 'label:ua': 'кінець', } ], }, thumbnails_clear_overlay: { _tagName: 'input', label: 'Hide overlay buttons on a thumbnail', 'label:zh': '隐藏覆盖在缩略图上的按钮', 'label:ja': 'サムネイルにオーバーレイされたボタンを非表示にする', 'label:ko': '축소판에서 오버레이 버튼 숨기기', 'label:id': 'Sembunyikan tombol overlay pada thumbnail', 'label:es': 'Ocultar botones superpuestos en una miniatura', 'label:pt': 'Ocultar botões de sobreposição em uma miniatura', 'label:fr': 'Masquer les boutons de superposition sur une vignette', 'label:it': 'Nascondi pulsanti sovrapposti su una miniatura', 'label:de': 'Überlagerungsschaltflächen auf einer Miniaturansicht ausblenden', 'label:pl': 'Ukryj przyciski nakładki na miniaturce', 'label:ua': 'Приховати кнопки на мініатюрі', type: 'checkbox', title: 'Hide [ADD TO QUEUE] [WATCH LATER]', }, } }); window.nova_plugins.push({ id: 'search-filter', title: 'Blocked channels', 'title:zh': '屏蔽频道列表', 'title:ja': 'ブロックされたチャネルのリスト', 'title:ko': '차단된 채널 목록', 'title:id': 'Saluran yang diblokir', 'title:es': 'Lista de canales bloqueados', 'title:pt': 'Lista de canais bloqueados', 'title:fr': 'Liste des chaînes bloquées', 'title:it': 'Canali bloccati', 'title:de': 'Liste der gesperrten Kanäle', 'title:pl': 'Zablokowane kanały', 'title:ua': 'Заблоковані канали', run_on_pages: 'results, feed, -mobile', section: 'other', desc: 'Hide channels on the search page', 'desc:zh': '在搜索页面上隐藏频道', 'desc:ja': '検索ページでチャンネルを非表示にする', 'desc:ko': '검색 페이지에서 채널 숨기기', 'desc:id': 'Sembunyikan saluran di halaman pencarian', 'desc:es': 'Ocultar canales en la página de búsqueda', 'desc:pt': 'Ocultar canais na página de pesquisa', 'desc:fr': 'Masquer les chaînes sur la page de recherche', 'desc:it': 'Nascondi i canali nella pagina di ricerca', 'desc:de': 'Kanäle auf der Suchseite ausblenden', 'desc:pl': 'Ukryj kanały na stronie wyszukiwania', 'desc:ua': 'Приховує канали на сторінці пошуку', _runtime: user_settings => { const keywords = NOVA.strToArray(user_settings.search_filter_channel_blocklist); const thumbsSelectors = [ 'ytd-rich-item-renderer', 'ytd-video-renderer', 'ytm-compact-video-renderer', ] .join(','); if (NOVA.isMobile) { NOVA.watchElements({ selectors: ['#channel-name'], attr_mark: 'nova-thumb-channel-filtered', callback: channel_name => { if (keywords.includes(channel_name.textContent.trim().toLowerCase()) && (thumb = channel_name.closest(thumbsSelectors)) ) { thumb.remove(); } } }); } else { document.addEventListener('yt-action', evt => { if ([ 'yt-append-continuation-items-action', 'ytd-update-grid-state-action', 'yt-service-request', ] .includes(evt.detail?.actionName) ) { document.body.querySelectorAll( '#channel-name a[href]' ) .forEach(channel_name => { keywords.forEach(keyword => { if (keyword.startsWith('@') && channel_name.href.includes(keyword) && (thumb = channel_name.closest(thumbsSelectors)) ) { thumb.remove(); } else if (channel_name.textContent.trim().toLowerCase().includes(keyword) && (thumb = channel_name.closest(thumbsSelectors)) ) { thumb.style.display = 'none'; } }); }); } }); } }, options: { search_filter_channel_blocklist: { _tagName: 'textarea', label: 'List', 'label:zh': '频道列表', 'label:ja': 'チャンネルリスト', 'label:ko': '채널 목록', 'label:id': 'Daftar', 'label:es': 'Lista', 'label:pt': 'Lista', 'label:fr': 'Liste', 'label:it': 'Elenco', 'label:de': 'Liste', 'label:pl': 'Lista', 'label:ua': 'Список', title: 'separator: "," or ";" or "new line"', 'title:zh': '分隔器: "," 或 ";" 或 "新队"', 'title:ja': 'セパレータ: "," または ";" または "改行"', 'title:ko': '구분 기호: "," 또는 ";" 또는 "새 줄"', 'title:id': 'pemisah: "," atau ";" atau "baris baru"', 'title:es': 'separador: "," o ";" o "new line"', 'title:pt': 'separador: "," ou ";" ou "new line"', 'title:fr': 'séparateur : "," ou ";" ou "nouvelle ligne"', 'title:it': 'separatore: "," o ";" o "nuova linea"', 'title:de': 'separator: "," oder ";" oder "new line"', 'title:pl': 'separator: "," lub ";" lub "now linia"', 'title:ua': 'розділювач: "," або ";" або "новий рядок"', placeholder: 'channel1\nchannel2', required: true, }, } }); window.nova_plugins.push({ id: 'thumbs-hide', title: 'Thumbnails filter', 'title:zh': '缩略图过滤', 'title:ja': 'サムネイルのフィルタリング', 'title:ko': '썸네일 필터링', 'title:id': 'Pemfilteran gambar mini', 'title:es': 'Filtrado de miniaturas', 'title:pt': 'Filtragem de miniaturas', 'title:fr': 'Filtrage des vignettes', 'title:it': 'Filtraggio miniature', 'title:de': 'Filtrowanie miniatur', 'title:pl': 'Ukryj kilka miniatur', 'title:ua': 'Фільтрування мініатюр', run_on_pages: 'home, results, feed, channel, watch, -mobile', section: 'other', _runtime: user_settings => { const thumbsSelectors = [ 'ytd-rich-item-renderer', 'ytd-video-renderer', 'ytd-compact-video-renderer', 'ytm-compact-video-renderer', 'ytm-item-section-renderer' ] .join(','); document.addEventListener('yt-action', evt => { if ([ 'yt-append-continuation-items-action', 'ytd-update-grid-state-action', 'yt-service-request', 'ytd-rich-item-index-update-action', ] .includes(evt.detail?.actionName) ) { switch (NOVA.currentPage) { case 'home': thumbRemove.live(); thumbRemove.mix(); thumbRemove.watched(); break; case 'results': thumbRemove.live(); thumbRemove.shorts(); thumbRemove.mix(); break; case 'feed': thumbRemove.live(); thumbRemove.streamed(); thumbRemove.shorts(); thumbRemove.durationLimits(); thumbRemove.premieres(); thumbRemove.mix(); thumbRemove.watched(); break; case 'channel': thumbRemove.live(); thumbRemove.streamed(); thumbRemove.premieres(); thumbRemove.watched(); break; case 'watch': thumbRemove.live(); thumbRemove.mix(); thumbRemove.watched(); break; } } }); if (user_settings.shorts_disable) { const stylesList = [ '#contents > ytd-reel-shelf-renderer', 'ytd-rich-section-renderer', ] .join(',\n'); NOVA.css.push(stylesList + `{ display: none !important; }`); } const thumbRemove = { shorts() { if (!user_settings.shorts_disable) return; if (NOVA.currentPage == 'channel' && NOVA.channelTab == 'shorts') return; document.body.querySelectorAll('a#thumbnail[href*="shorts/"]') .forEach(el => el.closest(thumbsSelectors)?.remove()); }, durationLimits() { if (!+user_settings.thumbs_min_duration) return; const OVERLAYS_TIME_SELECTOR = '#thumbnail #overlays #text:not(:empty)'; NOVA.waitSelector(OVERLAYS_TIME_SELECTOR) .then(() => { document.body.querySelectorAll(OVERLAYS_TIME_SELECTOR) .forEach(el => { if ((thumb = el.closest(thumbsSelectors)) && (timeSec = NOVA.timeFormatTo.hmsToSec(el.textContent.trim())) && (timeSec * (user_settings.rate_default || 1)) < (+user_settings.thumbs_min_duration || 60) ) { thumb.style.border = '2px solid blue'; } }); }); }, premieres() { if (!user_settings.premieres_disable) return; document.body.querySelectorAll( `#thumbnail #overlays [aria-label="Premiere"], #thumbnail #overlays [aria-label="Upcoming"]` ) .forEach(el => el.closest(thumbsSelectors)?.remove()); document.body.querySelectorAll('#video-badges > [class*="live-now"]') .forEach(el => el.closest(thumbsSelectors)?.remove()); }, live() { if (!user_settings.live_disable) return; if (NOVA.currentPage == 'channel' && NOVA.channelTab == 'streams') return; const keywords = NOVA.strToArray(user_settings.streamed_disable_channels_exception); document.body.querySelectorAll('#thumbnail img[src*="_live.jpg"]') .forEach(el => { if (thumb = el.closest(thumbsSelectors)) { if (keywords?.includes(thumb.querySelector('#channel-name a')?.textContent.trim().toLowerCase())) { if (user_settings['search-filter']) { thumb.style.display = 'block'; } return; } thumb.remove(); } }); }, streamed() { if (!user_settings.streamed_disable) return; if (NOVA.currentPage == 'channel' && NOVA.channelTab == 'streams') return; const keywords = NOVA.strToArray(user_settings.streamed_disable_channels_exception); document.body.querySelectorAll('#metadata') .forEach(el => { if (el.querySelector('#metadata-line > span:last-of-type')?.textContent?.split(' ').length === 4 && (thumb = el.closest(thumbsSelectors)) ) { if (keywords?.includes(thumb.querySelector('#channel-name a')?.textContent.trim().toLowerCase())) { if (user_settings['search-filter']) { thumb.style.display = 'block'; } return; } thumb.remove(); } }); }, mix() { if (!user_settings.mix_disable) return; document.body.querySelectorAll( `a[href*="list="][href*="start_radio="]:not([hidden]), #video-title[title^="Mix -"]:not([hidden])` ) .forEach(el => el.closest('ytd-radio-renderer, ytd-compact-radio-renderer, ' + thumbsSelectors)?.remove()); }, watched() { if (!user_settings.watched_disable) return; if (!user_settings['thumbnails-watched']) return; const PERCENT_COMPLETE = user_settings.watched_disable_percent_complete || 90; document.body.querySelectorAll('#thumbnail #overlays #progress') .forEach(el => { if (parseInt(el.style.width) > PERCENT_COMPLETE) { el.closest(thumbsSelectors)?.remove(); } }); }, }; if (user_settings.mix_disable) { NOVA.css.push( `ytd-radio-renderer { display: none !important; }`); } }, options: { shorts_disable: { _tagName: 'input', label: 'Hide Shorts', 'label:zh': '隐藏短裤', 'label:ja': 'ショーツを隠す', 'label:ko': '반바지 숨기기', 'label:id': 'Sembunyikan Celana Pendek', 'label:es': 'Ocultar pantalones cortos', 'label:pt': 'Ocultar shorts', 'label:fr': 'Masquer les shorts', 'label:it': 'Nascondi pantaloncini', 'label:de': 'Shorts verstecken', 'label:pl': 'Ukryj YouTube Shorts', 'label:ua': 'Приховати прев`ю', type: 'checkbox', }, thumbs_min_duration: { _tagName: 'input', label: 'Min duration in sec (for regular video)', 'label:zh': '最短持续时间(以秒为单位)', 'label:ja': '秒単位の最小期間', 'label:ko': '최소 지속 시간(초)', 'label:id': 'Durasi lebih sedikit dalam detik', 'label:es': 'Duración mínima en segundos', 'label:pt': 'Duração mínima em segundos', 'label:fr': 'Durée minimale en secondes', 'label:it': 'Meno durata in sec', 'label:de': 'Mindestdauer in Sekunden', 'label:pl': 'Poniżej czasu trwania w sekundach', 'label:ua': 'Мінімальна триваліcть в cекундах', type: 'number', title: 'in sec / 0 - disable', placeholder: '60-3600', step: 1, min: 0, max: 3600, value: 0, }, premieres_disable: { _tagName: 'input', label: 'Hide Premieres/Upcoming', 'label:zh': '隐藏首映/即将上映', 'label:ja': 'プレミア公開/近日公開を非表示', 'label:ko': 'Premieres/예정 숨기기', 'label:id': 'Sembunyikan Tayang Perdana/Mendatang', 'label:es': 'Ocultar estrenos/próximos', 'label:pt': 'Ocultar Estreias/Próximas', 'label:fr': 'Masquer les premières/à venir', 'label:it': 'Nascondi anteprime/in arrivo', 'label:de': 'Premieren/Kommende ausblenden', 'label:pl': 'Ukrywaj premiery', 'label:ua': 'Приховати прем`єри', type: 'checkbox', title: 'Premiere Announcements', }, live_disable: { _tagName: 'input', label: 'Hide Live streams', 'label:zh': '隐藏直播', 'label:ja': 'ライブ ストリームを非表示にする', 'label:ko': '라이브 스트림 숨기기', 'label:id': 'Sembunyikan streaming langsung', 'label:es': 'Ocultar transmisiones en vivo', 'label:pt': 'Ocultar transmissões ao vivo', 'label:fr': 'Masquer les flux en direct', 'label:it': 'Nascondi live streaming', 'label:de': 'Live-Streams ausblenden', 'label:pl': 'Ukryj strumień (na żywo)', 'label:ua': 'Приховати живі транcляції', type: 'checkbox', title: 'Now airing', 'title:zh': '正在播出', 'title:ja': '放映中', 'title:ko': '지금 방영중', 'title:id': 'Sekarang ditayangkan', 'title:es': 'Ahora al aire', 'title:pt': 'Agora no ar', 'title:fr': 'Diffusion en cours', 'title:it': 'Ora in onda', 'title:de': 'Jetzt Lüften', 'title:pl': 'Teraz wietrzenie', 'title:ua': 'Зараз в ефірі', }, streamed_disable_channels_exception: { _tagName: 'textarea', label: 'Сhannels exception', title: 'separator: "," or ";" or "new line"', 'title:zh': '分隔器: "," 或 ";" 或 "新队"', 'title:ja': 'セパレータ: "," または ";" または "改行"', 'title:ko': '구분 기호: "," 또는 ";" 또는 "새 줄"', 'title:id': 'pemisah: "," atau ";" atau "baris baru"', 'title:es': 'separador: "," o ";" o "new line"', 'title:pt': 'separador: "," ou ";" ou "new line"', 'title:fr': 'séparateur : "," ou ";" ou "nouvelle ligne"', 'title:it': 'separatore: "," o ";" o "nuova linea"', 'title:de': 'separator: "," oder ";" oder "new line"', 'title:pl': 'separator: "," lub ";" lub "now linia"', 'title:ua': 'розділювач: "," або ";" або "новий рядок"', placeholder: 'channel1\nchannel2', 'data-dependent': { 'live_disable': true }, }, streamed_disable: { _tagName: 'input', label: 'Hide finished streams', 'label:zh': '隐藏完成的流', 'label:ja': '終了したストリームを非表示にする', 'label:ko': '완료된 스트림 숨기기', 'label:id': 'Sembunyikan aliran yang sudah selesai', 'label:es': 'Ocultar flujos terminados', 'label:pt': 'Ocultar streams concluídos', 'label:fr': 'Masquer les flux terminés', 'label:it': 'Nascondi i flussi finiti', 'label:de': 'Fertige Streams ausblenden', 'label:pl': 'Ukryj po streamie', 'label:ua': 'cховати завершені транcляції', type: 'checkbox', //title: '', 'data-dependent': { 'live_disable': true }, }, mix_disable: { _tagName: 'input', label: "Hide 'Mix' thumbnails", 'label:zh': '隐藏[混合]缩略图', 'label:ja': '「Mix」サムネイルを非表示', 'label:ko': '"믹스" 썸네일 숨기기', 'label:id': 'Sembunyikan gambar mini "Mix"', 'label:es': "Ocultar miniaturas de 'Mix'", 'label:pt': "Ocultar miniaturas de 'Mix'", 'label:fr': 'Masquer les vignettes "Mix"', 'label:it': 'Nascondi le miniature "Mix".', 'label:de': '„Mix“-Thumbnails ausblenden', 'label:pl': 'Ukryj miniaturki "Mix"', 'label:ua': 'Приховати мікc мініатюр', type: 'checkbox', title: '[Mix] offers to rewatch what has already saw', 'title:zh': '[混合]提供重新观看已经看过的内容', 'title:ja': '「Mix」は、すでに見たものを再視聴することを提案します', 'title:ko': '[Mix]는 이미 본 것을 다시 볼 것을 제안합니다', 'title:id': '[Mix] menawarkan untuk menonton ulang apa yang telah dilihat', 'title:es': '[Mix] ofrece volver a ver lo que ya vio', 'title:pt': '[Mix] se oferece para rever o que já viu', 'title:it': '[Mix] si offre di rivedere ciò che ha già visto', 'title:de': '[Mix] bietet an, bereits Gesehenes noch einmal anzuschauen', 'title:pl': '[Mix] proponuje ponowne obejrzenie już obejrzanych filmów', 'title:ua': '[Mix] пропонує передивитиcя вже побачене', }, watched_disable: { _tagName: 'input', label: 'Hide watched', 'label:zh': '隐藏观看', 'label:ja': '監視対象を非表示', 'label:ko': '시청 숨기기', 'label:id': 'Sembunyikan ditonton', 'label:es': 'Ocultar visto', 'label:pt': 'Ocultar assistidos', 'label:fr': 'Masquer surveillé', 'label:it': 'Nascondi guardato', 'label:de': 'Ausblenden beobachtet', 'label:pl': 'Ukryj oglądane', 'label:ua': 'cховати переглянуті відео', type: 'checkbox', title: 'Need to Turn on [YouTube History]', }, watched_disable_percent_complete: { _tagName: 'input', label: 'Threshold percent', type: 'number', title: 'in %', placeholder: '%', step: 5, min: 5, max: 100, value: 90, 'data-dependent': { 'watched_disable': true }, }, } }); window.nova_plugins.push({ id: 'thumbnails-watched', title: 'Mark watched thumbnails', 'title:zh': '标记您观看的缩略图', 'title:ja': '視聴したサムネイルにマークを付ける', 'title:ko': '본 썸네일 표시', 'title:id': 'Tandai gambar mini yang ditonton', 'title:es': 'Mark vio miniaturas', 'title:pt': 'Mark assistiu às miniaturas', 'title:fr': 'Marquer les vignettes visionnées', 'title:it': 'Contrassegna le miniature visualizzate', 'title:de': 'Angesehene Miniaturansichten markieren', 'title:pl': 'Oznacz obejrzane miniaturki', 'title:ua': 'Позначити переглянуті мініатюри', run_on_pages: 'home, results, feed, channel, watch, -mobile', section: 'other', _runtime: user_settings => { NOVA.css.push( `a#thumbnail, a[class*="thumbnail"] { outline: 1px solid var(--yt-spec-general-background-a); } a#thumbnail:visited, a[class*="thumbnail"]:visited { outline: 1px solid ${user_settings.thumbnails_watched_frame_color || 'red'} !important; } ytd-playlist-panel-video-renderer a:visited #meta * { color: ${user_settings.thumbnails_watched_title_color || '#ff4500'} !important; }`); if (user_settings.thumbnails_watched_title) { NOVA.css.push( `a#video-title:visited:not(:hover), #description a:visited { color: ${user_settings.thumbnails_watched_title_color} !important; }`); } }, options: { thumbnails_watched_frame_color: { _tagName: 'input', label: 'Frame color', 'label:zh': '框架颜色', 'label:ja': 'フレームカラー', 'label:ko': '프레임 색상', 'label:id': 'Warna bingkai', 'label:es': 'Color del marco', 'label:pt': 'Cor da moldura', 'label:fr': 'Couleur du cadre', 'label:it': 'Colore del telaio', 'label:de': 'Rahmenfarbe', 'label:pl': 'Kolor ramki', 'label:ua': 'Колір рамки', type: 'color', value: '#FF0000', }, thumbnails_watched_title: { _tagName: 'input', label: 'Set title color', 'label:zh': '您要更改标题颜色吗?', 'label:ja': 'タイトルの色を変更しますか?', 'label:ko': '제목 색상 설정', 'label:id': 'Setel warna judul', 'label:es': 'Establecer el color del título', 'label:pt': 'Definir a cor do título', 'label:fr': 'Définir la couleur du titre', 'label:it': 'Imposta il colore del titolo', 'label:de': 'Titelfarbe festlegen', 'label:pl': 'Ustaw kolor tytułu', 'label:ua': 'Встановити колір заголовку', type: 'checkbox', }, thumbnails_watched_title_color: { _tagName: 'input', label: 'Choose title color', 'label:zh': '选择标题颜色', 'label:ja': 'タイトルの色を選択', 'label:ko': '제목 색상 선택', 'label:id': 'Pilih warna judul', 'label:es': 'Elija el color del título', 'label:pt': 'Escolha a cor do título', 'label:fr': 'Choisissez la couleur du titre', 'label:it': 'Scegli il colore del titolo', 'label:de': 'Titelfarbe auswählen', 'label:pl': 'Wybierz kolor tytułu', 'label:ua': 'Обрати колір заголовку', type: 'color', value: '#ff4500', 'data-dependent': { 'thumbnails_watched_title': true }, }, } }); window.nova_plugins.push({ id: 'miniplayer-disable', title: 'Disable miniplayer', 'title:ua': 'Вимкнути мінівідтворювач', run_on_pages: 'watch, -mobile', section: 'other', desc: 'shown on changeable page when playing playlist', 'desc:ua': 'Відображається на іншій сторінці під час відтворення плейлиста', _runtime: user_settings => { NOVA.css.push( `.ytp-right-controls .ytp-miniplayer-button { display: none !important; }`); document.addEventListener('yt-action', evt => { if (evt.detail?.actionName.includes('miniplayer')) { document.body.querySelector('ytd-miniplayer[active] #movie_player button.ytp-miniplayer-close-button') ?.click(); } }); }, }); window.nova_plugins.push({ id: 'channel-trailer-stop-preload', title: 'Stop play channel trailer', 'title:zh': '停止频道预告片', 'title:ja': 'チャンネルの予告編を停止する', 'title:ko': '채널 예고편 중지', 'title:id': 'Hentikan cuplikan saluran', 'title:es': 'Detener el tráiler del canal', 'title:pt': 'Parar o trailer do canal', 'title:fr': 'Arrêter la bande-annonce de la chaîne', 'title:it': 'Interrompi il trailer del canale', 'title:de': 'Kanaltrailer stoppen', 'title:pl': 'Zatrzymaj zwiastun kanału', 'title:ua': 'Не відтворювати трейлер каналу', run_on_pages: 'channel, -mobile', restart_on_location_change: true, section: 'channel', _runtime: user_settings => { NOVA.waitSelector('#c4-player.playing-mode', { destroy_if_url_changes: true }) .then(player => player.stopVideo()); }, }); window.nova_plugins.push({ id: 'thumbnails-title-normalize', title: 'Decapitalize thumbnails title', 'title:zh': '从大写中删除缩略图标题', 'title:ja': 'サムネイルのタイトルを大文字から外す', 'title:ko': '썸네일 제목을 대문자로', 'title:id': 'Judul gambar mini decapitalize', 'title:es': 'Descapitalizar el título de las miniaturas', 'title:pt': 'Decapitalize o título das miniaturas', 'title:fr': 'Démajuscule le titre des vignettes', 'title:it': 'Decapitalizza il titolo delle miniature', 'title:de': 'Thumbnails-Titel entfernen', 'title:pl': 'Zmniejsz czcionkę w tytule miniatur', 'title:ua': 'Завжди маленькі літери для назв мініатюр', run_on_pages: 'home, feed, channel, watch', section: 'other', desc: 'Upper Case thumbnails title back to normal', 'desc:ua': 'Зняти слова з великої літери для назв мініатюр', _runtime: user_settings => { const VIDEO_TITLE_SELECTOR = [ '#video-title', 'a > [class*="media-item-headline"]', ] .map(i => i + ':not(:empty)'), MAX_CAPS_LETTERS = +user_settings.thumbnails_title_normalize_smart_max_words || 2, ATTR_MARK = 'nova-thumb-title-normalized', clearOfSymbols = str => str.replace(/[\u2011-\u26FF]/g, ' ').replace(/\s{2,}/g, ' '), clearOfEmoji = str => str.replace(/[^<>=\p{L}\p{N}\p{P}\p{Z}{\^\$}]/gu, ' ').replace(/\s{2,}/g, ' '); if (user_settings.thumbnails_title_normalize_show_full) { NOVA.css.push( VIDEO_TITLE_SELECTOR.join(',') + `{ display: block !important; max-height: unset !important; }`); } const UpperCaseLetterRegex = new RegExp("([\-0-9A-ZÀ-ÖØ-ÞĀĂĄĆĈĊČĎĐĒĔĖĘĚĜĞĠĢĤĦĨĪĬĮİIJĴĶĹĻĽĿŁŃŅŇŊŌŎŐŒŔŖŘŚŜŞŠŢŤŦŨŪŬŮŰŲŴŶŸ-ŹŻŽƁ-ƂƄƆ-ƇƉ-ƋƎ-ƑƓ-ƔƖ-ƘƜ-ƝƟ-ƠƢƤƦ-ƧƩƬƮ-ƯƱ-ƳƵƷ-ƸƼDŽLJNJǍǏǑǓǕǗǙǛǞǠǢǤǦǨǪǬǮDZǴǶ-ǸǺǼǾȀȂȄȆȈȊȌȎȐȒȔȖȘȚȜȞȠȢȤȦȨȪȬȮȰȲȺ-ȻȽ-ȾɁɃ-ɆɈɊɌɎͰͲͶΆΈ-ΊΌΎ-ΏΑ-ΡΣ-ΫϏϒ-ϔϘϚϜϞϠϢϤϦϨϪϬϮϴϷϹ-ϺϽ-ЯѠѢѤѦѨѪѬѮѰѲѴѶѸѺѼѾҀҊҌҎҐҒҔҖҘҚҜҞҠҢҤҦҨҪҬҮҰҲҴҶҸҺҼҾӀ-ӁӃӅӇӉӋӍӐӒӔӖӘӚӜӞӠӢӤӦӨӪӬӮӰӲӴӶӸӺӼӾԀԂԄԆԈԊԌԎԐԒԔԖԘԚԜԞԠԢԱ-Ֆ֊־٠-٩۰-۹߀-߉०-९০-৯੦-੯૦-૯୦-୯௦-௯౦-౯೦-೯൦-൯๐-๙໐-໙༠-༩၀-၉႐-႙Ⴀ-Ⴥ០-៩᠆᠐-᠙᥆-᥏᧐-᧙᭐-᭙᮰-᮹᱀-᱉᱐-᱙ḀḂḄḆḈḊḌḎḐḒḔḖḘḚḜḞḠḢḤḦḨḪḬḮḰḲḴḶḸḺḼḾṀṂṄṆṈṊṌṎṐṒṔṖṘṚṜṞṠṢṤṦṨṪṬṮṰṲṴṶṸṺṼṾẀẂẄẆẈẊẌẎẐẒẔẞẠẢẤẦẨẪẬẮẰẲẴẶẸẺẼẾỀỂỄỆỈỊỌỎỐỒỔỖỘỚỜỞỠỢỤỦỨỪỬỮỰỲỴỶỸỺỼỾἈ-ἏἘ-ἝἨ-ἯἸ-ἿὈ-ὍὙὛὝὟὨ-ὯᾸ-ΆῈ-ΉῘ-ΊῨ-ῬῸ-Ώ‐-―ℂℇℋ-ℍℐ-ℒℕℙ-ℝℤΩℨK-ℭℰ-ℳℾ-ℿⅅↃⰀ-ⰮⱠⱢ-ⱤⱧⱩⱫⱭ-ⱯⱲⱵⲀⲂⲄⲆⲈⲊⲌⲎⲐⲒⲔⲖⲘⲚⲜⲞⲠⲢⲤⲦⲨⲪⲬⲮⲰⲲⲴⲶⲸⲺⲼⲾⳀⳂⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢ⸗⸚〜〰゠꘠-꘩ꙀꙂꙄꙆꙈꙊꙌꙎꙐꙒꙔꙖꙘꙚꙜꙞꙢꙤꙦꙨꙪꙬꚀꚂꚄꚆꚈꚊꚌꚎꚐꚒꚔꚖꜢꜤꜦꜨꜪꜬꜮꜲꜴꜶꜸꜺꜼꜾꝀꝂꝄꝆꝈꝊꝌꝎꝐꝒꝔꝖꝘꝚꝜꝞꝠꝢꝤꝦꝨꝪꝬꝮꝹꝻꝽ-ꝾꞀꞂꞄꞆꞋ꣐-꣙꤀-꤉꩐-꩙︱-︲﹘﹣-0-9A-Z]|\ud801[\udc00-\udc27\udca0-\udca9]|\ud835[\udc00-\udc19\udc34-\udc4d\udc68-\udc81\udc9c\udc9e-\udc9f\udca2\udca5-\udca6\udca9-\udcac\udcae-\udcb5\udcd0-\udce9\udd04-\udd05\udd07-\udd0a\udd0d-\udd14\udd16-\udd1c\udd38-\udd39\udd3b-\udd3e\udd40-\udd44\udd46\udd4a-\udd50\udd6c-\udd85\udda0-\uddb9\uddd4-\udded\ude08-\ude21\ude3c-\ude55\ude70-\ude89\udea8-\udec0\udee2-\udefa\udf1c-\udf34\udf56-\udf6e\udf90-\udfa8\udfca\udfce-\udfff]){2,}", 'g'); NOVA.css.push({ 'text-transform': 'uppercase', }, VIDEO_TITLE_SELECTOR.map(e => `${e}[${ATTR_MARK}]::first-letter`), 'important'); NOVA.watchElements({ selectors: VIDEO_TITLE_SELECTOR, attr_mark: ATTR_MARK, callback: async videoTitleEl => { if (NOVA.currentPage == 'results') return; let countCaps = 0; if (user_settings.thumbnails_title_clear_emoji) { videoTitleEl.textContent = clearOfEmoji(videoTitleEl.innerText).trim(); } if (user_settings.thumbnails_title_clear_symbols) { videoTitleEl.textContent = clearOfSymbols(videoTitleEl.innerText).trim(); } const normalizedText = videoTitleEl.innerText.replace(UpperCaseLetterRegex, match => { ++countCaps; return ( /\d/.test(match) || (match.length === 1 && /[A-Z]/.test(match)) //|| (match.length < 5 && match.includes('.') && /([A-Z]\.){2,}/.test(match)) || (match.length < 5 && match.length > 1 && ['HD', 'UHD', 'USB', 'TV', 'CPU', 'GPU', 'APU', 'AMD', 'XT', 'RX', 'GTX', 'RTX', 'GT', 'FX', 'SE', 'HP', 'SSD', 'RAM', 'PC', 'FPS', 'RDNA', 'FSR', 'DLSS', 'MSI', 'VR', 'GOTY', 'AAA', 'UI', 'BBC', 'WWE', 'OS', 'OP', 'ED', 'MV', 'PV', 'OST', 'NCS', 'BGM', 'EDM', 'GMV', 'AMV', 'MMD', 'MAD', 'SQL', 'CAPS'].includes(match)) || (match.length < 5 && /(M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3}))/i.test(match)) ) ? match : match.toLowerCase(); }); if (countCaps > MAX_CAPS_LETTERS || (countCaps > 1 && normalizedText.split(/\s+/).length === countCaps) ) { videoTitleEl.innerText = normalizedText; } } }); document.addEventListener('yt-action', evt => { if (evt.detail?.actionName == 'yt-chip-cloud-chip-select-action') { window.addEventListener('transitionend', restoreTitle, { capture: true, once: true }); } }); function restoreTitle() { const selectorOldTitle = '#video-title-link[title]'; if (NOVA.channelTab == 'videos') { document.body.querySelectorAll(`${selectorOldTitle} ${VIDEO_TITLE_SELECTOR}[${ATTR_MARK}]`) .forEach(el => { if (oldTitle = el.closest(selectorOldTitle)?.title) { el.innerText = oldTitle; el.removeAttribute(ATTR_MARK); } }); } } }, options: { thumbnails_title_normalize_show_full: { _tagName: 'input', label: 'Show full title', 'label:zh': '显示完整标题', 'label:ja': '完全なタイトルを表示', 'label:ko': '전체 제목 표시', 'label:id': 'Tampilkan judul lengkap', 'label:es': 'Mostrar título completo', 'label:pt': 'Mostrar título completo', 'label:fr': 'Afficher le titre complet', 'label:it': 'Mostra il titolo completo', 'label:de': 'Vollständigen Titel anzeigen', 'label:pl': 'Pokaż pełny tytuł', 'label:ua': 'Показати повну назву', type: 'checkbox' }, thumbnails_title_normalize_smart_max_words: { _tagName: 'input', label: 'Max words in uppercase', 'label:zh': '大写字数上限', 'label:ja': '大文字の最大単語数', 'label:ko': '대문자의 최대 단어 수', 'label:id': 'Maks kata dalam huruf besar', 'label:es': 'Máximo de palabras en mayúsculas', 'label:pt': 'Máximo de palavras em maiúsculas', 'label:fr': 'Mots maximum en majuscules', 'label:it': 'Max parole in maiuscolo', 'label:de': 'Maximale Wörter in Großbuchstaben', 'label:pl': 'Maksymalna liczba słów pisanych wielkimi literami', 'label:ua': 'Максимальна кількість слів ВЕЛИКИМИ літерами', type: 'number', placeholder: '1-10', min: 1, max: 10, value: 2, }, thumbnails_title_clear_emoji: { _tagName: 'input', label: 'Remove emoji', 'label:zh': '从表情符号中清除标题', 'label:ja': 'クリア絵文字', 'label:ko': '이모티콘 지우기', 'label:id': 'Hapus emoji', 'label:es': 'Borrar emoji', 'label:pt': 'Limpar emoji', 'label:fr': 'Emoji clair', 'label:it': 'Emoji trasparenti', 'label:de': 'Emoji löschen', 'label:pl': 'Usuń emoji', 'label:ua': 'Очистити емодзі', type: 'checkbox', }, thumbnails_title_clear_symbols: { _tagName: 'input', label: 'Remove symbols', type: 'checkbox', }, } }); window.nova_plugins.push({ id: 'collapse-navigation-panel', title: 'Collapse navigation panel', run_on_pages: '*, -watch, -embed, -live_chat', section: 'other', _runtime: user_settings => { NOVA.waitSelector('#guide[opened]') .then(el => { document.getElementById('guide-button').click(); el.removeAttribute('opened'); }); }, }); window.nova_plugins.push({ id: 'shorts-redirect', title: 'Redirect Shorts to regular (watch) URLs', 'title:zh': '将 Shorts 重定向到常规(watch)URL', 'title:ja': 'ショートパンツを通常の(watch)URLにリダイレクトする', 'title:ko': 'Shorts를 일반(watch) URL로 리디렉션', 'title:id': 'Redirect Shorts ke URL reguler (watch)', 'title:es': 'Redirigir Shorts a URL normales (watch)', 'title:pt': 'Redirecionar Shorts para URLs regulares (watch)', 'title:fr': 'Rediriger les shorts vers des URL normales (watch)', 'title:it': 'Reindirizza i cortometraggi a URL normali (watch).', 'title:de': 'Leiten Sie Shorts zu regulären (watch) URLs um', 'title:pl': 'Przełączaj Shorts na zwykłe adresy URL', 'title:ua': 'Перенаправляйте прев`ю на звичайні URL-адреси (для перегляду)', run_on_pages: 'shorts', restart_on_location_change: true, section: 'player', desc: 'Redirect Shorts video to normal player', 'desc:zh': '将 Shorts 视频重定向到普通播放器', 'desc:ja': 'ショートパンツのビデオを通常のプレーヤーにリダイレクトする', 'desc:ko': 'Shorts 비디오를 일반 플레이어로 리디렉션', 'desc:id': 'Redirect video Shorts ke pemutar normal', 'desc:es': 'Redirigir el video de Shorts al reproductor normal', 'desc:pt': 'Redirecionar o vídeo do Shorts para o player normal', 'desc:fr': 'Rediriger la vidéo Short vers un lecteur normal', 'desc:it': 'Reindirizza il video dei cortometraggi al lettore normale', 'desc:de': 'Shorts-Video auf normalen Player umleiten', 'desc:pl': 'Przełącza krótkie filmy do normalnego odtwarzacza', 'desc:ua': 'Перенаправляйте прев`ю відео у звичайний відтворювач', _runtime: user_settings => { location.href = location.href.replace('shorts/', 'watch?v='); }, }); window.nova_plugins.push({ id: 'rss-link', title: 'Add RSS feed link', 'title:zh': '添加 RSS 提要链接', 'title:ja': 'RSSフィードリンクを追加', 'title:ko': 'RSS 피드 링크 추가', 'title:id': 'Tambahkan tautan Umpan RSS', 'title:es': 'Agregar enlace de fuente RSS', 'title:pt': 'Adicionar link de feed RSS', 'title:fr': 'Ajouter un lien de flux RSS', 'title:it': 'Aggiungi collegamento al feed RSS', 'title:de': 'RSS-Feed-Link hinzufügen', 'title:pl': 'Dodaj kanał RSS', 'title:ua': 'Додати RSS-посилання', run_on_pages: 'channel, playlist, -mobile', restart_on_location_change: true, section: 'channel', _runtime: user_settings => { const SELECTOR_ID = 'nova-rss-link', rssLinkPrefix = '/feeds/videos.xml', playlistURL = rssLinkPrefix + '?playlist_id=' + NOVA.queryURL.get('list'), genChannelURL = channelId => rssLinkPrefix + '?channel_id=' + channelId; switch (NOVA.currentPage) { case 'channel': NOVA.waitSelector('#channel-header #links-holder #primary-links') .then(container => { if (!parseInt(NOVA.css.getValue('#header div.banner-visible-area', 'height'))) { container = document.body.querySelector('#channel-header #inner-header-container #buttons'); } if (url = (document.querySelector('link[type="application/rss+xml"][href]')?.href || genChannelURL(NOVA.getChannelId(user_settings['user-api-key']))) ) { insertToHTML({ 'url': url, 'container': container }); } }); break; case 'playlist': NOVA.waitSelector('ytd-playlist-header-renderer .metadata-buttons-wrapper', { destroy_if_url_changes: true }) .then(container => { insertToHTML({ 'url': playlistURL, 'container': container, 'is_playlist': true }); }); break; } function insertToHTML({ url = required(), container = required(), is_playlist }) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); (container.querySelector(`#${SELECTOR_ID}`) || (function () { const link = document.createElement('a'); link.id = SELECTOR_ID; link.target = '_blank'; link.className = `yt-spec-button-shape-next--overlay`; link.innerHTML = `<svg viewBox="-35 -35 55 55" height="100%" width="100%" style="width: auto;"> <g fill="currentColor"> <path fill="#F60" d="M-17.392 7.875c0 3.025-2.46 5.485-5.486 5.485s-5.486-2.46-5.486-5.485c0-3.026 2.46-5.486 5.486-5.486s5.486 2.461 5.486 5.486zm31.351 5.486C14.042.744 8.208-11.757-1.567-19.736c-7.447-6.217-17.089-9.741-26.797-9.708v9.792C-16.877-19.785-5.556-13.535.344-3.66a32.782 32.782 0 0 1 4.788 17.004h8.827v.017zm-14.96 0C-.952 5.249-4.808-2.73-11.108-7.817c-4.821-3.956-11.021-6.184-17.255-6.15v8.245c6.782-.083 13.432 3.807 16.673 9.774a19.296 19.296 0 0 1 2.411 9.326h8.278v-.017z"/> </g> </svg>`; Object.assign(link.style, { height: '20px', display: 'inline-block', padding: '5px', }); if (is_playlist) { Object.assign(link.style, { 'margin-right': '8px', 'border-radius': '20px', 'background-color': 'var(--yt-spec-static-overlay-button-secondary)', color: 'var(--yt-spec-static-overlay-text-primary)', padding: '8px', 'margin-right': '8px', 'white-space': 'nowrap', 'font-size': 'var(--ytd-tab-system-font-size, 1.4rem)', 'font-weight': 'var(--ytd-tab-system-font-weight, 500)', 'letter-spacing': 'var(--ytd-tab-system-letter-spacing, .007px)', 'text-transform': 'var(--ytd-tab-system-text-transform, uppercase)', }); } container.prepend(link); return link; })()) .href = url; } }, }); window.nova_plugins.push({ id: 'channel-default-tab', title: 'Default tab on channel page', 'title:zh': '频道页默认选项卡', 'title:ja': 'チャンネルページのデフォルトタブ', 'title:ko': '채널 페이지의 기본 탭', 'title:id': 'Tab default di halaman saluran', 'title:es': 'La pestaña predeterminada en la página del canal', 'title:pt': 'A guia padrão na página do canal', 'title:fr': 'Onglet par défaut sur la page de la chaîne', 'title:it': 'Scheda predefinita nella pagina del canale', 'title:de': 'Die Standardregisterkarte auf der Kanalseite', 'title:pl': 'Domyślna karta na stronie kanału', 'title:ua': 'Вкладка за умовчанням на сторінці каналу', run_on_pages: 'channel', restart_on_location_change: true, section: 'channel', _runtime: user_settings => { if (NOVA.channelTab) return; if (user_settings.channel_default_tab_mode == 'redirect') { location.pathname += '/' + user_settings.channel_default_tab; } else { const tabSelectors = '#tabsContent [role="tab"]'; NOVA.waitSelector(tabSelectors, { destroy_if_url_changes: true }) .then(() => { let tabActive; const tabs = [...document.body.querySelectorAll(tabSelectors)]; switch (user_settings.channel_default_tab) { case 'videos': tabActive = tabs[1]; break; case 'playlists': tabActive = tabs[tabs.length - 4]; break; case 'community': tabActive = tabs[tabs.length - 3]; break; case 'about': tabActive = tabs.pop(); break; default: location.pathname += '/' + user_settings.channel_default_tab; } tabActive?.click(); }); } }, options: { channel_default_tab: { _tagName: 'select', label: 'Default tab', 'label:zh': '默认标签页', 'label:ja': 'デフォルトのタブ', 'label:ko': '기본 탭', 'label:id': 'tab bawaan', 'label:es': 'Ficha predeterminada', 'label:pt': 'Aba padrão', 'label:fr': 'Onglet par défaut', 'label:it': 'Scheda predefinita', 'label:de': 'Standard-Tab', 'label:pl': 'Domyślna karta', 'label:ua': 'Вкладка за умовчанням', options: [ { label: 'videos', value: 'videos', selected: true, 'label:pl': 'wideo', 'label:ua': 'відео', }, { label: 'shorts', value: 'shorts', }, { label: 'live', value: 'streams', }, { label: 'playlists', value: 'playlists', 'label:pl': 'playlista', 'label:ua': 'плейлисти', }, { label: 'community', value: 'community', }, { label: 'about', value: 'about', 'label:pl': 'o kanale', 'label:ua': 'про канал', }, ], }, channel_default_tab_mode: { _tagName: 'select', label: 'Mode', 'label:zh': '模式', 'label:ja': 'モード', 'label:ko': '방법', 'label:es': 'Modo', 'label:pt': 'Modo', 'label:it': 'Modalità', 'label:de': 'Modus', 'label:pl': 'Tryb', 'label:ua': 'Режим', title: 'Redirect is safer but slower', 'title:zh': '重定向是安全的,但速度很慢', 'title:ja': 'リダイレクトは安全ですが遅くなります', 'title:ko': '리디렉션이 더 안전하지만 느립니다', 'title:id': 'Redirect lebih aman tetapi lebih lambat', 'title:es': 'La redirección es más segura pero más lenta', 'title:pt': 'O redirecionamento é mais seguro, mas mais lento', 'title:fr': 'La redirection est plus sûre mais plus lente', 'title:it': 'Il reindirizzamento è più sicuro ma più lento', 'title:de': 'Redirect ist sicherer, aber langsamer', 'title:pl': 'Przekierowanie jest bezpieczniejsze, ale wolniejsze', 'title:ua': 'Перенаправлення безпечніше, але повільніше', options: [ { label: 'redirect', value: 'redirect', 'label:pl': 'przekierowanie', 'label:ua': 'перенаправити', }, { label: 'click', selected: true, 'label:pl': 'klik', 'label:ua': 'клік', }, ], 'data-dependent': { 'channel_default_tab': ['videos', 'playlists', 'community', 'about'] }, }, } }); window.nova_plugins.push({ id: 'thumbnails-grid-count', title: 'Thumbnails count in row', run_on_pages: 'feed, channel, -mobile', section: 'other', _runtime: user_settings => { const origMathMin = Math.min, addRowCount = +user_settings.thumbnails_grid_count || 1; Math.min = function () { return origMathMin.apply(Math, arguments) + (/calcElementsPerRow/img.test(Error().stack || '') ? addRowCount - 1 : 0); }; }, options: { thumbnails_grid_count: { _tagName: 'input', label: 'Add to row', type: 'number', placeholder: '1-10', step: 1, min: 1, max: 10, value: 1, }, } }); window.nova_plugins.push({ id: 'thumbs-preview-stop', title: 'Autostop thumbnail playback on hover', run_on_pages: 'home, feed, -mobile', section: 'other', _runtime: user_settings => { NOVA.waitSelector('#inline-preview-player') .then(player => { NOVA.waitSelector('video', { 'container': player }) .then(video => { video.addEventListener('play', () => player.stopVideo()); }); }); }, }); window.nova_plugins.push({ id: 'ad-skip-button', title: 'Ad intro skip', 'title:zh': '广告视频跳过', 'title:ja': '広告スキップ', 'title:ko': '광고 건너뛰기', 'title:id': 'Intro iklan Lewati', 'title:es': 'Saltar anuncios', 'title:pt': 'Pular anúncios', 'title:fr': 'Ignorer les annonces', 'title:it': 'Salta introduttivo', 'title:de': 'Anzeigen überspringen', 'title:pl': 'Pomiń początkową reklamę', //'title:ua': 'Натиснути пропустити рекламу', 'title:ua': 'Кнопка пропустити рекламу', run_on_pages: 'watch', section: 'player', desc: 'Auto click on the [Skip Ad] button', 'desc:zh': '自动点击“Skip Ad”按钮', 'desc:ja': '「Skip Ad」ボタンの自動クリック', 'desc:ko': '【광고 건너뛰기】버튼 자동 클릭', 'desc:id': 'Klik otomatis pada tombol [Lewati Iklan]', 'desc:es': 'Haga clic automáticamente en el botón [Omitir anuncio]', 'desc:pt': 'Clique automaticamente no botão [Ignorar anúncio]', 'desc:fr': "Clic automatique sur le bouton [Ignorer l'annonce]", 'desc:it': 'Fare clic automaticamente sul pulsante [Salta annuncio].', 'desc:pl': 'Auto kliknięcie przycisku [Pomiń reklamę]', 'desc:ua': 'Автоматично натискати кнопку для пропуску реклами', _runtime: user_settings => { NOVA.waitSelector('#movie_player.ad-showing video') .then(video => { adSkip(); movie_player.addEventListener('onAdStateChange', adSkip.bind(video)); video.addEventListener('loadedmetadata', adSkip.bind(video)); video.addEventListener('loadeddata', adSkip.bind(video)); video.addEventListener('canplay', adSkip.bind(video)); }); function adSkip() { if (!movie_player.classList.contains('ad-showing')) return; this.currentTime = this.duration; NOVA.waitSelector('div.ytp-ad-text.ytp-ad-skip-button-text:not([hidden]), button.ytp-ad-skip-button:not([hidden])', { destroy_if_url_changes: true }) .then(btn => btn.click()); } }, }); window.nova_plugins.push({ id: 'player-control-below', title: 'Control panel below the player', 'title:ua': 'Панель керування під плеєром', run_on_pages: 'watch, -mobile', section: 'player', _runtime: user_settings => { NOVA.waitSelector('.ytp-chrome-bottom') .then(async control_panel => { if ((heightPanel = NOVA.css.getValue(control_panel, 'height')) && (heightProgressBar = NOVA.css.getValue('.ytp-progress-bar-container', 'height')) ) { const height = `calc(${heightPanel} + ${heightProgressBar})` || '51px'; let SELECTOR_CONTAINER = 'ytd-watch-flexy:not([fullscreen])'; if (['cinema_mode', 'force', 'offset'].includes(user_settings.player_full_viewport_mode) ) { SELECTOR_CONTAINER += `:not([theater])`; } NOVA.css.push( ` ${SELECTOR_CONTAINER} .ytp-caption-window-bottom { margin-bottom: 0; } ${SELECTOR_CONTAINER} .ytp-gradient-bottom { transform: translateY(${height}); display: block !important; opacity: 1 !important; height: ${height} !important; padding: 0; background-color: #0f0f0f; } ${SELECTOR_CONTAINER} .ytp-chrome-bottom { transform: translateY(${height}); opacity: 1 !important; } ${SELECTOR_CONTAINER} .html5-video-player { overflow: visible; } ${SELECTOR_CONTAINER} .ytp-player-content.ytp-iv-player-content { bottom: ${NOVA.css.getValue('.ytp-player-content.ytp-iv-player-content', 'left') || '12px'}; } ${SELECTOR_CONTAINER} .ytp-tooltip, ${SELECTOR_CONTAINER} .ytp-settings-menu { transform: translateY(${height}); } ${SELECTOR_CONTAINER}[theater] > #columns, ${SELECTOR_CONTAINER}:not([theater]) #below { margin-top: ${height} !important; } #ytd-player { overflow: visible !important; } `); if (user_settings['player-float-progress-bar']) { NOVA.css.push( `#movie_player.ytp-autohide .ytp-chrome-bottom .ytp-progress-bar-container { display: none !important; }`); } fixControlFreeze(); } }); function fixControlFreeze(ms = 2000) { if (user_settings.player_hide_elements?.includes('time_display') && (user_settings['theater-mode'] && ['cinema_mode', 'force', 'offset'].includes(user_settings.player_full_viewport_mode)) ) { return; } return window.setInterval(() => { if (['smart'].includes(user_settings.player_full_viewport_mode) && NOVA.css.getValue(movie_player, 'z-index') != '2020' && NOVA.css.getValue(movie_player, 'position') != 'fixed') return; if (NOVA.currentPage === 'watch' && document.visibilityState == 'visible' && movie_player.classList.contains('playing-mode') && !NOVA.isFullscreen() ) { movie_player.wakeUpControls(); } }, ms); } }, }); window.nova_plugins.push({ id: 'player-hide-elements', title: 'Hide some player buttons/elements', run_on_pages: 'watch, embed, -mobile', section: 'player', _runtime: user_settings => { const SELECTORS = { 'videowall_endscreen': '#movie_player .videowall-endscreen', 'card_endscreen': '#movie_player [class^="ytp-ce-"]', 'prev_button': '#movie_player .ytp-chrome-bottom .ytp-prev-button', 'play_button': '#movie_player .ytp-chrome-bottom .ytp-play-button', 'next_button': '#movie_player .ytp-chrome-bottom .ytp-next-button', 'volume_area': '#movie_player .ytp-chrome-bottom .ytp-volume-area', 'time_display': '#movie_player .ytp-chrome-bottom .ytp-time-display' + (user_settings['time-remaining'] ? ' span > span:not([id])' : ''), 'time_duration_display': '#movie_player .ytp-chrome-bottom .ytp-time-duration, #movie_player .ytp-chrome-bottom .ytp-time-separator', 'chapter_container': '#movie_player .ytp-chrome-bottom .ytp-chapter-container', 'autonav_toggle_button': '#movie_player .ytp-chrome-bottom button.ytp-button[data-tooltip-target-id="ytp-autonav-toggle-button"]', 'subtitles_button': '#movie_player .ytp-chrome-bottom button.ytp-subtitles-button', 'settings_button': '#movie_player .ytp-chrome-bottom button.ytp-settings-button', 'size_button': '#movie_player .ytp-chrome-bottom button.ytp-size-button', 'miniplayer_button': '#movie_player .ytp-chrome-bottom button.ytp-miniplayer-button', 'logo_button': '#movie_player .ytp-chrome-bottom .yt-uix-sessionlink', 'fullscreen_button': '#movie_player .ytp-chrome-bottom button.ytp-fullscreen-button', 'brave_jump_button': '#movie_player .ytp-chrome-bottom button.ytp-jump-button', }; const toArray = a => Array.isArray(a) ? a : [a]; let list = []; toArray(user_settings.player_hide_elements) .forEach(el => (data = SELECTORS[el]) && list.push(data)); if (list.length) { NOVA.css.push( list.join(',\n') + ` { display: none !important; }`); } }, options: { player_hide_elements: { _tagName: 'select', label: 'Items', title: '[Ctrl+Click] to select several', 'title:zh': '[Ctrl+Click] 选择多个', 'title:ja': '「Ctrl+Click」して、いくつかを選択します', 'title:ko': '[Ctrl+Click] 여러 선택', 'title:id': '[Ctrl+Klik] untuk memilih beberapa', 'title:es': '[Ctrl+Click] para seleccionar varias', 'title:pt': '[Ctrl+Click] para selecionar vários', 'title:fr': '[Ctrl+Click] pour sélectionner plusieurs', 'title:it': '[Ctrl+Clic] per selezionarne diversi', 'title:de': '[Ctrl+Click] um mehrere auszuwählen', 'title:pl': 'Ctrl+kliknięcie, aby zaznaczyć kilka', 'title:ua': '[Ctrl+Click] щоб обрати декілька', multiple: null, required: true, size: 10, options: [ { label: 'videowall (thumbs)', value: 'videowall_endscreen', }, { label: 'card', value: 'card_endscreen', }, { label: 'prev', value: 'prev_button', }, { label: 'play', value: 'play_button', }, { label: 'next', value: 'next_button', }, { label: 'jump (for Brave)', value: 'brave_jump_button', title: 'Seek backwards/forward 10 seconds' }, { label: 'volume', value: 'volume_area', }, { label: 'time', value: 'time_display', }, { label: 'time duration', value: 'time_duration_display', }, { label: 'chapter', value: 'chapter_container', }, { label: 'autonav toggle', value: 'autonav_toggle_button', }, { label: 'subtitles', value: 'subtitles_button', }, { label: 'settings', value: 'settings_button', }, { label: 'size', value: 'size_button', }, { label: 'miniplayer', value: 'miniplayer_button', }, { label: 'logo (embed)', value: 'logo_button', }, { label: 'fullscreen', value: 'fullscreen_button', }, ], }, } }); window.nova_plugins.push({ id: 'player-resize-ratio', title: 'Player force resize 16:9', 'title:ua': 'Примусова зміна розміру програвача 16:9', run_on_pages: 'watch', section: 'player', desc: 'only for 4:3 video', 'desc:ua': 'Лише для відео розміром 4:3', _runtime: user_settings => { NOVA.waitSelector('ytd-watch-flexy:not([theater])') .then(ytd_watch => { NOVA.waitSelector('#movie_player video') .then(video => { console.assert(ytd_watch.calculateCurrentPlayerSize_, '"ytd_watch" does not have fn "calculateCurrentPlayerSize_"'); const heightRatio = .5625, check4to3 = () => '4:3' == NOVA.aspectRatio.getAspectRatio({ 'width': video.videoWidth, 'height': video.videoHeight, }); if (ytd_watch.calculateCurrentPlayerSize_ && ytd_watch.updateStyles) { const backupFn = ytd_watch.calculateCurrentPlayerSize_; patchYtCalculateFn(); video.addEventListener('loadeddata', patchYtCalculateFn); function sizeBypass() { let width = height = NaN; if (!ytd_watch.theater) { width = movie_player.offsetWidth; height = Math.round(movie_player.offsetWidth / (16 / 9)); if (ytd_watch.updateStyles) { ytd_watch.updateStyles({ '--ytd-watch-flexy-width-ratio': 1, '--ytd-watch-flexy-height-ratio': heightRatio, }); window.dispatchEvent(new Event('resize')); } } return { 'width': width, 'height': height, }; } function patchYtCalculateFn() { ytd_watch.calculateCurrentPlayerSize_ = check4to3() ? sizeBypass : backupFn; } } else { new MutationObserver(mutationRecordsArray => { if (!ytd_watch.theater && heightRatio != ytd_watch.style.getPropertyValue('--ytd-watch-flexy-height-ratio')) { updateRatio(); } }) .observe(ytd_watch, { attributes: true, attributeFilter: ['style'] }); } window.addEventListener('resize', updateRatio); function updateRatio() { if (check4to3()) { ytd_watch.style.setProperty('--ytd-watch-flexy-width-ratio', 1); ytd_watch.style.setProperty('--ytd-watch-flexy-height-ratio', heightRatio); } } }); }); }, }); window.nova_plugins.push({ id: 'player-hotkeys-focused', title: 'Player shortcuts always active', 'title:zh': '播放器热键始终处于活动状态', 'title:ja': 'プレーヤーのホットキーは常にアクティブです', 'title:ko': '플레이어 단축키는 항상 활성화되어 있습니다', 'title:id': 'Tombol pintas pemain selalu aktif', 'title:es': 'Teclas de acceso rápido del jugador siempre activas', 'title:pt': 'Teclas de atalho do jogador sempre ativas', 'title:fr': 'Les raccourcis clavier du joueur sont toujours actifs', 'title:it': 'Tasti di scelta rapida del giocatore sempre attivi', 'title:de': 'Player-Hotkeys immer aktiv', 'title:pl': 'Klawisze skrótów dla graczy zawsze aktywne', 'title:ua': 'Гарячі клавіші відтворювача завжди активні', run_on_pages: 'watch, embed, -mobile', section: 'player', _runtime: user_settings => { document.addEventListener('keydown', evt => { setPlayerFocus(evt.target); if (user_settings.hotkeys_disable_numpad && evt.code.startsWith('Numpad')) { evt.preventDefault(); evt.stopPropagation(); evt.stopImmediatePropagation(); } }); document.addEventListener('click', evt => evt.isTrusted && setPlayerFocus(evt.target)); function setPlayerFocus(target) { if (['input', 'textarea', 'select'].includes(target.localName) || target.isContentEditable) return; movie_player.focus({ preventScroll: true }); } }, options: { hotkeys_disable_numpad: { _tagName: 'input', label: 'Disable numpad', type: 'checkbox', }, } }); window.nova_plugins.push({ id: 'disable-player-sleep-mode', title: 'Disable the "Continue watching?" popup', 'title:zh': '玩家永远保持活跃', 'title:ja': 'プレーヤーは永遠にアクティブなままです', 'title:ko': '플레이어는 영원히 활성 상태를 유지', 'title:id': 'Pemain tetap aktif selamanya', 'title:es': 'El jugador permanece activo para siempre', 'title:pt': 'Jogador permanece ativo para sempre', 'title:fr': 'Le joueur reste actif pour toujours', 'title:it': 'Il giocatore resta attivo per sempre', 'title:de': 'Spieler bleiben für immer aktiv', 'title:pl': 'Wyłącz tryb uśpienia odtwarzacza', 'title:ua': 'Вимкнути режим сну відтворювача', run_on_pages: 'watch, -mobile', section: 'player', _runtime: user_settings => { window.setInterval(() => { if (!document.hasFocus()) { document.dispatchEvent( new KeyboardEvent('keyup', { bubbles: true, cancelable: true, keyCode: 143, which: 143 }) ); } }, 1000 * 60 * 5); }, }); window.nova_plugins.push({ id: 'embed-show-control-force', title: 'Force enable control panel (for embed)', 'title:zh': '埋め込みでコントロール パネルを強制的に有効にする', 'title:ja': '强制启用嵌入的控制面板', 'title:ko': '임베디드에서 강제 활성화 제어판', 'title:id': 'Paksa aktifkan panel kontrol di sematan', 'title:es': 'Forzar habilitar el panel de control en incrustar', 'title:pt': 'Forçar ativação do painel de controle na incorporação', 'title:fr': "Forcer l'activation du panneau de contrôle dans l'intégration", 'title:it': "Forza l'abilitazione del pannello di controllo nell'incorporamento", 'title:de': 'Erzwingen Sie die Aktivierung des Bedienfelds in der Einbettung', 'title:pl': 'Wymuś włączenie panelu sterowania w osadzeniu', 'title:ua': 'Примусово показувати панель керування у вбудованому відео', run_on_pages: 'embed', section: 'player', _runtime: user_settings => { const href = location.href.replace(/&/g, '&'); if (['0', 'false'].includes(NOVA.queryURL.get('controls', href))) { NOVA.updateUrl(NOVA.queryURL.remove('controls', href)); } }, }); window.nova_plugins.push({ id: 'time-jump', title: 'Time jump', 'title:zh': '时间跳跃', 'title:ja': 'タイムジャンプ', 'title:ko': '시간 점프', 'title:id': 'Lompatan waktu', 'title:es': 'Salto de tiempo', 'title:pt': 'Salto no tempo', 'title:fr': 'Saut dans le temps', 'title:it': 'Salto nel tempo', 'title:de': 'Zeitsprung', 'title:pl': 'Skok czasowy', 'title:ua': 'Стрибок часу', run_on_pages: 'watch, embed, -mobile', section: 'player', desc: 'Use to skip the intro or ad inserts', 'desc:zh': '用于跳过介绍或广告插入', 'desc:ja': 'イントロや広告挿入をスキップするために使用します', 'desc:ko': '인트로 또는 광고 삽입을 건너뛸 때 사용', 'desc:id': 'Gunakan untuk melewati intro atau sisipan iklan', 'desc:pt': 'Use para pular a introdução ou inserções de anúncios', 'desc:fr': "Utiliser pour ignorer l'intro ou les encarts publicitaires", 'desc:pl': 'Służy do pomijania wstępu lub wstawek reklamowych', 'desc:ua': 'Використовуйте щоб пропустити інтро', _runtime: user_settings => { if (user_settings.time_jump_title_offset) addTitleOffset(); NOVA.waitSelector('#movie_player video') .then(video => { let chapterList; video.addEventListener('loadeddata', () => chapterList = []); doubleKeyPressListener(timeLeap, user_settings.time_jump_hotkey); function timeLeap() { if (movie_player.getVideoData().isLive || (NOVA.currentPage == 'embed' && window.self.location.href.includes('live_stream')) ) return; if (chapterList !== null && !chapterList?.length) { chapterList = NOVA.getChapterList(movie_player.getDuration()) || null; } const currentTime = movie_player.getCurrentTime(), nextChapterIndex = chapterList?.findIndex(c => c.sec > currentTime), separator = ' • '; let msg; if (chapterList?.length && nextChapterIndex !== -1 ) { const nextChapterData = chapterList?.find(({ sec }) => sec >= currentTime); seekTime(nextChapterData.sec + .5); msg = nextChapterData.title + separator + nextChapterData.time; } else { seekTime(+user_settings.time_jump_step + currentTime); msg = `+${user_settings.time_jump_step} sec` + separator + NOVA.timeFormatTo.HMS.digit(currentTime); } NOVA.triggerHUD(msg); } function seekTime(sec) { if (typeof movie_player.seekBy === 'function') { movie_player.seekTo(sec); } else if (NOVA.videoElement) { NOVA.videoElement.currentTime = sec; } else { const errorText = '[time-jump] > "seekTime" detect player error' console.error(errorText); throw errorText; } } }); function addTitleOffset() { NOVA.css.push( `.ytp-tooltip-text:after { content: attr(data-before); color: #ffcc00; }`); NOVA.waitSelector('.ytp-progress-bar') .then(progressContainer => { if (tooltipEl = document.body.querySelector('.ytp-tooltip-text')) { progressContainer.addEventListener('mousemove', () => { if (movie_player.getVideoData().isLive || (NOVA.currentPage == 'embed' && window.self.location.href.includes('live_stream')) ) return; const cursorTime = NOVA.timeFormatTo.hmsToSec(tooltipEl.textContent), offsetTime = cursorTime - NOVA.videoElement?.currentTime, sign = (offsetTime >= 1) ? '+' : (Math.sign(offsetTime) === -1) ? '-' : ''; tooltipEl.setAttribute('data-before', ` ${sign + NOVA.timeFormatTo.HMS.digit(offsetTime)}`); }); progressContainer.addEventListener('mouseleave', () => tooltipEl.removeAttribute('data-before')); } }); } function doubleKeyPressListener(callback, keyCodeFilter) { let pressed, isDoublePress, lastPressed = parseInt(keyCodeFilter) || null; const timeOut = () => setTimeout(() => isDoublePress = false, 500), handleDoublePresss = key => { if (callback && typeof callback === 'function') return callback(key); }; function keyPress(evt) { if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return; pressed = evt.keyCode; if (isDoublePress && pressed === lastPressed) { isDoublePress = false; handleDoublePresss(evt); } else { isDoublePress = true; timeOut(); } if (!keyCodeFilter) lastPressed = pressed; } document.addEventListener('keyup', keyPress); } if (user_settings['save-channel-state']) { NOVA.waitSelector('#movie_player video') .then(video => { NOVA.runOnPageInitOrTransition(async () => { const CACHE_PREFIX = 'nova-resume-playback-time', getCacheName = () => CACHE_PREFIX + ':' + (NOVA.queryURL.get('v') || movie_player.getVideoData().video_id); if ((NOVA.currentPage == 'watch' || NOVA.currentPage == 'embed') && !+sessionStorage.getItem(getCacheName()) && !NOVA.queryURL.has('t') && (userSeek = await NOVA.storage_obj_manager.getParam('skip-into')) ) { video.addEventListener('canplay', timeLeapInto.apply(video, [userSeek]), { capture: true, once: true }); } }); }); } else if (+user_settings.skip_into_step) { NOVA.waitSelector('#movie_player video') .then(video => { NOVA.runOnPageInitOrTransition(() => { if (NOVA.currentPage == 'watch') { video.addEventListener('canplay', timeLeapInto.bind(video), { capture: true, once: true }); } }); }); } function timeLeapInto(time_seek = user_settings.skip_into_step || 10) { if (!time_seek && !user_settings.skip_into_step_in_music && NOVA.isMusic()) return; const CACHE_PREFIX = 'resume-playback-time', getCacheName = () => CACHE_PREFIX + ':' + (NOVA.queryURL.get('v') || movie_player.getVideoData().video_id); if (user_settings['player-resume-playback'] && (saveTime = +sessionStorage.getItem(getCacheName())) && (saveTime > (this.duration - 3)) ) return; if ((isNaN(this.duration) || this.duration > 30) && this.currentTime < (+user_settings.skip_into_step || +time_seek) ) { this.currentTime = +time_seek; } } }, options: { time_jump_step: { _tagName: 'input', label: 'Step time', 'label:zh': '步骤时间', 'label:ko': '단계 시간', 'label:id': 'Langkah waktu', 'label:es': 'Tiempo de paso', 'label:pt': 'Tempo da etapa', 'label:fr': 'Temps de pas', 'label:it': 'Tempo di passaggio', 'label:de': 'Schrittzeit', 'label:pl': 'Krok czasowy', 'label:ua': 'Крок часу', type: 'number', title: 'In seconds', placeholder: 'sec', min: 3, max: 300, value: 30, }, time_jump_hotkey: { _tagName: 'select', label: 'Hotkey (double click)', 'label:zh': '热键(双击)', 'label:ja': 'Hotkey (ダブルプレス)', 'label:ko': '단축키(더블 클릭)', 'label:id': 'Tombol pintas (klik dua kali)', 'label:es': 'Tecla de acceso rápido (doble clic)', 'label:pt': 'Atalho (duplo clique)', 'label:fr': 'Raccourci clavier (double clic)', 'label:it': 'Tasto di scelta rapida (doppio clic)', 'label:de': 'Hotkey (Doppelklick)', 'label:pl': 'Klawisz skrótu (podwójne kliknięcie)', 'label:ua': 'Гаряча клавіша (двічі натиснути)', options: [ { label: 'shift', value: 16 }, { label: 'ctrl', value: 17, selected: true }, { label: 'alt', value: 18 }, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '[', ']', '[', '+', '-', ',', '.', '/', '<', ';', '\\' ], }, time_jump_title_offset: { _tagName: 'input', label: 'Show time offset on progress bar', 'label:zh': '在进度条中显示时间偏移', 'label:ja': 'プログレスバーに時間オフセットを表示する', 'label:ko': '진행률 표시줄에 시간 오프셋 표시', 'label:id': 'Tampilkan offset waktu di bilah kemajuan', 'label:es': 'Mostrar compensación de tiempo en la barra de progreso', 'label:pt': 'Mostrar a diferença de tempo na barra de progresso', 'label:fr': 'Afficher le décalage horaire sur la barre de progression', 'label:it': "Mostra l'offset di tempo sulla barra di avanzamento", 'label:de': 'Zeitverschiebung im Fortschrittsbalken anzeigen', 'label:pl': 'Pokaż przesunięcie czasu na pasku postępu', 'label:ua': 'Показувати часовий зсув на панелі прогресу', type: 'checkbox', title: 'Time offset from current playback time', 'title:zh': '与当前播放时间的时间偏移', 'title:ja': '現在の再生時間からの時間オフセット', 'title:ko': '현재 재생 시간으로부터의 시간 오프셋', 'label:id': 'Waktu offset dari waktu pemutaran saat ini', 'title:es': 'Desfase de tiempo del tiempo de reproducción actual', 'title:pt': 'Deslocamento de tempo do tempo de reprodução atual', 'title:fr': "Décalage temporel par rapport à l'heure de lecture actuelle", 'title:it': 'Spostamento temporale dal tempo di riproduzione corrente', 'title:de': 'Zeitverschiebung zur aktuellen Wiedergabezeit', 'title:pl': 'Przesunięcie czasu względem bieżącego czasu odtwarzania', 'title:ua': 'Часовий зсув відносно поточного часу відтворення', }, skip_into_step: { _tagName: 'input', label: 'Start playback at', 'label:zh': '设置开始时间', 'label:ja': '開始時刻を設定', 'label:ko': '시작 시간 설정', 'label:id': 'Tetapkan waktu mulai', 'label:es': 'Establecer hora de inicio', 'label:pt': 'Definir horário de início', 'label:fr': "Définir l'heure de début", 'label:it': "Imposta l'ora di inizio", 'label:de': 'Startzeit festlegen', 'label:pl': 'Ustaw czas rozpoczęcia', 'label:ua': 'Встановіть час початку', type: 'number', title: 'in sec / 0 - disable', placeholder: '1-30', step: 1, min: 0, max: 30, value: 0, }, skip_into_step_in_music: { _tagName: 'input', label: 'Apply for music genre', type: 'checkbox', 'data-dependent': { 'skip_into_step': "!0" }, }, } }); window.nova_plugins.push({ id: 'player-indicator', title: 'Replace HUD (bezel)', 'title:zh': '替换默认指示器', 'title:ja': 'デフォルトのインジケーターを置き換える', 'title:ko': '기본 표시기 교체', 'title:id': 'Ganti HUD (bezel)', 'title:es': 'Reemplazar indicador predeterminado', 'title:pt': 'Substituir o indicador padrão', 'title:fr': "Remplacer l'indicateur par défaut", 'title:it': 'Sostituisci HUD (cornice)', 'title:de': 'Standardkennzeichen ersetzen', 'title:pl': 'Zamień wskaźnik standardowy', 'title:ua': 'Замінити стандартний інтерфейс', run_on_pages: 'watch, embed, -mobile', section: 'player', desc: "'bezel' that's what YouTube called", _runtime: user_settings => { const SELECTOR_ID = 'nova-player-indicator-info', COLOR_HUD = user_settings.player_indicator_color || '#ff0000'; NOVA.waitSelector('#movie_player video') .then(video => { video.addEventListener('volumechange', function () { HUD.set({ 'pt': Math.round(movie_player.getVolume()), 'suffix': '%', }); }); video.addEventListener('ratechange', () => HUD.set({ 'pt': video.playbackRate, 'suffix': 'x', })); if (user_settings.player_indicator_chapter) { NOVA.waitSelector('ytd-watch-metadata #description.ytd-watch-metadata') .then(() => { const getNextChapterIndex = () => chapterList?.findIndex(c => c.sec > ~~video.currentTime); let chapterList, lastChapTime = 0; video.addEventListener('loadeddata', () => chapterList = []); video.addEventListener('timeupdate', function () { if (chapterList !== null && !chapterList?.length) { chapterList = NOVA.getChapterList(movie_player.getDuration()) || null; } if (chapterList?.length && this.currentTime > lastChapTime ) { const nextChapterIndex = getNextChapterIndex(); lastChapTime = chapterList[nextChapterIndex]?.sec; if (chapterData = chapterList[nextChapterIndex - 1]) { const separator = ' • '; const msg = chapterData.title + separator + chapterData.time; NOVA.triggerHUD(msg); } } }); video.addEventListener('seeking', function () { if (chapterList?.length && (nexChapterData = chapterList[getNextChapterIndex()])) { lastChapTime = nexChapterData.sec; } }); }); } }); NOVA.waitSelector('.ytp-bezel-text') .then(target => { new MutationObserver(mutationRecordsArray => { if (target.textContent) { if (target.textContent?.endsWith('%') || target.textContent?.endsWith('x')) { return; } HUD.set({ 'pt': target.textContent, 'timeout_ms': (user_settings.player_indicator_chapter_time || 1.8) * 1000, }); } }) .observe(target, { attributes: true, childList: true }); }); const HUD = { get() { return this.container || this.create(); }, create() { NOVA.css.push( `.ytp-bezel-text-wrapper, .ytp-doubletap-ui-legacy.ytp-time-seeking, .ytp-chapter-seek { display:none !important; }`); NOVA.css.push( `#${SELECTOR_ID} { --color: #fff; --bg-color: rgba(0,0,0,${user_settings.player_indicator_opacity || .3}); --zindex: ${1 + Math.max(NOVA.css.getValue('.ytp-chrome-top', 'z-index'), 60)}; position: absolute; right: 0; z-index: calc(var(--zindex) + 1); margin: 0 auto; text-align: center; opacity: 0; background-color: var(--bg-color); color: var(--color); }`); movie_player.insertAdjacentHTML('beforeend', `<div id="${SELECTOR_ID}"><span></span></div>`); this.container = document.getElementById(SELECTOR_ID); this.hudSpan = this.container.querySelector('span'); switch (user_settings.player_indicator_type) { case 'bar-center': Object.assign(this.container.style, { left: 0, bottom: '20%', width: '30%', 'font-size': '1.2em', }); Object.assign(this.hudSpan.style, { 'background-color': COLOR_HUD, transition: 'width 100ms ease-out 0s', display: 'inline-block', }); break; case 'bar-vertical': Object.assign(this.container.style, { top: 0, height: '100%', width: '25px', 'font-size': '1.2em', }); Object.assign(this.hudSpan.style, { position: 'absolute', bottom: 0, right: 0, 'background-color': COLOR_HUD, transition: 'height 100ms ease-out 0s', display: 'inline-block', width: '100%', 'font-weight': 'bold', }); break; default: Object.assign(this.container.style, { top: 0, width: '100%', padding: '.2em', 'font-size': '1.55em', }); } return this.container; }, set({ pt = 100, suffix = '', timeout_ms = 800 }) { if (typeof this.fateNovaHUD === 'number') clearTimeout(this.fateNovaHUD); let hudContainer = this.get(); const text = pt + suffix; if (suffix == 'x') { const maxPercent = (+user_settings.rate_step % .25) === 0 ? 2 : 3; pt = (+pt / maxPercent) * 100; } pt = Math.round(pt); switch (user_settings.player_indicator_type) { case 'bar-center': this.hudSpan.style.width = pt + '%'; this.hudSpan.textContent = text; break; case 'bar-vertical': this.hudSpan.style.height = pt + '%'; this.hudSpan.textContent = text; break; case 'bar-top': hudContainer.style.background = `linear-gradient(to right, ${COLOR_HUD}50 ${pt}%, rgba(0,0,0,.8) ${pt}%)`; this.hudSpan.style.width = pt + '%'; this.hudSpan.textContent = text; break; default: this.hudSpan.textContent = text; } hudContainer.style.transition = 'none'; hudContainer.style.opacity = 1; this.fateNovaHUD = setTimeout(() => { hudContainer.style.transition = 'opacity 200ms ease-in'; hudContainer.style.opacity = null; }, timeout_ms); //total 1s = 800ms + 200ms(hudContainer.style.transition) } }; }, options: { player_indicator_type: { _tagName: 'select', label: 'Indicator type', 'label:zh': '指标类型', 'label:ja': 'インジケータータイプ', 'label:ko': '표시기 유형', 'label:id': 'Gösterge tipi', 'label:es': 'Tipo de indicador', 'label:pt': 'Tipo de indicador', 'label:fr': "Type d'indicateur", 'label:it': 'Tipo di indicatore', 'label:de': 'Indikatortyp', 'label:pl': 'Typ wskaźnika', 'label:ua': 'Тип індикатора', options: [ { label: 'text-top', value: 'text-top', selected: true, 'label:ua': 'текст зверху', }, { label: 'bar-top', value: 'bar-top', 'label:ua': 'панель зверху', }, { label: 'bar-center', value: 'bar-center', 'label:ua': 'панель в центрі', }, { label: 'bar-vertical', value: 'bar-vertical', 'label:ua': 'вертикальна панель', }, ], }, player_indicator_color: { _tagName: 'input', type: 'color', value: '#ff0000', label: 'Color', 'label:zh': '颜色', 'label:ja': '色', 'label:ko': '색깔', 'label:id': 'Warna', 'label:pt': 'Cor', 'label:fr': 'Couleur', 'label:it': 'Colore', 'label:de': 'Farbe', 'label:pl': 'Kolor', 'label:ua': 'Колір', 'data-dependent': { 'player_indicator_type': '!text-top' }, }, player_indicator_chapter: { _tagName: 'input', label: 'Show info at start chapter', 'label:zh': '在开始章节显示信息', 'label:ja': '章の開始時に情報を表示', 'label:ko': '시작 장에 정보 표시', 'label:id': 'Tampilkan info di awal bab', 'label:es': 'Mostrar información al inicio del capítulo', 'label:pt': 'Mostrar informações no capítulo inicial', 'label:fr': 'Afficher les informations au début du chapitre', 'label:it': "Mostra informazioni all'inizio del capitolo", 'label:de': 'Info beim Startkapitel anzeigen', 'label:pl': 'Pokaż informacje na początku rozdziału', 'label:ua': 'Показати інформацію на початку розділу', type: 'checkbox', }, player_indicator_chapter_time: { _tagName: 'input', label: 'Chapter timeout', type: 'number', title: 'in sec', placeholder: '0-10', step: .1, min: .1, max: 10, value: 1.8, 'data-dependent': { 'player_indicator_chapter': true }, }, player_indicator_opacity: { _tagName: 'input', label: 'Opacity', 'label:zh': '不透明度', 'label:ja': '不透明度', 'label:ko': '불투명', 'label:id': 'Kegelapan', 'label:es': 'Opacidad', 'label:pt': 'Opacidade', 'label:fr': 'Opacité', 'label:it': 'Opacità', 'label:tr': 'opaklık', 'label:de': 'Opazität', 'label:pl': 'Przezroczystość', 'label:ua': 'Прозорість', type: 'number', title: 'less value - more transparency', placeholder: '0-1', step: .1, min: .1, max: .9, value: .3, }, } }); window.nova_plugins.push({ id: 'theater-mode', title: 'Theater mode', 'title:pl': 'Tryb kinowy', 'title:ua': 'Режим кінотеарту', run_on_pages: 'watch, -mobile', section: 'player', _runtime: user_settings => { if (user_settings.player_full_viewport_mode == 'redirect_watch_to_embed') { return location.assign(`https://www.youtube.com/embed/` + NOVA.queryURL.get('v')); } NOVA.waitSelector('ytd-watch-flexy') .then(el => { if (location.search.includes('list=')) { if (user_settings.theater_mode_ignore_playlist) { el.theaterModeChanged_(false); return; } } el.theaterModeChanged_(true); }); if (user_settings.player_full_viewport_mode == '') return; if (user_settings['player-fullscreen-mode'] && !user_settings.player_fullscreen_mode_embed && user_settings.player_full_viewport_mode != 'cinema_mode' ) { return; } NOVA.waitSelector('#movie_player') .then(movie_player => { const PLAYER_CONTAINER_SELECTOR = 'ytd-watch-flexy[theater]:not([fullscreen]) #ytd-player', PINNED_SELECTOR = '.nova-player-pin', PLAYER_SCROLL_LOCK_CLASS_NAME = 'nova-lock-scroll', PLAYER_SELECTOR = `${PLAYER_CONTAINER_SELECTOR} #movie_player:not(${PINNED_SELECTOR}):not(.${PLAYER_SCROLL_LOCK_CLASS_NAME})`, zIindex = Math.max(getComputedStyle(movie_player)['z-index'], 2020); addScrollDownBehavior(); switch (user_settings.player_full_viewport_mode) { case 'offset': NOVA.css.push( PLAYER_CONTAINER_SELECTOR + ` { min-height: calc(100vh - ${user_settings['header-compact'] ? 36 : NOVA.css.getValue('#masthead-container', 'height') || 56 }px) !important; }`); break; case 'force': setPlayerFullViewport(user_settings.player_full_viewport_mode_exit); break; case 'smart': if (user_settings.player_full_viewport_mode_exclude_shorts && NOVA.currentPage == 'shorts') { return; } NOVA.waitSelector('video') .then(video => { video.addEventListener('loadeddata', function () { if (user_settings.player_full_viewport_mode_exclude_shorts && this.videoWidth < this.videoHeight) { return; } const miniSize = NOVA.aspectRatio.sizeToFit({ 'srcWidth': this.videoWidth, 'srcHeight': this.videoHeight, }); if (miniSize.width < window.innerWidth) { setPlayerFullViewport('player_full_viewport_mode_exit'); } }); }); break; case 'cinema_mode': NOVA.css.push( PLAYER_CONTAINER_SELECTOR + ` { z-index: ${zIindex}; } ${PLAYER_SELECTOR}:before { content: ''; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, ${+user_settings.cinema_mode_opacity}); opacity: 0; transition: opacity .4s ease-in-out; pointer-events: none; } ${PLAYER_SELECTOR}.playing-mode:before { opacity: 1; } .ytp-ad-player-overlay, #playlist:hover, #masthead-container:hover, iframe, #guide, [class*="popup"], [role="navigation"], [role="dialog"] { z-index: ${zIindex + 1}; } #playlist:hover { position: relative; }`); addHideScrollbarCSS(); break; } function setPlayerFullViewport(exclude_pause) { const CLASS_OVER_PAUSED = 'nova-player-fullviewport'; NOVA.css.push( `${PLAYER_SELECTOR}.playing-mode, ${exclude_pause ? '' : `${PLAYER_SELECTOR}.paused-mode,`} ${PLAYER_SELECTOR}.${CLASS_OVER_PAUSED} { width: 100vw; height: 100vh; position: fixed; bottom: 0 !important; z-index: ${zIindex}; background-color: black; }`); NOVA.css.push( `#masthead-container:has( ~ #page-manager ytd-watch-flexy[theater]) { position: fixed; z-index: ${zIindex + 1}; opacity: 0; } #masthead-container:has( ~ #page-manager ytd-watch-flexy[theater]):hover, #masthead-container:has( ~ #page-manager ytd-watch-flexy[theater]):focus { opacity: 1; }`); addHideScrollbarCSS(); if (user_settings.player_full_viewport_mode_exit) { NOVA.waitSelector('video') .then(video => { video.addEventListener('pause', () => { if (!document.body.querySelector('.ytp-progress-bar')?.contains(document.activeElement)) { window.dispatchEvent(new Event('resize')); } }); }); NOVA.waitSelector('.ytp-progress-bar') .then(progress_bar => { ['mousedown', 'mouseup'].forEach(evt => { progress_bar.addEventListener(evt, () => { movie_player.classList.add(CLASS_OVER_PAUSED); }); }); }); } } function addScrollDownBehavior() { if (activateScrollElement = document.body.querySelector('.ytp-chrome-controls')) { activateScrollElement.addEventListener('wheel', evt => { switch (Math.sign(evt.wheelDelta)) { case -1: movie_player.classList.add(PLAYER_SCROLL_LOCK_CLASS_NAME); break; } }); document.addEventListener('scroll', evt => { if (window.scrollY === 0 && movie_player.classList.contains(PLAYER_SCROLL_LOCK_CLASS_NAME) ) { movie_player.classList.remove(PLAYER_SCROLL_LOCK_CLASS_NAME); } }); } } function addHideScrollbarCSS() { if (user_settings['scrollbar-hide']) return; NOVA.css.push( `html body:has(${PLAYER_SELECTOR})::-webkit-scrollbar { display: none; }`); } }); }, options: { player_full_viewport_mode: { _tagName: 'select', label: 'Mode', 'label:zh': '模式', 'label:ja': 'モード', 'label:ko': '방법', 'label:es': 'Modo', 'label:pt': 'Modo', 'label:it': 'Modalità', 'label:de': 'Modus', 'label:pl': 'Tryb', 'label:ua': 'Режим', options: [ { label: 'default', selected: true, 'label:ua': 'за замовчуванням', }, { label: 'cinema', value: 'cinema_mode', 'label:ua': 'кінотеатр', }, { label: 'full-viewport (auto)', value: 'smart', 'label:ua': 'повноекранний (авто)', }, { label: 'full-viewport', value: 'force', 'label:ua': 'повноекранний', }, { label: 'offset', value: 'offset', }, { label: 'redirect to embedded', value: 'redirect_watch_to_embed', 'label:ua': 'передавати на вбудований', }, ], }, player_full_viewport_mode_exit: { _tagName: 'input', label: 'Exit Fullscreen on video end/pause', 'label:zh': '视频结束/暂停时退出', 'label:ja': 'ビデオが終了/一時停止したら終了します', 'label:ko': '동영상이 종료/일시 중지되면 종료', 'label:id': 'Keluar dari viewport penuh jika video berakhir/jeda', 'label:es': 'Salir si el video termina/pausa', 'label:pt': 'Sair se o vídeo terminar/pausar', 'label:fr': 'Quitter si la vidéo se termine/pause', 'label:it': 'Uscita dalla visualizzazione completa se il video termina/mette in pausa', 'label:de': 'Beenden, wenn das Video endet/pausiert', 'label:pl': 'Wyjdź, gdy film się kończy/pauzuje', 'label:ua': 'Вихід із повного вікна перегляду, якщо відео закінчується/призупиняється', type: 'checkbox', 'data-dependent': { 'player_full_viewport_mode': ['force', 'smart'] }, }, player_full_viewport_mode_exclude_shorts: { _tagName: 'input', label: 'Full-viewport exclude shorts', 'label:zh': '全视口不包括短裤', 'label:ja': 'フルビューポートはショートパンツを除外します', 'label:ko': '전체 뷰포트 제외 반바지', 'label:id': 'Area pandang penuh tidak termasuk celana pendek', 'label:es': 'Vista completa excluir pantalones cortos', 'label:pt': 'Shorts de exclusão da janela de visualização completa', 'label:fr': 'La fenêtre complète exclut les shorts', 'label:it': 'La visualizzazione completa esclude i cortometraggi', 'label:de': 'Vollbildansicht schließt Shorts aus', 'label:pl': 'Pełny ekran wyklucza krótkie filmy', 'label:ua': 'Повне вікно перегляду без прев`ю', type: 'checkbox', 'data-dependent': { 'player_full_viewport_mode': 'smart' }, }, cinema_mode_opacity: { _tagName: 'input', label: 'Opacity', 'label:zh': '不透明度', 'label:ja': '不透明度', 'label:ko': '불투명', 'label:id': 'Kegelapan', 'label:es': 'Opacidad', 'label:pt': 'Opacidade', 'label:fr': 'Opacité', 'label:it': 'Opacità', 'label:de': 'Opazität', 'label:pl': 'Przezroczystość', 'label:ua': 'Прозорість', type: 'number', title: '0-1', placeholder: '0-1', step: .05, min: 0, max: 1, value: .75, 'data-dependent': { 'player_full_viewport_mode': 'cinema_mode' }, }, theater_mode_ignore_playlist: { _tagName: 'select', label: 'Ignore playlist', type: 'checkbox', }, } }); window.nova_plugins.push({ id: 'page-title-time', title: 'Show time in tab title', 'title:zh': '在标签标题中显示时间', 'title:ja': 'タブタイトルに時間を表示する', 'title:ko': '탭 제목에 시간 표시', 'title:id': 'Tampilkan waktu di judul tab', 'title:es': 'Mostrar la hora en el título de la pestaña', 'title:pt': 'Mostrar tempo no título da guia', 'title:fr': "Afficher l'heure dans le titre de l'onglet", 'title:it': "Mostra l'ora nel titolo della scheda", 'title:de': 'Zeit im Tab-Titel anzeigen', 'title:pl': 'Pokaż czas w tytule karty', 'title:ua': 'Відображення часу в заголовку вкладки', run_on_pages: 'watch', section: 'player', _runtime: user_settings => { NOVA.waitSelector('video') .then(video => { document.addEventListener('yt-navigate-start', () => pageTitle.backup = null); video.addEventListener('playing', pageTitle.save.bind(pageTitle)); video.addEventListener('timeupdate', () => pageTitle.update(video)); video.addEventListener('pause', () => pageTitle.restore(video)); video.addEventListener('ended', () => pageTitle.restore(video)); }); const pageTitle = { strSplit: ' | ', saveCheck() { return (result = (this.backup || document.title).includes(this.strSplit)) ? new RegExp(`^((\\d?\\d:){1,2}\\d{2})(${this.strSplit.replace('|', '\\|')})`, '') .test(document.title) : result; }, save() { if (this.backup || movie_player.getVideoData().isLive || movie_player.classList.contains('ad-showing') || this.saveCheck() ) { return; } this.backup = movie_player.getVideoData().title; }, update(video = NOVA.videoElement) { if (!this.backup) return; let newTitleArr = []; switch (movie_player.getVideoData().isLive ? 'current' : user_settings.page_title_time_mode) { case 'current': newTitleArr = [video.currentTime]; break; case 'current-duration': if (!isNaN(video.duration)) { newTitleArr = [video.currentTime, ' / ', video.duration]; } break; default: if (!isNaN(video.duration)) { newTitleArr = [video.duration - video.currentTime]; } } newTitleArr = newTitleArr .map(t => (typeof t === 'string') ? t : NOVA.timeFormatTo.HMS.digit(t / video.playbackRate)) .join(''); this.set([newTitleArr, this.backup]); }, restore(video = NOVA.videoElement) { if (!this.backup) return; this.set([movie_player.getVideoData().isLive && video.currentTime, this.backup]); }, set(arr) { document.title = arr .filter(Boolean) .join(this.strSplit); }, }; }, options: { page_title_time_mode: { _tagName: 'select', label: 'Mode', 'label:zh': '模式', 'label:ja': 'モード', 'label:ko': '방법', 'label:es': 'Modo', 'label:pt': 'Modo', 'label:it': 'Modalità', 'label:de': 'Modus', 'label:pl': 'Tryb', 'label:ua': 'Режим', options: [ { label: 'left', value: 'left', selected: true, 'label:zh': '剩下', 'label:ja': '左', 'label:ko': '왼쪽', 'label:id': 'tetap', 'label:es': 'izquierda', 'label:pt': 'deixou', 'label:fr': 'à gauche', 'label:it': 'è rimasta', 'label:de': 'links', 'label:pl': 'pozostało', 'label:ua': 'лишилось', }, { label: 'current/duration', value: 'current-duration', 'label:zh': '现在/期间', 'label:ja': '現在/期間', 'label:ko': '현재/기간', 'label:id': 'saat ini/durasi', 'label:es': 'actual/duración', 'label:pt': 'atual/duração', 'label:fr': 'courant/durée', 'label:it': 'corrente/durata', 'label:de': 'strom/dauer', 'label:pl': 'bieżący czas', 'label:ua': 'поточний/тривалість', }, ], }, } }); window.nova_plugins.push({ id: 'player-progress-bar-color', title: 'Player progress bar color', 'title:ua': 'Колір індикатора прогресу програвача', run_on_pages: 'watch, embed, -mobile', section: 'player', _runtime: user_settings => { NOVA.css.push( `.ytp-swatch-background-color { background-color: ${user_settings.player_progress_bar_color || '#f00'} !important; }`); }, options: { player_progress_bar_color: { _tagName: 'input', type: 'color', value: '#0089ff', label: 'Color', 'label:zh': '颜色', 'label:ja': '色', 'label:ko': '색깔', 'label:id': 'Warna', 'label:pt': 'Cor', 'label:fr': 'Couleur', 'label:it': 'Colore', 'label:de': 'Farbe', 'label:pl': 'Kolor', 'label:ua': 'Колір', }, } }); window.nova_plugins.push({ id: 'embed-popup', title: 'Redirect embedded to popup', 'title:zh': '将嵌入式视频重定向到弹出窗口', 'title:ja': '埋め込まれたビデオをポップアップにリダイレクトします', 'title:ko': '포함된 비디오를 팝업으로 리디렉션', 'title:id': '포함된 비디오를 팝업으로 리디렉션', 'title:es': 'Redirigir video incrustado a ventana emergente', 'title:pt': 'Redirecionar vídeo incorporado para pop-up', 'title:fr': 'Rediriger la vidéo intégrée vers une fenêtre contextuelle', 'title:it': 'Reindirizza il video incorporato al popup', 'title:de': 'Leiten Sie eingebettete Videos zum Popup um', 'title:pl': 'Przekieruj osadzone wideo do wyskakującego okienka', 'title:ua': 'Переспрямувати вбудоване відео у спливаюче вікно', run_on_pages: 'embed, -mobile', section: 'player', desc: 'if iframe width is less than 720p', 'data-conflict': 'player-fullscreen-mode', _runtime: user_settings => { if (window.top === window.self || location.hostname.includes('googleapis.com') || NOVA.queryURL.has('popup') ) { return; } if (user_settings.player_full_viewport_mode == 'redirect_watch_to_embed') return; if (user_settings['player-fullscreen-mode']) return; if (window.innerWidth > 720) return; NOVA.waitSelector('#movie_player video') .then(video => { video.addEventListener('loadeddata', createPopup.bind(video)); }); function createPopup() { movie_player.stopVideo(); const { width, height } = NOVA.aspectRatio.sizeToFit({ 'srcWidth': this.videoWidth, 'srcHeight': this.videoHeight, 'maxWidth': screen.width / (+user_settings.player_buttons_custom_popup_width || 4), }); const url = new URL( document.querySelector('link[itemprop="embedUrl"][href]')?.href || (location.origin + '/embed/' + movie_player.getVideoData().video_id) ); url.searchParams.set('autoplay', 1); url.searchParams.set('popup', true); openPopup({ 'url': url.href, 'title': document.title, 'width': width, 'height': height }); function openPopup({ url, title, width, height }) { const left = (screen.width / 2) - (width / 2); const top = (screen.height / 2) - (height / 2); const newWindow = window.open(url, '_blank', `popup=1,toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=${width},height=${height},top=${top},left=${left}`); newWindow.document.title = title; } } }, }); window.nova_plugins.push({ id: 'sponsor-block', title: 'SponsorBlock', run_on_pages: 'watch, embed', section: 'player', _runtime: user_settings => { NOVA.waitSelector('#movie_player video') .then(video => { let segmentsList = []; let muteState; let videoId; video.addEventListener('loadeddata', init.bind(video)); async function init() { videoId = NOVA.queryURL.get('v') || movie_player.getVideoData().video_id; segmentsList = await getSkipSegments(videoId) || []; if (user_settings['player-float-progress-bar'] && segmentsList.length) { const SELECTOR = 'nova-player-float-progress-bar-chapters'; const deflectionSec = 5; let chaptersEls; await NOVA.waitUntil(() => (chaptersEls = document.body.querySelectorAll(`#${SELECTOR} > span[time]`)) && chaptersEls.length , 1000); chaptersEls.forEach((chapterEl, idx) => { if (idx === chaptersEls.length - 1) return; const chapterStart = ~~NOVA.timeFormatTo.hmsToSec(chapterEl.getAttribute('time')), chapterNextStart = ~~NOVA.timeFormatTo.hmsToSec(chaptersEls[idx + 1].getAttribute('time')); for (const [i, value] of segmentsList.entries()) { const [segmentStart, segmentEnd, category] = value; if (((~~segmentStart + deflectionSec) <= chapterNextStart) && ((~~segmentEnd - deflectionSec) >= chapterStart) ) { chapterEl.style.title = category; let color; switch (category) { case 'sponsor': color = '255, 231, 0'; break; case 'interaction': color = '255, 127, 80'; break; case 'selfpromo': color = '255, 99, 71'; break; case 'intro': color = '255, 165, 0'; break; case 'outro': color = '255, 165, 0'; break; } chapterEl.style.background = `rgb(${color},.4`; } } }); } } video.addEventListener('timeupdate', function () { let segmentStart, segmentEnd, category; for (let i = 0; i < segmentsList.length; i++) { [segmentStart, segmentEnd, category] = segmentsList[i]; segmentStart = ~~segmentStart; segmentEnd = Math.ceil(segmentEnd); const inSegment = (this.currentTime > segmentStart && this.currentTime < segmentEnd); switch (user_settings.sponsor_block_action) { case 'mute': if (inSegment && !muteState && !this.muted) { muteState = true; movie_player.mute(true); return novaNotification('muted'); } else if (!inSegment && muteState && this.muted) { muteState = false; movie_player.unMute(); segmentsList.splice(i, 1); return novaNotification('unMuted'); } break; case 'skip': if (inSegment) { this.currentTime = segmentEnd; segmentsList.splice(i, 1); return novaNotification('skipped'); } break; } } function novaNotification(prefix = '') { const msg = `${prefix} [${category}] • ${NOVA.timeFormatTo.HMS.digit(segmentStart)} - ${NOVA.timeFormatTo.HMS.digit(segmentEnd)}`; console.info(videoId, msg); NOVA.triggerHUD(msg); } }); }); async function getSkipSegments(videoId = required()) { const CACHE_PREFIX = 'nova-videos-sponsor-block:'; if ( navigator.cookieEnabled && (storage = sessionStorage.getItem(CACHE_PREFIX + videoId)) ) { return JSON.parse(storage); } else { const actionTypes = (Array.isArray(user_settings.sponsor_block_action) ? user_settings.sponsor_block_action : [user_settings.sponsor_block_action]) || ['skip', 'mute'], categories = user_settings.sponsor_block_category || [ 'sponsor', 'interaction', 'selfpromo', 'intro', 'outro', ], params = { 'videoID': videoId, 'actionTypes': JSON.stringify(actionTypes), 'categories': JSON.stringify(categories), }, query = Object.keys(params) .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) .join('&'); const fetchAPI = () => fetch((user_settings.sponsor_block_url || 'https://sponsor.ajay.app') + `/api/skipSegments?${query}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } } ) .then(response => response.json()) .then(json => json .map(a => [...a.segment, a.category]) ) .catch(error => { }); if (result = await fetchAPI()) { if (navigator.cookieEnabled) { sessionStorage.setItem(CACHE_PREFIX + videoId, JSON.stringify(result)); } return result; } } } }, options: { sponsor_block_category: { _tagName: 'select', label: 'Category', title: '[Ctrl+Click] to select several', 'title:zh': '[Ctrl+Click] 选择多个', 'title:ja': '「Ctrl+Click」して、いくつかを選択します', 'title:ko': '[Ctrl+Click] 여러 선택', 'title:id': '[Ctrl+Klik] untuk memilih beberapa', 'title:es': '[Ctrl+Click] para seleccionar varias', 'title:pt': '[Ctrl+Click] para selecionar vários', 'title:fr': '[Ctrl+Click] pour sélectionner plusieurs', 'title:it': '[Ctrl+Clic] per selezionarne diversi', 'title:de': '[Ctrl+Click] um mehrere auszuwählen', 'title:pl': 'Ctrl+kliknięcie, aby zaznaczyć kilka', 'title:ua': '[Ctrl+Click] щоб обрати декілька', multiple: null, required: true, size: 7, options: [ { label: 'Sponsor', value: 'sponsor', }, { label: 'Unpaid/Self Promotion', value: 'selfpromo', }, { label: 'Reminder Subscribe', value: 'interaction', }, { label: 'Intro', value: 'intro', }, { label: 'Endcards/Credits (Outro)', value: 'outro', }, { label: 'Preview/Recap', value: 'preview', }, { label: 'Music: Non-Music Section', value: 'music_offtopic', }, { label: 'Full Video Label Only', value: 'exclusive_access', }, ], }, sponsor_block_action: { _tagName: 'select', label: 'Mode', 'label:zh': '模式', 'label:ja': 'モード', 'label:ko': '방법', 'label:es': 'Modo', 'label:pt': 'Modo', 'label:it': 'Modalità', 'label:de': 'Modus', 'label:pl': 'Tryb', 'label:ua': 'Режим', options: [ { label: 'skip', value: 'skip', selected: true, }, { label: 'mute', value: 'mute', }, ], }, sponsor_block_url: { _tagName: 'input', label: 'URL', type: 'url', pattern: "https://.*", placeholder: 'https://youtube.com/...', value: 'https://sponsor.ajay.app', required: true, }, } }); window.nova_plugins.push({ id: 'player-live-duration', title: 'Show duration on live video', 'title:ua': 'Показувати тривалість трансляції', run_on_pages: 'watch, embed, -mobile', section: 'player', _runtime: user_settings => { NOVA.waitSelector('#movie_player video') .then(video => { video.addEventListener('canplay', function () { if (movie_player.getVideoData().isLive && (el = document.body.querySelector('#movie_player .ytp-chrome-controls .ytp-live .ytp-time-current')) ) { el.style = 'display: block !important; margin-right: 5px;'; } }); NOVA.css.push( `#movie_player .ytp-chrome-controls .ytp-time-display.ytp-live { display: flex !important; }`); }); }, }); window.nova_plugins.push({ id: 'player-disable-fullscreen-scroll', title: 'Disable scrolling for fullscreen player', 'title:zh': '禁用全屏滚动', 'title:ja': 'フルスクリーンスクロールを無効にする', 'title:ko': '전체 화면 스크롤 비활성화', 'title:id': 'Nonaktifkan pengguliran pemain dalam mode layar penuh', 'title:es': 'Desactivar el desplazamiento a pantalla completa', 'title:pt': 'Desabilitar rolagem em tela cheia', 'title:fr': 'Désactiver le défilement plein écran', 'title:it': 'Disabilita lo scorrimento del lettore in modalità a schermo intero', 'title:de': 'Deaktivieren Sie das Scrollen im Vollbildmodus', 'title:pl': 'Wyłącz przewijanie w trybie pełnoekranowym', 'title:ua': 'Вимкнути прокрутку у повноекранному режимі', run_on_pages: 'watch', section: 'player', _runtime: user_settings => { NOVA.css.push(`.ytp-fullerscreen-edu-button { display: none !important; }`); document.addEventListener('fullscreenchange', () => { document.fullscreenElement ? document.addEventListener('wheel', lockscroll, { passive: false }) : document.removeEventListener('wheel', lockscroll) } ); function lockscroll(evt) { evt.preventDefault(); } }, }); window.nova_plugins.push({ id: 'player-control-autohide', title: 'Hide player control panel if not hovered', 'title:zh': '播放器上的自动隐藏控件', 'title:ja': 'プレーヤーのコントロールを自動非表示', 'title:ko': '플레이어의 자동 숨기기 컨트롤', 'title:id': 'Sembunyikan kontrol pada pemutar', 'title:es': 'Ocultar automáticamente los controles en el reproductor', 'title:pt': 'Auto-ocultar controles no player', 'title:fr': 'Masque le panneau de contrôle du lecteur', 'title:it': 'Nascondi i controlli sul giocatore', 'title:de': 'Blendet das Player-Bedienfeld aus', 'title:pl': 'Ukrywaj elementy w odtwarzaczu', 'title:ua': 'Приховати панель керування у відтворювачі', run_on_pages: 'watch, -mobile', section: 'player', desc: 'Hover controls to display it', 'desc:zh': '将鼠标悬停在它上面以显示它', 'desc:ja': 'カーソルを合わせると表示されます', 'desc:ko': '그것을 표시하려면 그 위로 마우스를 가져갑니다', 'desc:id': 'Arahkan kontrol untuk menampilkannya', 'desc:es': 'Coloca el cursor sobre él para mostrarlo', 'desc:pt': 'Passe o mouse sobre ele para exibi-lo', 'desc:fr': "Survolez-le pour l'afficher", 'desc:it': 'Passa il mouse sui controlli per visualizzarlo', 'desc:de': 'Bewegen Sie den Mauszeiger darüber, um es anzuzeigen', 'desc:pl': 'Najedź, aby wyświetlić', 'desc:ua': 'Наведіть мишкою щоб показати', 'data-conflict': 'player-control-below', _runtime: user_settings => { if (user_settings['player-control-below']) return; let selectorHover, selectorGradientHide; switch (user_settings.player_control_autohide_container) { case 'player': selectorHover = 'ytd-watch-flexy:not([fullscreen]) #movie_player:hover .ytp-chrome-bottom'; selectorGradientHide = '#movie_player:not(:hover) .ytp-gradient-bottom'; NOVA.waitSelector('#movie_player') .then(movie_player => { triggerOnHoverElement({ 'element': movie_player, 'callback': function (hovered) { if (hovered) this.mouseMoveIntervalId = fixControlFreeze(); else clearInterval(this.mouseMoveIntervalId); }, }); }); break; default: selectorHover = '.ytp-chrome-bottom:hover'; selectorGradientHide = '#movie_player:has(.ytp-chrome-bottom:not(:hover)) .ytp-gradient-bottom'; break; } NOVA.css.push( `.ytp-chrome-bottom { opacity: 0; } ${selectorHover} { opacity: 1; } ytd-watch-flexy:not([fullscreen]) #movie_player.ytp-autohide:hover #nova-player-float-progress-bar { visibility: hidden !important; }`); NOVA.css.push( `${selectorGradientHide} { opacity: 0; }`); function triggerOnHoverElement({ element = required(), callback = required() }) { if (!(element instanceof HTMLElement)) return console.error('triggerOnHoverElement:', typeof element); if (typeof callback !== 'function') return console.error('triggerOnHoverElement:', typeof callback); const isHover = e => e.parentElement.querySelector(':hover') === e; document.addEventListener('mousemove', function checkHover() { const hovered = isHover(element); if (hovered !== checkHover.hovered) { checkHover.hovered = hovered; return callback(hovered); } }); } function fixControlFreeze(ms = 2000) { return window.setInterval(() => { if (NOVA.currentPage === 'watch' && document.visibilityState == 'visible' && movie_player.classList.contains('playing-mode') && !NOVA.isFullscreen() ) { movie_player.wakeUpControls(); } }, ms); } }, options: { player_control_autohide_container: { _tagName: 'select', label: 'Hover container', 'label:ua': 'Відображати вміст при наведенні', options: [ { label: 'player', value: 'player', selected: true, 'label:ua': 'програвач', }, { label: 'control', value: 'control', 'label:ua': 'панель керування', }, ], }, } }); window.nova_plugins.push({ id: 'scrollbar-hide', title: 'Hide scrollbar (for watch page)', 'title:ua': 'Приховати смугу прокрутки на сторінці перегляду', run_on_pages: '*, -embed, -mobile', section: 'player', _runtime: user_settings => { const HIDE_SCROLL_ATTR = 'nova-scrollbar-hide'; NOVA.css.push( `html[${HIDE_SCROLL_ATTR}] body::-webkit-scrollbar { display: none; }`); NOVA.runOnPageInitOrTransition(() => { let hasAttr = document.documentElement.hasAttribute(HIDE_SCROLL_ATTR); if ((NOVA.currentPage == 'watch') && !hasAttr) { document.documentElement.toggleAttribute(HIDE_SCROLL_ATTR); } else if ((NOVA.currentPage != 'watch') && hasAttr) { document.documentElement.removeAttribute(HIDE_SCROLL_ATTR); } }); if (user_settings.scrollbar_hide_livechat && NOVA.currentPage.includes('live_chat')) { return NOVA.css.push( `*, #item-scroller { -ms-overflow-style: none; scrollbar-width: none; } *::-webkit-scrollbar, #item-scroller::-webkit-scrollbar { display: none; }`); } }, options: { scrollbar_hide_livechat: { _tagName: 'input', label: 'In live-chat frame', 'label:ua': 'У живому чаті', type: 'checkbox', 'data-dependent': { 'livechat_visibility_mode': ['!disable'] }, }, } }); window.nova_plugins.push({ id: 'player-loop', title: 'Add repeat (loop) playback button', 'title:zh': '添加循环播放按钮', 'title:ja': 'ループ再生ボタンを追加する', 'title:ko': '루프 재생 버튼 추가', 'title:id': 'Tambahkan tombol pemutaran ulangi (loop)', 'title:es': 'Agregar un botón de reproducción en bucle', 'title:pt': 'Adicionar um botão de reprodução em loop', 'title:fr': 'Ajouter un bouton de lecture en boucle', 'title:it': 'Aggiungi il pulsante di riproduzione ripetuta (loop).', 'title:de': 'Füge einen Loop-Play-Button hinzu', 'title:pl': 'Dodaj przycisk odtwarzania pętli', 'title:ua': 'Додати кнопку повтор', run_on_pages: 'watch', section: 'player', _runtime: user_settings => { NOVA.waitSelector('#movie_player .ytp-left-controls .ytp-play-button') .then(container => { const SELECTOR_CLASS = 'nova-right-custom-button', btn = document.createElement('button'); btn.className = `ytp-button ${SELECTOR_CLASS}`; btn.style.opacity = .5; btn.style.minWidth = getComputedStyle(container).width || '48px'; btn.title = 'Repeat'; btn.innerHTML = `<svg viewBox="-6 -6 36 36" height="100%" width="100%"> <g fill="currentColor"> <path d="M 7 7 L 17 7 L 17 10 L 21 6 L 17 2 L 17 5 L 5 5 L 5 11 L 7 11 L 7 7 Z M 7.06 17 L 7 14 L 3 18 L 7 22 L 7 19 L 19 19 L 19 13 L 17 13 L 17 17 L 7.06 17 Z"/> </g> </svg>`; btn.addEventListener('click', () => { if (!NOVA.videoElement) return console.error('btn > videoElement empty:', NOVA.videoElement); NOVA.videoElement.loop = !NOVA.videoElement.loop; if (movie_player.classList.contains('ad-showing')) NOVA.videoElement.removeAttribute('loop'); btn.style.opacity = NOVA.videoElement.hasAttribute('loop') ? 1 : .5; }); container.after(btn); NOVA.waitSelector('#movie_player video') .then(video => { video.addEventListener('loadeddata', ({ target }) => { if (movie_player.classList.contains('ad-showing')) return; if (btn.style.opacity == 1 && !target.loop) target.loop = true; if (target.loop) btn.style.opacity = 1; }); }); }); }, options: { } }); window.nova_plugins.push({ id: 'video-unblock-warn-content', title: 'Skip warn about inappropriate/offensive content', 'title:ua': 'Пропустити попередження про неприйнятний або образливий вміст', run_on_pages: 'watch, embed, -mobile', section: 'player', desc: "skip 'The following content may contain suicide or self-harm topics.'", 'desc:ua': 'пропустити "Наступний контент може містити теми суїциду або самоушкодження".', _runtime: user_settings => { NOVA.waitSelector('ytd-watch-flexy[player-unavailable] yt-player-error-message-renderer #button.yt-player-error-message-renderer button', { destroy_if_url_changes: true }) .then(btn => btn.click()); }, }); window.nova_plugins.push({ id: 'save-channel-state', title: 'Add button "Save params for the channel"', 'title:zh': '특정 채널에 저장', 'title:ja': '特定のチャンネル用に保存', 'title:ko': '특정 채널에 저장', 'title:id': 'Simpan untuk saluran tertentu', 'title:es': 'Guardar para un canal específico', 'title:pt': 'Salvar para canal específico', 'title:fr': 'Enregistrer pour un canal spécifique', 'title:it': 'Salva per canale specifico', 'title:de': 'Speichern Sie für einen bestimmten Kanal', 'title:pl': 'Zapisz dla określonego kanału', 'title:ua': 'Зберегти для конкретного каналу', run_on_pages: 'watch, embed', section: 'player', _runtime: user_settings => { const SELECTOR_BUTTON_ID = 'nova-channels-state', SELECTOR_BUTTON = '#' + SELECTOR_BUTTON_ID, SELECTOR_BUTTON_CLASS_NAME = 'nova-right-custom-button', SELECTOR_BUTTON_LIST_ID = SELECTOR_BUTTON_CLASS_NAME + '-list', SELECTOR_BUTTON_LIST = '#' + SELECTOR_BUTTON_LIST_ID, SELECTOR_BUTTON_TITLE_ID = SELECTOR_BUTTON_CLASS_NAME + '-title'; NOVA.waitSelector('#movie_player .ytp-right-controls') .then(container => { initStyles(); NOVA.runOnPageInitOrTransition(async () => { if (NOVA.currentPage == 'watch' || NOVA.currentPage == 'embed') { await NOVA.storage_obj_manager.initStorage(); if (btn = document.getElementById(SELECTOR_BUTTON_ID)) { btn.append(genList()); } else { const btn = document.createElement('button'); btn.id = SELECTOR_BUTTON_ID; btn.className = `ytp-button ${SELECTOR_BUTTON_CLASS_NAME}`; btn.title = 'Save channel state'; const btnTitle = document.createElement('span'); btnTitle.id = SELECTOR_BUTTON_TITLE_ID; btnTitle.style.display = 'flex'; btnTitle.innerHTML = `<svg width="100%" height="100%" viewBox="-140 -140 500 500"> <g fill="currentColor"> <path d="M198.5,0h-17v83h-132V0h-49v231h230V32.668L198.5,0z M197.5,199h-165v-83h165V199z" /> <rect width="33" x="131.5" height="66" /> </g> </svg>`; btn.prepend(btnTitle); btn.append(genList()); container.prepend(btn); } btnTitleStateUpdate(Boolean(NOVA.storage_obj_manager.read())); } }); }); function btnTitleStateUpdate(state) { document.getElementById(SELECTOR_BUTTON_TITLE_ID) .style.setProperty('opacity', state ? 1 : .3); } function genList() { const ul = document.createElement('ul'); ul.id = SELECTOR_BUTTON_LIST_ID; let listItem = []; if (user_settings['video-quality']) { listItem.push({ name: 'quality', getSaveDataFn: movie_player.getPlaybackQuality }); } if (user_settings['rate-wheel']) { listItem.push({ name: 'speed', getSaveDataFn: movie_player.getPlaybackRate }); } if (user_settings['volume-wheel']) { listItem.push({ name: 'volume', getSaveDataFn: () => ~~(movie_player.getVolume()) }); } listItem.push({ name: 'subtitles', getSaveDataFn: () => { movie_player.toggleSubtitlesOn(); return true; }, customInit: () => { NOVA.waitUntil(() => { movie_player.toggleSubtitlesOn(); return document.body.querySelector('.ytp-caption-window-top[id^="caption-window"]'); }, 500); }, }); if (user_settings['player-resume-playback']) { listItem.push({ name: 'ignore-playback', label: 'unsave playback time', getSaveDataFn: () => true }); } if (user_settings['player-loop']) { listItem.push({ name: 'loop', getSaveDataFn: () => true }); } listItem.forEach(async element => { const storage = NOVA.storage_obj_manager._getParam(element.name); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `checkbox-${element.name}`; checkbox.checked = Boolean(storage); checkbox.className = 'ytp-menuitem-toggle-checkbox'; const li = document.createElement('li'); li.innerHTML = `<label for="checkbox-${element.name}"> ${element.label || element.name} <span>${storage || ''}</span> </label>`; li.title = storage ? `Currently stored value ${storage}` : 'none'; if (Boolean(storage) && element.hasOwnProperty('customInit') && typeof element.customInit === 'function') { element.customInit(); } checkbox.addEventListener('change', () => { let state; if (checkbox.checked && (state = element.getSaveDataFn())) { NOVA.storage_obj_manager.save({ [element.name]: state }); } else { NOVA.storage_obj_manager.remove(element.name); } li.title = state ? `Currently stored value ${state}` : 'none'; li.querySelector('span').textContent = state || ''; btnTitleStateUpdate(Boolean(state)); }); li.prepend(checkbox); ul.append(li); }); if (user_settings['time-jump']) { const SLIDER_LABEL = 'skip into', SLIDER_STORAGE_NAME = 'skip-into', storage = +NOVA.storage_obj_manager._getParam(SLIDER_STORAGE_NAME); const slider = document.createElement('input'); slider.type = 'range'; slider.min = 0; slider.max = 120; slider.step = 1; slider.value = storage || 0; const li = document.createElement('li'); li.innerHTML = `<label for="checkbox-${SLIDER_STORAGE_NAME}"> ${SLIDER_LABEL} <span>${storage || ''}</span> </label>`; li.title = 'Simple alternative SponsorBlock'; slider.addEventListener('change', sliderChange); slider.addEventListener('input', sliderChange); slider.addEventListener('wheel', evt => { evt.preventDefault(); evt.target.value = +evt.target.value + Math.sign(evt.wheelDelta); sliderChange(evt); }); li.prepend(slider); ul.append(li); function sliderChange({ target }) { if (state = +target.value) { NOVA.storage_obj_manager.save({ [SLIDER_STORAGE_NAME]: +target.value }); } else { NOVA.storage_obj_manager.remove(SLIDER_STORAGE_NAME); } li.title = state ? `Currently stored value ${state}` : 'none'; li.querySelector('span').textContent = state || ''; btnTitleStateUpdate(Boolean(state)); } } return ul; } function initStyles() { NOVA.css.push( SELECTOR_BUTTON + ` { overflow: visible !important; position: relative; text-align: center !important; vertical-align: top; font-weight: bold; } .ytp-left-controls { overflow: visible !important; } ${SELECTOR_BUTTON_LIST} { position: absolute; bottom: 2.5em !important; left: -2.2em; list-style: none; padding-bottom: 1.5em !important; z-index: ${+NOVA.css.getValue('.ytp-progress-bar', 'z-index') + 1}; } html[data-cast-api-enabled] ${SELECTOR_BUTTON_LIST} { margin: 0; padding: 0; bottom: 3.3em; } ${SELECTOR_BUTTON}:not(:hover) ${SELECTOR_BUTTON_LIST} { display: none; } ${SELECTOR_BUTTON_LIST} li { cursor: pointer; white-space: nowrap; line-height: 1.4; background: rgba(28, 28, 28, 0.9); margin: .3em 0; padding: .5em 1em; border-radius: .3em; color: #fff; text-align: left !important; display: grid; grid-template-columns: auto auto; align-items: center; justify-content: start; } ${SELECTOR_BUTTON_LIST} li label { cursor: pointer; padding-left: 5px; } ${SELECTOR_BUTTON_LIST} li.active { background: #720000; } ${SELECTOR_BUTTON_LIST} li.disable { color: #666; } ${SELECTOR_BUTTON_LIST} li:not(:hover) { opacity: .8; } ${SELECTOR_BUTTON_LIST} li span:not(:empty):before { content: '('; } ${SELECTOR_BUTTON_LIST} li span:not(:empty):after { content: ')'; } ${SELECTOR_BUTTON_LIST} [type="checkbox"] { appearance: none; outline: none; cursor: pointer; } ${SELECTOR_BUTTON_LIST} [type="checkbox"]:checked { background: #f00; } ${SELECTOR_BUTTON_LIST} [type="checkbox"]:checked:after { left: 20px; background-color: #fff; }`); } }, }); window.nova_plugins.push({ id: 'time-remaining', title: 'Remaining time', 'title:zh': '剩余时间', 'title:ja': '余日', 'title:ko': '남은 시간', 'title:id': 'Waktu yang tersisa', 'title:es': 'Tiempo restante', 'title:pt': 'Tempo restante', 'title:fr': 'Temps restant', 'title:it': 'Tempo rimanente', 'title:de': 'Verbleibende Zeit', 'title:pl': 'Pozostały czas', 'title:ua': 'Час, що залишився', run_on_pages: 'watch, embed, -mobile', section: 'player', desc: 'Remaining time until the end of the video', 'desc:zh': '距离视频结束的剩余时间', 'desc:ja': 'ビデオの終わりまでの残り時間', 'desc:ko': '영상 끝까지 남은 시간', 'desc:id': 'Sisa waktu sampai akhir video', 'desc:es': 'Tiempo restante hasta el final del video', 'desc:pt': 'Tempo restante até o final do vídeo', 'desc:fr': "Temps restant jusqu'à la fin de la vidéo", 'desc:it': 'Tempo rimanente fino alla fine del video', 'desc:de': 'Verbleibende Zeit bis zum Ende des Videos', 'desc:pl': 'Czas pozostały do końca filmu', 'desc:ua': 'Час, що залишився до кінця відео', _runtime: user_settings => { const SELECTOR_ID = 'nova-player-time-remaining'; NOVA.waitSelector('.ytp-time-duration, ytm-time-display .time-display-content') .then(container => { NOVA.waitSelector('video') .then(video => { video.addEventListener('timeupdate', setRemaining.bind(video)); video.addEventListener('ratechange', setRemaining.bind(video)); video.addEventListener('ended', () => insertToHTML({ 'container': container })); document.addEventListener('yt-navigate-finish', () => insertToHTML({ 'container': container })); }); function setRemaining() { if (isNaN(this.duration) || movie_player.getVideoData().isLive || (NOVA.currentPage == 'embed' && window.self.location.href.includes('live_stream')) || document.visibilityState == 'hidden' || movie_player.classList.contains('ytp-autohide') ) return; const getProgressPt = () => { const floatRound = pt => (this.duration > 3600) ? pt.toFixed(2) : (this.duration > 1500) ? pt.toFixed(1) : Math.round(pt); return floatRound((this.currentTime / this.duration) * 100) + '%'; }, getLeftTime = () => '-' + NOVA.timeFormatTo.HMS.digit((this.duration - this.currentTime) / this.playbackRate); let text; switch (user_settings.time_remaining_mode) { case 'pt': text = ' • ' + getProgressPt(); break; case 'time': text = getLeftTime(); break; default: text = getLeftTime(); text += text && ` (${getProgressPt()})`; } if (text) { insertToHTML({ 'text': text, 'container': container }); } } function insertToHTML({ text = '', container = required() }) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); (document.getElementById(SELECTOR_ID) || (function () { container.insertAdjacentHTML('afterend', ` <span id="${SELECTOR_ID}">${text}</span>`); return document.getElementById(SELECTOR_ID); })()) .textContent = text; } }); }, options: { time_remaining_mode: { _tagName: 'select', label: 'Mode', 'label:zh': '模式', 'label:ja': 'モード', 'label:ko': '방법', 'label:es': 'Modo', 'label:pt': 'Modo', 'label:it': 'Modalità', 'label:de': 'Modus', 'label:pl': 'Tryb', 'label:ua': 'Режим', options: [ { label: 'time+(%)', value: 'full', 'label:ua': 'час+(%)', }, { label: 'time', value: 'time', selected: true, 'label:ua': 'час', }, { label: '%', value: 'pt', }, ], }, } }); window.nova_plugins.push({ id: 'player-float-progress-bar', title: 'Float player progress bar', 'title:zh': '浮动播放器进度条', 'title:ja': 'フロートプレーヤーのプログレスバー', 'title:ko': '플로팅 플레이어 진행률 표시줄', 'title:id': 'Bilah kemajuan pemain mengambang', 'title:es': 'Barra de progreso flotante del jugador', 'title:pt': 'Barra de progresso do jogador flutuante', 'title:fr': 'Barre de progression du joueur flottant', 'title:it': 'Barra di avanzamento del giocatore mobile', 'title:de': 'Float-Player-Fortschrittsbalken', 'title:pl': 'Pływający pasek postępu odtwarzacza', 'title:ua': 'Плаваючий індикатор прогресу відтворення', run_on_pages: 'watch, embed, -mobile', section: 'player', _runtime: user_settings => { if (NOVA.currentPage == 'embed' && window.self.location.href.includes('live_stream') ) return; if (NOVA.currentPage == 'embed' && ['0', 'false'].includes(NOVA.queryURL.get('controls'))) return; const SELECTOR_CONTAINER = '#movie_player.ytp-autohide', SELECTOR_ID = 'nova-player-float-progress-bar', SELECTOR = '#' + SELECTOR_ID, CHAPTERS_MARK_WIDTH_PX = '2px'; NOVA.waitSelector(`${SELECTOR_CONTAINER} video`) .then(video => { const container = insertFloatBar({ 'init_container': movie_player, 'z_index': Math.max(NOVA.css.getValue('.ytp-chrome-bottom', 'z-index'), 59) + 1 }), bufferEl = document.getElementById(`${SELECTOR_ID}-buffer`), progressEl = document.getElementById(`${SELECTOR_ID}-progress`); renderChapters.init(video); video.addEventListener('canplay', resetBar); video.addEventListener('loadeddata', resetBar); video.addEventListener('timeupdate', function () { if (notInteractiveToRender()) return; if (!isNaN(this.duration)) { progressEl.style.transform = `scaleX(${this.currentTime / this.duration})`; } }); video.addEventListener('progress', renderBuffer.bind(video)); video.addEventListener('seeking', renderBuffer.bind(video)); function renderBuffer() { if (notInteractiveToRender()) return; if ((totalDuration = movie_player.getDuration()) && !isNaN(totalDuration)) { bufferEl.style.transform = `scaleX(${movie_player.getVideoLoadedFraction()})`; } } function resetBar() { container.style.display = movie_player.getVideoData().isLive ? 'none' : 'initial'; container.classList.remove('transition'); bufferEl.style.transform = 'scaleX(0)'; progressEl.style.transform = 'scaleX(0)'; container.classList.add('transition'); renderChapters.init(video); } function notInteractiveToRender() { return (document.visibilityState == 'hidden' || movie_player.getVideoData().isLive ); } }); function insertFloatBar({ init_container = movie_player, z_index = 60 }) { if (!(init_container instanceof HTMLElement)) { return console.error('vid not HTMLElement:', init_container); } return document.getElementById(SELECTOR_ID) || (function () { init_container.insertAdjacentHTML('beforeend', `<div id="${SELECTOR_ID}" class=""> <div class="container"> <div id="${SELECTOR_ID}-buffer" class="ytp-load-progress"></div> <div id="${SELECTOR_ID}-progress" class="ytp-swatch-background-color"></div> </div> <div id="${SELECTOR_ID}-chapters"></div> </div>`); NOVA.css.push( `[id|=${SELECTOR_ID}] { position: absolute; bottom: 0; } ${SELECTOR} { --opacity: ${+user_settings.player_float_progress_bar_opacity || .7}; --height: ${+user_settings.player_float_progress_bar_height || 3}px; --bg-color: ${NOVA.css.getValue('.ytp-progress-list', 'background-color') || 'rgba(255,255,255,.2)'}; --zindex: ${z_index}; opacity: var(--opacity); z-index: var(--zindex); background-color: var(--bg-color); width: 100%; height: var(--height); visibility: hidden; } ${SELECTOR_CONTAINER} ${SELECTOR} { visibility: visible; } ${SELECTOR_CONTAINER} ${SELECTOR}.transition [id|=${SELECTOR_ID}] { transition: transform .2s linear; } ${SELECTOR}-progress, ${SELECTOR}-buffer { width: 100%; height: 100%; transform-origin: 0 0; transform: scaleX(0); } ${SELECTOR}-progress { z-index: calc(var(--zindex) + 1); } ${SELECTOR}-chapters { position: relative; width: 100%; display: flex; justify-content: flex-end; } ${SELECTOR}-chapters span { height: var(--height); z-index: calc(var(--zindex) + 1); box-sizing: border-box; padding: 0; margin: 0; } ${SELECTOR}-chapters span:not(:first-child) { border-left: ${CHAPTERS_MARK_WIDTH_PX} solid rgba(255,255,255,.7); }`); return document.getElementById(SELECTOR_ID); })(); } const renderChapters = { async init(vid) { if (NOVA.currentPage == 'watch' && !(vid instanceof HTMLElement)) { return console.error('vid not HTMLElement:', chaptersContainer); } await NOVA.waitUntil(() => !isNaN(vid.duration), 1000); switch (NOVA.currentPage) { case 'watch': this.from_description(vid.duration); break; case 'embed': let chaptersContainer; await NOVA.waitUntil(() => ( chaptersContainer = document.body.querySelector('.ytp-chapters-container')) && chaptersContainer?.children.length > 1 , 1000); this.renderChaptersMarkers(vid.duration) || this.from_div(chaptersContainer); break; } }, from_description(duration = required()) { if (Math.sign(duration) !== 1) return console.error('duration not positive number:', duration); const selectorTimestampLink = 'a[href*="&t="]'; NOVA.waitSelector(`ytd-watch-metadata #description.ytd-watch-metadata ${selectorTimestampLink}`, { destroy_if_url_changes: true }) .then(() => this.renderChaptersMarkers(duration)); NOVA.waitSelector(`#comments #comment #comment-content ${selectorTimestampLink}`, { destroy_if_url_changes: true }) .then(() => this.renderChaptersMarkers(duration)); }, from_div(chaptersContainer = required()) { if (!(chaptersContainer instanceof HTMLElement)) return console.error('container not HTMLElement:', chaptersContainer); const progressContainerWidth = parseInt(getComputedStyle(chaptersContainer).width), chaptersOut = document.getElementById(`${SELECTOR_ID}-chapters`); for (const chapter of chaptersContainer.children) { const newChapter = document.createElement('span'), { width, marginLeft, marginRight } = getComputedStyle(chapter), chapterMargin = parseInt(marginLeft) + parseInt(marginRight); newChapter.style.width = (((parseInt(width) + chapterMargin) / progressContainerWidth) * 100) + '%'; chaptersOut.append(newChapter); } }, renderChaptersMarkers(duration) { if (isNaN(duration)) return console.error('duration isNaN:', duration); if (chaptersContainer = document.getElementById(`${SELECTOR_ID}-chapters`)) { chaptersContainer.innerHTML = ''; } const chapterList = NOVA.getChapterList(duration); chapterList ?.forEach((chapter, i, chapters_list) => { const newChapter = document.createElement('span'); const nextChapterSec = chapters_list[i + 1]?.sec || duration; newChapter.style.width = ((nextChapterSec - chapter.sec) / duration) * 100 + '%'; if (chapter.title) newChapter.title = chapter.title; newChapter.setAttribute('time', chapter.time); chaptersContainer.append(newChapter); }); return chapterList; }, }; }, options: { player_float_progress_bar_height: { _tagName: 'input', label: 'Height', 'label:zh': '高度', 'label:ja': '身長', 'label:ko': '키', 'label:id': 'Tinggi', 'label:es': 'Altura', 'label:pt': 'Altura', 'label:fr': 'Hauteur', 'label:it': 'Altezza', 'label:de': 'Höhe', 'label:pl': 'Wysokość', 'label:ua': 'Висота', type: 'number', title: 'in pixels', placeholder: 'px', min: 1, max: 9, value: 3, }, player_float_progress_bar_opacity: { _tagName: 'input', label: 'Opacity', 'label:zh': '不透明度', 'label:ja': '不透明度', 'label:ko': '불투명', 'label:id': 'Kegelapan', 'label:es': 'Opacidad', 'label:pt': 'Opacidade', 'label:fr': 'Opacité', 'label:it': 'Opacità', 'label:de': 'Opazität', 'label:pl': 'Przejrzystość', 'label:ua': 'Прозорість', type: 'number', placeholder: '0-1', step: .05, min: 0, max: 1, value: .7, }, } }); window.nova_plugins.push({ id: 'player-quick-buttons', title: 'Custom player buttons', 'title:zh': '自定义按钮', 'title:ja': 'カスタムボタン', 'title:ko': '사용자 정의 버튼', 'title:id': 'Tombol pemutar khusus', 'title:es': 'Botones personalizados', 'title:pt': 'Botões personalizados', 'title:fr': 'Boutons personnalisés', 'title:it': 'Pulsanti personalizzati del giocatore', 'title:de': 'Benutzerdefinierte Schaltflächen', 'title:pl': 'Własne przyciski odtwarzacza', 'title:ua': 'Власні кнопки відтворювання', run_on_pages: 'watch, embed, -mobile', section: 'player', _runtime: user_settings => { const SELECTOR_BTN_CLASS_NAME = 'nova-right-custom-button', SELECTOR_BTN = '.' + SELECTOR_BTN_CLASS_NAME; NOVA.waitSelector('#movie_player .ytp-right-controls') .then(async container => { NOVA.videoElement = await NOVA.waitSelector('video'); NOVA.css.push( `${SELECTOR_BTN} { user-select: none; } ${SELECTOR_BTN}:hover { color: #66afe9 !important; } ${SELECTOR_BTN}:active { color: #2196f3 !important; }`); NOVA.css.push( `${SELECTOR_BTN}[tooltip]:hover::before { content: attr(tooltip); position: absolute; top: -3em; transform: translateX(-30%); line-height: normal; background-color: rgba(28,28,28,.9); border-radius: 2px; padding: 5px 9px; color: #fff; font-weight: bold; white-space: nowrap; } html[data-cast-api-enabled] ${SELECTOR_BTN}[tooltip]:hover::before { font-weight: normal; }`); if (user_settings.player_buttons_custom_items?.includes('picture-in-picture')) { const pipBtn = document.createElement('button'); pipBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; pipBtn.setAttribute('tooltip', 'Open in PictureInPicture'); pipBtn.innerHTML = createSVG(); pipBtn.addEventListener('click', () => document.pictureInPictureElement ? document.exitPictureInPicture() : NOVA.videoElement.requestPictureInPicture() ); container.prepend(pipBtn); NOVA.videoElement?.addEventListener('enterpictureinpicture', () => pipBtn.innerHTML = createSVG(2)); NOVA.videoElement?.addEventListener('leavepictureinpicture', () => pipBtn.innerHTML = createSVG()); function createSVG(alt) { const svg = document.createElement('svg'); svg.setAttribute('width', '100%'); svg.setAttribute('height', '100%'); svg.setAttribute('viewBox', '-8 -6 36 36'); const path = document.createElement('path'); path.setAttribute('fill', 'currentColor'); path.setAttribute('d', alt ? 'M18.5,11H18v1h.5A1.5,1.5,0,0,1,20,13.5v5A1.5,1.5,0,0,1,18.5,20h-8A1.5,1.5,0,0,1,9,18.5V18H8v.5A2.5,2.5,0,0,0,10.5,21h8A2.5,2.5,0,0,0,21,18.5v-5A2.5,2.5,0,0,0,18.5,11Z M14.5,4H2.5A2.5,2.5,0,0,0,0,6.5v8A2.5,2.5,0,0,0,2.5,17h12A2.5,2.5,0,0,0,17,14.5v-8A2.5,2.5,0,0,0,14.5,4Z' : 'M2.5,17A1.5,1.5,0,0,1,1,15.5v-9A1.5,1.5,0,0,1,2.5,5h13A1.5,1.5,0,0,1,17,6.5V10h1V6.5A2.5,2.5,0,0,0,15.5,4H2.5A2.5,2.5,0,0,0,0,6.5v9A2.5,2.5,0,0,0,2.5,18H7V17Z M18.5,11h-8A2.5,2.5,0,0,0,8,13.5v5A2.5,2.5,0,0,0,10.5,21h8A2.5,2.5,0,0,0,21,18.5v-5A2.5,2.5,0,0,0,18.5,11Z'); svg.append(path); return svg.outerHTML; } } if (user_settings.player_buttons_custom_items?.indexOf('popup') !== -1 && !NOVA.queryURL.has('popup')) { const popupBtn = document.createElement('button'); popupBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; popupBtn.setAttribute('tooltip', 'Open in popup'); popupBtn.innerHTML = `<svg viewBox="-8 -8 36 36" height="100%" width="100%"> <g fill="currentColor"> <path d="M18 2H6v4H2v12h12v-4h4V2z M12 16H4V8h2v6h6V16z M16 12h-2h-2H8V8V6V4h8V12z" /> </g> </svg>`; popupBtn.addEventListener('click', () => { const { width, height } = NOVA.aspectRatio.sizeToFit({ 'srcWidth': NOVA.videoElement.videoWidth, 'srcHeight': NOVA.videoElement.videoHeight, 'maxWidth': screen.width / (+user_settings.player_buttons_custom_popup_width || 4), }); url = new URL( document.querySelector('link[itemprop="embedUrl"][href]')?.href || (location.origin + '/embed/' + movie_player.getVideoData().video_id) ); if (currentTime = ~~NOVA.videoElement?.currentTime) url.searchParams.set('start', currentTime); url.searchParams.set('autoplay', 1); url.searchParams.set('popup', true); openPopup({ 'url': url.href, 'title': document.title, 'width': width, 'height': height }); }); container.prepend(popupBtn); function openPopup({ url, title, width, height }) { const left = (screen.width / 2) - (width / 2); const top = (screen.height / 2) - (height / 2); const newWindow = window.open(url, '_blank', `popup=1,toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=${width},height=${height},top=${top},left=${left}`); newWindow.document.title = title; } } if (user_settings.player_buttons_custom_items?.includes('screenshot')) { const SELECTOR_SCREENSHOT_ID = 'nova-screenshot-result', SELECTOR_SCREENSHOT = '#' + SELECTOR_SCREENSHOT_ID; NOVA.css.push( SELECTOR_SCREENSHOT + ` { --width: 400px; --height: 400px; position: fixed; top: 0; right: 0; overflow: hidden; margin: 36px 30px; box-shadow: 0 0 15px #000; max-width: var(--width); max-height: var(--height); } ${SELECTOR_SCREENSHOT} canvas { max-width: var(--width); max-height: var(--height); } ${SELECTOR_SCREENSHOT} .close-btn { position: absolute; bottom: 0; right: 0; background-color: rgba(0, 0, 0, .5); color: #FFF; cursor: pointer; font-size: 12px; display: grid; height: 100%; width: 25%; } ${SELECTOR_SCREENSHOT} .close-btn:hover { background-color: rgba(0, 0, 0, .65); } ${SELECTOR_SCREENSHOT} .close-btn > * { margin: auto; }`); const screenshotBtn = document.createElement('button'); screenshotBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; screenshotBtn.setAttribute('tooltip', 'Take screenshot'); screenshotBtn.innerHTML = `<svg viewBox="0 -166 512 860" height="100%" width="100%"> <g fill="currentColor"> <circle cx="255.811" cy="285.309" r="75.217" /> <path d="M477,137H352.718L349,108c0-16.568-13.432-30-30-30H191c-16.568,0-30,13.432-30,30l-3.718,29H34 c-11.046,0-20,8.454-20,19.5v258c0,11.046,8.954,20.5,20,20.5h443c11.046,0,20-9.454,20-20.5v-258C497,145.454,488.046,137,477,137 z M255.595,408.562c-67.928,0-122.994-55.066-122.994-122.993c0-67.928,55.066-122.994,122.994-122.994 c67.928,0,122.994,55.066,122.994,122.994C378.589,353.495,323.523,408.562,255.595,408.562z M474,190H369v-31h105V190z" /> </g> </svg>`; screenshotBtn.addEventListener('click', () => { const container = document.getElementById(SELECTOR_SCREENSHOT_ID) || document.createElement('a'), canvas = container.querySelector('canvas') || document.createElement('canvas'); canvas.width = NOVA.videoElement.videoWidth; canvas.height = NOVA.videoElement.videoHeight canvas.getContext('2d').drawImage(NOVA.videoElement, 0, 0, canvas.width, canvas.height); canvas.title = 'Click to save'; try { canvas.toBlob(blob => { container.href = URL.createObjectURL(blob); if (user_settings.player_buttons_custom_screenshot_to_clipboard) { navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]); } }, `image/${user_settings.player_buttons_custom_screenshot || 'png'}`); } catch (error) { } if (!container.id) { container.id = SELECTOR_SCREENSHOT_ID; container.target = '_blank'; if (headerContainer = document.getElementById('masthead-container')) { container.style.marginTop = (headerContainer?.offsetHeight || 0) + 'px'; container.style.zIndex = +getComputedStyle(headerContainer)['z-index'] + 1; } canvas.addEventListener('click', evt => { evt.preventDefault(); downloadCanvasAsImage(evt.target); container.remove(); }); container.append(canvas); const close = document.createElement('a'); close.className = 'close-btn' close.innerHTML = '<span>CLOSE</span>'; close.title = 'Close'; close.addEventListener('click', evt => { evt.preventDefault(); container.remove(); }); container.append(close); document.body.append(container); } }); function downloadCanvasAsImage(canvas) { const downloadLink = document.createElement('a'), downloadFileName = [ movie_player.getVideoData().title .replace(/[\\/:*?"<>|]+/g, '') .replace(/\s+/g, ' ').trim(), `[${NOVA.timeFormatTo.HMS.abbr(NOVA.videoElement.currentTime)}]`, ] .join(' '); downloadLink.href = canvas.toBlob(blob => URL.createObjectURL(blob)); downloadLink.download = downloadFileName + '.' + (user_settings.player_buttons_custom_screenshot || 'png'); downloadLink.click(); } container.prepend(screenshotBtn); } if (user_settings.player_buttons_custom_items?.includes('thumbnail')) { const thumbBtn = document.createElement('button'); thumbBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; thumbBtn.setAttribute('tooltip', 'View Thumbnail'); thumbBtn.innerHTML = `<svg viewBox="0 -10 21 40" height="100%" width="100%"> <g fill="currentColor"> <circle cx='8' cy='7.2' r='2'/> <path d='M0 2v16h20V2H0z M18 16H2V4h16V16z'/> <polygon points='17 10.9 14 7.9 9 12.9 6 9.9 3 12.9 3 15 17 15' /> </g> </svg>`; thumbBtn.addEventListener('click', async () => { const videoId = NOVA.queryURL.get('v') || movie_player.getVideoData().video_id, thumbsSizesTemplate = [ 'maxres', 'sd', 'hq', 'mq', '' ]; document.body.style.cursor = 'wait'; for (const resPrefix of thumbsSizesTemplate) { const imgUrl = `https://i.ytimg.com/vi/${videoId}/${resPrefix}default.jpg`, response = await fetch(imgUrl); if (response.status === 200) { document.body.style.cursor = 'default'; window.open(imgUrl); break; } } }); container.prepend(thumbBtn); } if (user_settings.player_buttons_custom_items?.includes('rotate')) { const hotkey = user_settings.player_buttons_custom_hotkey_rotate || 'r', rotateBtn = document.createElement('button'); rotateBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; rotateBtn.setAttribute('tooltip', `Rotate video (${hotkey})`); Object.assign(rotateBtn.style, { padding: '0 1.1em', }); rotateBtn.innerHTML = `<svg viewBox="0 0 1536 1536" height="100%" width="100%"> <g fill="currentColor"> <path d="M1536 128v448q0 26-19 45t-45 19h-448q-42 0-59-40-17-39 14-69l138-138Q969 256 768 256q-104 0-198.5 40.5T406 406 296.5 569.5 256 768t40.5 198.5T406 1130t163.5 109.5T768 1280q119 0 225-52t179-147q7-10 23-12 14 0 25 9l137 138q9 8 9.5 20.5t-7.5 22.5q-109 132-264 204.5T768 1536q-156 0-298-61t-245-164-164-245T0 768t61-298 164-245T470 61 768 0q147 0 284.5 55.5T1297 212l130-129q29-31 70-14 39 17 39 59z"/> </path> </g> </svg>`; rotateBtn.addEventListener('click', rotateVideo); document.addEventListener('keyup', evt => { if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return; if (evt.key === hotkey) { rotateVideo(); } }); function rotateVideo() { let angle = parseInt(NOVA.videoElement.style.transform.replace(/\D+/, '')) || 0; const scale = (angle === 0 || angle === 180) ? movie_player.clientHeight / NOVA.videoElement.clientWidth : 1; angle += 90; NOVA.videoElement.style.transform = (angle === 360) ? '' : `rotate(${angle}deg) scale(${scale})`; } container.prepend(rotateBtn); } if (user_settings.player_buttons_custom_items?.includes('aspect-ratio')) { const aspectRatioBtn = document.createElement('a'), aspectRatioList = [ { '16:9': 1.335 }, { '4:3': .75 }, { '9:16': 1.777777778 }, { 'auto': 1 }, ], genTooltip = (key = 0) => `Switch aspect ratio to ` + Object.keys(aspectRatioList[key]); aspectRatioBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; aspectRatioBtn.style.textAlign = 'center'; aspectRatioBtn.style.fontWeight = 'bold'; aspectRatioBtn.setAttribute('tooltip', genTooltip()); aspectRatioBtn.innerHTML = '1:1'; aspectRatioBtn.addEventListener('click', () => { if (!NOVA.videoElement) return; const getNextIdx = () => (this.listIdx < aspectRatioList.length - 1) ? this.listIdx + 1 : 0; this.listIdx = getNextIdx(); NOVA.videoElement.style.transform = `scaleX(${Object.values(aspectRatioList[this.listIdx])})`; aspectRatioBtn.setAttribute('tooltip', genTooltip(getNextIdx())); aspectRatioBtn.textContent = Object.keys(aspectRatioList[this.listIdx]); }); container.prepend(aspectRatioBtn); } if (user_settings.player_buttons_custom_items?.includes('watch-later')) { NOVA.waitSelector('.ytp-watch-later-button') .then(watchLaterDefault => { NOVA.css.push( `.${SELECTOR_BTN_CLASS_NAME} .ytp-spinner-container { position: relative; top: 0; left: 0; scale: .5; margin: 0; } .${SELECTOR_BTN_CLASS_NAME}.watch-later-btn svg { scale: .85; }`); const watchLaterBtn = document.createElement('button'); watchLaterBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME} watch-later-btn`; watchLaterBtn.setAttribute('tooltip', 'Watch later'); renderIcon(); watchLaterBtn.addEventListener('click', () => { watchLaterDefault.click(); renderIcon(); const waitStatus = setInterval(() => { if (watchLaterDefault.querySelector('svg')) { clearInterval(waitStatus); renderIcon(); } }, 100); }); [...document.getElementsByClassName(SELECTOR_BTN_CLASS_NAME)].pop() ?.after(watchLaterBtn); function renderIcon() { watchLaterBtn.innerHTML = watchLaterDefault.querySelector('.ytp-watch-later-icon')?.innerHTML; } }); } if (user_settings.player_buttons_custom_items?.includes('card-switch') && !user_settings.player_hide_elements?.includes('videowall_endscreen') && !user_settings.player_hide_elements?.includes('card_endscreen') ) { const cardAttrName = 'nova-hide-endscreen', cardBtn = document.createElement('button'); NOVA.css.push( `#movie_player[${cardAttrName}] .videowall-endscreen, #movie_player[${cardAttrName}] .ytp-pause-overlay, #movie_player[${cardAttrName}] [class^="ytp-ce-"] { display: none !important; }`); cardBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; cardBtn.innerHTML = createSVG(); if (user_settings.player_buttons_custom_card_switch) { switchState(movie_player.toggleAttribute(cardAttrName)); } cardBtn.addEventListener('click', () => switchState(movie_player.toggleAttribute(cardAttrName))); function switchState(state = required()) { cardBtn.innerHTML = createSVG(state) cardBtn.setAttribute('tooltip', `The cards are currently ${state ? 'hidden' : 'showing'}`); } function createSVG(alt) { const svg = document.createElement('svg'); svg.setAttribute('width', '100%'); svg.setAttribute('height', '100%'); svg.setAttribute('viewBox', '-200 0 912 512'); const g = document.createElement('g'); g.setAttribute('fill', 'currentColor'); g.innerHTML = alt ? '<path d="M 409 57.104 C 407.625 57.641, 390.907 73.653, 371.848 92.687 L 337.196 127.293 323.848 120.738 C 301.086 109.561, 283.832 103.994, 265.679 101.969 C 217.447 96.591, 148.112 134.037, 59.026 213.577 C 40.229 230.361, 4.759 265.510, 2.089 270 C -0.440 274.252, -0.674 281.777, 1.575 286.516 C 4.724 293.153, 67.054 352.112, 89.003 369.217 L 92.490 371.934 63.330 401.217 C 37.873 426.781, 34.079 430.988, 33.456 434.346 C 31.901 442.720, 38.176 452.474, 46.775 455.051 C 56.308 457.907, 41.359 471.974, 244.317 269.173 C 350.152 163.421, 429.960 82.914, 431.067 80.790 C 436.940 69.517, 428.155 55.840, 415.185 56.063 C 413.158 56.098, 410.375 56.566, 409 57.104 M 245.500 137.101 C 229.456 139.393, 201.143 151.606, 177.500 166.433 C 151.339 182.839, 120.778 206.171, 89.574 233.561 C 72.301 248.723, 42 277.649, 42 278.977 C 42 280.637, 88.281 323.114, 108.367 339.890 L 117.215 347.279 139.209 325.285 L 161.203 303.292 159.601 293.970 C 157.611 282.383, 157.570 272.724, 159.465 261.881 C 165.856 225.304, 193.011 195.349, 229.712 184.389 C 241.299 180.929, 261.648 179.996, 272.998 182.405 L 280.496 183.996 295.840 168.652 L 311.183 153.309 303.342 149.583 C 292.100 144.242, 277.007 139.186, 267.205 137.476 C 257.962 135.865, 254.565 135.806, 245.500 137.101 M 377.500 163.164 C 374.231 164.968, 369.928 169.297, 368.295 172.423 C 366.203 176.431, 366.351 184.093, 368.593 187.889 C 369.597 189.587, 375.944 195.270, 382.699 200.516 C 406.787 219.226, 444.129 252.203, 462.500 270.989 L 470.500 279.170 459 290.204 C 374.767 371.030, 302.827 418.200, 259.963 420.709 C 239.260 421.921, 213.738 412.918, 179.575 392.352 C 167.857 385.298, 166.164 384.571, 161.448 384.571 C 154.702 384.571, 149.091 388.115, 146.121 394.250 C 143.531 399.600, 143.472 403.260, 145.890 408.500 C 148.270 413.656, 150.468 415.571, 162 422.535 C 198.520 444.590, 230.555 455.992, 256 455.992 C 305.062 455.992, 376.663 414.097, 462 335.458 C 483.584 315.567, 509.652 289.051, 510.931 285.685 C 512.694 281.042, 512.218 273.876, 509.889 270 C 507.494 266.017, 484.252 242.741, 463.509 223.552 C 437.964 199.922, 398.967 167.566, 391.300 163.639 C 387.656 161.773, 380.470 161.526, 377.500 163.164 M 235.651 219.459 C 231.884 220.788, 226.369 223.351, 223.395 225.153 C 216.405 229.389, 206.759 239.019, 202.502 246.010 C 198.959 251.828, 193.677 266.197, 194.194 268.611 C 194.372 269.437, 205.637 258.890, 220.993 243.519 C 249.683 214.801, 249.910 214.427, 235.651 219.459 M 316.962 223.250 C 313.710 224.890, 311.876 226.720, 310.200 230 C 307.188 235.893, 307.781 240.006, 313.805 255 C 317.867 265.109, 318.470 267.589, 318.790 275.500 C 319.554 294.378, 313.786 309.236, 300.522 322.557 C 287.282 335.854, 274.164 341.408, 256 341.408 C 244.216 341.408, 238.392 340.027, 226.837 334.489 C 214.541 328.596, 204.996 330.563, 200.250 339.966 C 191.301 357.697, 210.339 372.220, 247.484 375.998 C 301.141 381.456, 350.063 339.760, 353.664 285.500 C 354.618 271.136, 351.039 249.928, 345.577 237.579 C 342.933 231.601, 337.061 224.600, 332.875 222.435 C 328.782 220.319, 322.095 220.661, 316.962 223.250" fill-rule="evenodd" />' : `<path d="M 377.5 163.164 C 374.231 164.968 375.944 195.27 382.699 200.516 C 406.787 219.226 444.129 252.203 462.5 270.989 L 470.5 279.17 L 459 290.204 C 374.767 371.03 302.827 418.2 259.963 420.709 C 239.26 421.921 213.738 412.918 179.575 392.352 C 167.857 385.298 166.164 384.571 161.448 384.571 C 154.702 384.571 149.091 388.115 146.121 394.25 C 143.531 399.6 143.472 403.26 145.89 408.5 C 148.27 413.656 150.468 415.571 162 422.535 C 198.52 444.59 230.555 455.992 256 455.992 C 305.062 455.992 376.663 414.097 462 335.458 C 483.584 315.567 509.652 289.051 510.931 285.685 C 512.694 281.042 512.218 273.876 509.889 270 C 507.494 266.017 484.252 242.741 463.509 223.552 C 437.964 199.922 398.967 167.566 391.3 163.639 C 387.656 161.773 380.47 161.526 377.5 163.164 M 316.962 223.25 C 313.71 224.89 311.876 226.72 310.2 230 C 307.188 235.893 307.781 240.006 313.805 255 C 317.867 265.109 318.47 267.589 318.79 275.5 C 319.554 294.378 313.786 309.236 300.522 322.557 C 287.282 335.854 274.164 341.408 256 341.408 C 244.216 341.408 238.392 340.027 226.837 334.489 C 214.541 328.596 204.996 330.563 200.25 339.966 C 191.301 357.697 210.339 372.22 247.484 375.998 C 301.141 381.456 350.063 339.76 353.664 285.5 C 354.618 271.136 351.039 249.928 345.577 237.579 C 342.933 231.601 337.061 224.6 332.875 222.435 C 328.782 220.319 322.095 220.661 316.962 223.25"></path> <path d="M 377.487 163.483 C 374.218 165.287 369.915 169.616 368.282 172.742 C 366.19 176.75 366.338 184.412 368.58 188.208 C 369.584 189.906 375.931 195.589 382.686 200.835 C 406.774 219.545 444.116 252.522 462.487 271.308 L 470.487 279.489 L 458.987 290.523 C 374.754 371.349 302.814 418.519 259.95 421.028 C 239.247 422.24 213.725 413.237 179.562 392.671 C 167.844 385.617 166.151 384.89 161.435 384.89 C 154.689 384.89 149.078 388.434 146.108 394.569 C 143.518 399.919 143.459 403.579 145.877 408.819 C 148.257 413.975 150.455 415.89 161.987 422.854 C 198.507 444.909 230.542 456.311 255.987 456.311 C 305.049 456.311 376.65 414.416 461.987 335.777 C 483.571 315.886 509.639 289.37 510.918 286.004 C 512.681 281.361 512.205 274.195 509.876 270.319 C 507.481 266.336 484.239 243.06 463.496 223.871 C 437.951 200.241 398.954 167.885 391.287 163.958 C 387.643 162.092 380.457 161.845 377.487 163.483 M 316.949 223.569 C 313.697 225.209 311.863 227.039 310.187 230.319 C 307.175 236.212 307.768 240.325 313.792 255.319 C 317.854 265.428 318.457 267.908 318.777 275.819 C 319.541 294.697 313.773 309.555 300.509 322.876 C 287.269 336.173 274.151 341.727 255.987 341.727 C 244.203 341.727 238.379 340.346 226.824 334.808 C 214.528 328.915 204.983 330.882 200.237 340.285 C 191.288 358.016 210.326 372.539 247.471 376.317 C 301.128 381.775 350.05 340.079 353.651 285.819 C 354.605 271.455 351.026 250.247 345.564 237.898 C 342.92 231.92 337.048 224.919 332.862 222.754 C 328.769 220.638 322.082 220.98 316.949 223.569" transform="matrix(-1, 0, 0, -1, 512.000305, 558.092285)"></path>`; svg.append(g); return svg.outerHTML; } container.prepend(cardBtn); } if (user_settings.player_buttons_custom_items?.includes('quick-quality')) { const SELECTOR_QUALITY_CLASS_NAME = 'nova-quick-quality', SELECTOR_QUALITY = '.' + SELECTOR_QUALITY_CLASS_NAME, qualityContainerBtn = document.createElement('a'), SELECTOR_QUALITY_LIST_ID = SELECTOR_QUALITY_CLASS_NAME + '-list', SELECTOR_QUALITY_LIST = '#' + SELECTOR_QUALITY_LIST_ID, listQuality = document.createElement('ul'), SELECTOR_QUALITY_TITLE_ID = SELECTOR_QUALITY_CLASS_NAME + '-title', qualityBtn = document.createElement('span'), qualityFormatList = { highres: { label: '4320p', badge: '8K' }, hd2880: { label: '2880p', badge: '5K' }, hd2160: { label: '2160p', badge: '4K' }, hd1440: { label: '1440p', badge: 'QHD' }, hd1080: { label: '1080p', badge: 'FHD' }, hd720: { label: '720p', badge: 'ᴴᴰ' }, large: { label: '480p' }, medium: { label: '360p' }, small: { label: '240p' }, tiny: { label: '144p' }, auto: { label: 'auto' }, }; NOVA.css.push( SELECTOR_QUALITY + ` { overflow: visible !important; position: relative; text-align: center !important; vertical-align: top; font-weight: bold; } ${SELECTOR_QUALITY_LIST} { position: absolute; bottom: 2.5em !important; left: -2.2em; list-style: none; padding-bottom: 1.5em !important; z-index: ${1 + Math.max(NOVA.css.getValue('.ytp-progress-bar', 'z-index'), 31)}; } html[data-cast-api-enabled] ${SELECTOR_QUALITY_LIST} { margin: 0; padding: 0; bottom: 3.3em; } .ytp-big-mode .ytp-menuitem-toggle-checkbox { width: 3.5em; height: 1.6em; } ${SELECTOR_QUALITY}:not(:hover) ${SELECTOR_QUALITY_LIST} { display: none; } ${SELECTOR_QUALITY_LIST} li { cursor: pointer; white-space: nowrap; line-height: 1.4; background: rgba(28, 28, 28, 0.9); margin: .3em 0; padding: .5em 3em; border-radius: .3em; color: #fff; } ${SELECTOR_QUALITY_LIST} li .quality-menu-item-label-badge { position: absolute; right: 1em; width: 1.7em; } ${SELECTOR_QUALITY_LIST} li.active { background: #720000; } ${SELECTOR_QUALITY_LIST} li.disable { color: #666; } ${SELECTOR_QUALITY_LIST} li:hover:not(.active) { background: #c00; }`); qualityContainerBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME} ${SELECTOR_QUALITY_CLASS_NAME}`; qualityBtn.id = SELECTOR_QUALITY_TITLE_ID; qualityBtn.textContent = qualityFormatList[movie_player.getPlaybackQuality()]?.label || '[out of range]'; listQuality.id = SELECTOR_QUALITY_LIST_ID; movie_player.addEventListener('onPlaybackQualityChange', quality => { document.getElementById(SELECTOR_QUALITY_TITLE_ID) .textContent = qualityFormatList[quality]?.label || '[out of range]'; }); qualityContainerBtn.prepend(qualityBtn); qualityContainerBtn.append(listQuality); container.prepend(qualityContainerBtn); fillQualityMenu(); NOVA.videoElement?.addEventListener('canplay', fillQualityMenu); function fillQualityMenu() { if (qualityList = document.getElementById(SELECTOR_QUALITY_LIST_ID)) { qualityList.innerHTML = ''; movie_player.getAvailableQualityLevels() .forEach(quality => { const qualityItem = document.createElement('li'); if (qualityData = qualityFormatList[quality]) { qualityItem.textContent = qualityData.label; if (badge = qualityData.badge) { qualityItem.insertAdjacentHTML('beforeend', `<span class="quality-menu-item-label-badge">${badge}</span>`); } if (movie_player.getPlaybackQuality() == quality) { qualityItem.className = 'active'; } else { const maxWidth = (NOVA.currentPage == 'watch') ? window.screen.width : window.innerWidth; if (+(qualityData.label.replace(/[^0-9]/g, '') || 0) <= (maxWidth * 1.3)) { qualityItem.addEventListener('click', () => { movie_player.setPlaybackQualityRange(quality, quality); if (quality == 'auto') return; qualityList.innerHTML = ''; }); } else { qualityItem.className = 'disable'; qualityItem.title = 'Max (window viewport + 30%)'; } } qualityList.append(qualityItem); } }); } } } if (user_settings.player_buttons_custom_items?.includes('clock')) { const clockEl = document.createElement('span'); clockEl.className = 'ytp-time-display'; clockEl.title = 'Now time'; container.prepend(clockEl); setInterval(() => { if (document.visibilityState == 'hidden' || movie_player.classList.contains('ytp-autohide') ) { return; } const time = new Date().toTimeString().slice(0, 8); clockEl.textContent = time; }, 1000); } if (user_settings.player_buttons_custom_items?.includes('range-speed')) { const speedSlider = document.createElement('input'), SELECTOR_RANGE_CLASS_NAME = 'nova-range-speed-input', SELECTOR_RANGE = '.' + SELECTOR_RANGE_CLASS_NAME; NOVA.css.push( `${SELECTOR_RANGE}[type="range"] { height: 100%; }`); speedSlider.className = `${SELECTOR_BTN_CLASS_NAME} ${SELECTOR_RANGE_CLASS_NAME}`; speedSlider.title = 'Playback Rate'; speedSlider.type = 'range'; speedSlider.min = speedSlider.step = +user_settings.rate_step || .1; speedSlider.max = user_settings.range_speed_unlimit ? +user_settings.rate_default : 2; speedSlider.value = NOVA.videoElement.playbackRate; updateTitleForSpeedSlider(NOVA.videoElement.playbackRate); NOVA.videoElement.addEventListener('ratechange', function () { speedSlider.value = this.playbackRate; updateTitleForSpeedSlider(this.playbackRate); }); speedSlider.addEventListener('change', ({ target }) => playerRate(target.value)); speedSlider.addEventListener('wheel', evt => { evt.preventDefault(); const rate = NOVA.videoElement.playbackRate + (speedSlider.step * Math.sign(evt.wheelDelta)); playerRate(rate); speedSlider.value = rate; }); container.prepend(speedSlider); function playerRate(rate) { if (!user_settings.range_speed_unlimit && rate > 2) return; NOVA.videoElement.playbackRate = (+rate).toFixed(2); updateTitleForSpeedSlider(rate); } function updateTitleForSpeedSlider(rate) { speedSlider.title = `Speed (${rate})`; speedSlider.setAttribute('tooltip', `Speed (${rate})`); } } if (user_settings.player_buttons_custom_items?.includes('toggle-speed')) { const speedBtn = document.createElement('a'), hotkey = user_settings.player_buttons_custom_hotkey_toggle_speed || 'a', defaultRateText = '1x', genTooltip = () => `Switch to ${NOVA.videoElement.playbackRate}>${speedBtn.textContent} (${hotkey})`; let rateOrig = {}; speedBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; speedBtn.style.textAlign = 'center'; speedBtn.style.fontWeight = 'bold'; speedBtn.innerHTML = defaultRateText; speedBtn.setAttribute('tooltip', genTooltip()); document.addEventListener('keyup', evt => { if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return; if (evt.key === hotkey) { switchRate(); } }); speedBtn.addEventListener('click', switchRate); function switchRate() { if (Object.keys(rateOrig).length) { playerRate.set(rateOrig); rateOrig = {}; speedBtn.innerHTML = defaultRateText; } else { rateOrig = (movie_player && NOVA.videoElement.playbackRate % .25) === 0 ? { 'default': movie_player.getPlaybackRate() } : { 'html5': NOVA.videoElement.playbackRate }; let resetRate = Object.assign({}, rateOrig); resetRate[Object.keys(resetRate)[0]] = 1; playerRate.set(resetRate); speedBtn.textContent = rateOrig[Object.keys(rateOrig)[0]] + 'x'; } speedBtn.setAttribute('tooltip', genTooltip()); } const playerRate = { set(obj) { if (obj.hasOwnProperty('html5') || !movie_player) { NOVA.videoElement.playbackRate = obj.html5; } else { movie_player.setPlaybackRate(obj.default); } }, }; container.prepend(speedBtn); visibilitySwitch(); NOVA.videoElement?.addEventListener('ratechange', visibilitySwitch); NOVA.videoElement?.addEventListener('loadeddata', () => { rateOrig = {}; speedBtn.textContent = defaultRateText; visibilitySwitch(); }); function visibilitySwitch() { if (!Object.keys(rateOrig).length) { speedBtn.style.display = (NOVA.videoElement?.playbackRate === 1) ? 'none' : ''; } } } }); }, options: { player_buttons_custom_items: { _tagName: 'select', label: 'Buttons', 'label:zh': '纽扣', 'label:ja': 'ボタン', 'label:ko': '버튼', 'label:id': 'Tombol', 'label:es': 'Botones', 'label:pt': 'Botões', 'label:fr': 'Boutons', 'label:it': 'Bottoni', 'label:de': 'Tasten', 'label:pl': 'Przyciski', 'label:ua': 'Кнопки', title: '[Ctrl+Click] to select several', 'title:zh': '[Ctrl+Click] 选择多个', 'title:ja': '「Ctrl+Click」して、いくつかを選択します', 'title:ko': '[Ctrl+Click] 여러 선택', 'title:id': '[Ctrl+Klik] untuk memilih beberapa', 'title:es': '[Ctrl+Click] para seleccionar varias', 'title:pt': '[Ctrl+Click] para selecionar vários', 'title:fr': '[Ctrl+Click] pour sélectionner plusieurs', 'title:it': '[Ctrl+Clic] per selezionarne diversi', 'title:de': '[Ctrl+Click] um mehrere auszuwählen', 'title:pl': 'Ctrl+kliknięcie, aby zaznaczyć kilka', 'title:ua': '[Ctrl+Click] щоб обрати декілька', multiple: null, required: true, size: 7, options: [ { label: 'clock', value: 'clock', }, { label: 'quick quality', value: 'quick-quality', 'label:zh': '质量', 'label:ja': '品質', 'label:ko': '품질', 'label:id': 'kualitas', 'label:es': 'calidad', 'label:pt': 'qualidade', 'label:fr': 'qualité', 'label:it': 'qualità', 'label:de': 'qualität', 'label:pl': 'jakość', 'label:ua': 'якість', }, { label: 'range speed', value: 'range-speed', }, { label: 'toggle speed', value: 'toggle-speed', 'label:zh': '切换速度', 'label:ja': 'トグル速度', 'label:ko': '토글 속도', 'label:id': 'beralih kecepatan', 'label:es': 'alternar velocidad', 'label:pt': 'velocidade de alternância', 'label:fr': 'basculer la vitesse', 'label:it': 'alternare la velocità', 'label:de': 'geschwindigkeit umschalten', 'label:pl': 'szybkość', 'label:ua': 'швидкість', }, { label: 'card-switch', value: 'card-switch', }, { label: 'screenshot', value: 'screenshot', 'label:zh': '截屏', 'label:ja': 'スクリーンショット', 'label:ko': '스크린샷', 'label:id': 'tangkapan layar', 'label:es': 'captura de pantalla', 'label:pt': 'captura de tela', 'label:fr': "capture d'écran", 'label:it': 'immagine dello schermo', 'label:de': 'bildschirmfoto', 'label:ua': 'фото екрану', }, { label: 'picture-in-picture', value: 'picture-in-picture', 'label:pl': 'obraz w obrazie', 'label:ua': 'картинка в картинці', }, { label: 'popup', value: 'popup', 'label:zh': '弹出式播放器', 'label:ja': 'ポップアッププレーヤー', 'label:ko': '썸네일', 'label:id': 'muncul', 'label:pt': 'jogador pop-up', 'label:fr': 'lecteur contextuel', 'label:it': 'apparire', 'label:de': 'auftauchen', 'label:pl': 'w okienku', 'label:ua': 'спливаюче повідомлення', }, { label: 'rotate', value: 'rotate', 'label:zh': '旋转', 'label:ja': '回転する', 'label:ko': '회전', 'label:id': 'memutar', 'label:es': 'girar', 'label:pt': 'girar', 'label:fr': 'tourner', 'label:it': 'ruotare', 'label:de': 'drehen', 'label:pl': 'obróć', 'label:ua': 'повернути', }, { label: 'aspect-ratio', value: 'aspect-ratio', 'label:ua': 'співвідношення сторін', }, { label: 'watch later', value: 'watch-later', 'label:ua': 'переглянути пізніше', }, { label: 'thumbnail', value: 'thumbnail', 'label:zh': '缩略图', 'label:ja': 'サムネイル', 'label:ko': '썸네일', 'label:es': 'miniatura', 'label:pt': 'captura de tela', 'label:fr': 'la vignette', 'label:it': 'miniatura', 'label:de': 'bildschirmfoto', 'label:pl': 'miniaturka', 'label:ua': 'мініатюра', }, ], }, player_buttons_custom_popup_width: { _tagName: 'input', label: 'Player window size aspect ratio', 'label:zh': '播放器窗口大小纵横比', 'label:ja': 'プレーヤーのウィンドウサイズのアスペクト比', 'label:ko': '플레이어 창 크기 종횡비', 'label:id': 'Rasio aspek ukuran jendela pemutar', 'label:es': 'Relación de aspecto del tamaño de la ventana del reproductor', 'label:pt': 'Proporção do tamanho da janela do jogador', 'label:fr': "Rapport d'aspect de la taille de la fenêtre du lecteur", 'label:it': 'Proporzioni della dimensione della finestra del lettore', 'label:de': 'Seitenverhältnis der Player-Fenstergröße', 'label:pl': 'Rozmiar okna odtwarzacza', 'label:ua': 'Співвідношення розміру вікна відтворювача', type: 'number', title: 'Less value - larger size', 'title:zh': '较小的值 - 较大的尺寸', 'title:ja': '小さい値-大きいサイズ', 'title:ko': '더 작은 값 - 더 큰 크기', 'title:id': 'Nilai lebih kecil - ukuran lebih besar', 'title:es': 'Valor más pequeño - tamaño más grande', 'title:pt': 'Valor menor - tamanho maior', 'title:fr': 'Plus petite valeur - plus grande taille', 'title:it': 'Meno valore - dimensioni maggiori', 'title:de': 'Kleiner Wert - größere Größe', 'title:pl': 'Mniejsza wartość - większy rozmiar', 'title:ua': 'Менше значення - більший розмір', placeholder: '1.5-4', step: 0.1, min: 1.5, max: 4, value: 2.5, 'data-dependent': { 'player_buttons_custom_items': ['popup'] }, }, player_buttons_custom_hotkey_toggle_speed: { _tagName: 'select', label: 'Hotkey toggle speed', 'label:zh': '热键切换速度', 'label:ja': '速度を切り替えるためのホットボタン', 'label:ko': '단축키 토글 속도', 'label:id': 'Kecepatan beralih tombol pintas', 'label:es': 'Velocidad de cambio de teclas de acceso rápido', 'label:pt': 'Velocidade de alternância da tecla de atalho', 'label:fr': 'Vitesse de basculement des raccourcis clavier', 'label:it': 'Tasto di scelta rapida per attivare/disattivare la velocità', 'label:de': 'Hotkey-Umschaltgeschwindigkeit', 'label:pl': 'Skrót przełączania prędkości', 'label:ua': 'Гаряча клавіша увімкнути швидкість', options: [ { label: 'A', value: 'a', selected: true }, 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ']', '[', '+', '-', ',', '.', '/', '<', ';', '\\' ], 'data-dependent': { 'player_buttons_custom_items': ['toggle-speed'] }, }, player_buttons_custom_hotkey_rotate: { _tagName: 'select', label: 'Hotkey rotate', options: [ { label: 'R', value: 'r', selected: true }, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ']', '[', '+', '-', ',', '.', '/', '<', ';', '\\' ], 'data-dependent': { 'player_buttons_custom_items': ['rotate'] }, }, player_buttons_custom_card_switch: { _tagName: 'select', label: 'Default card state', options: [ { label: 'show', value: false, selected: true, }, { label: 'hide', value: true, }, ], 'data-dependent': { 'player_buttons_custom_items': ['card-switch'] }, }, player_buttons_custom_screenshot: { _tagName: 'select', label: 'Screenshot format', options: [ { label: 'png', value: 'png', selected: true, }, { label: 'jpg', value: 'jpg', }, ], 'data-dependent': { 'player_buttons_custom_items': ['screenshot'] }, }, player_buttons_custom_screenshot_to_clipboard: { _tagName: 'input', label: 'Screenshot copy to clipboard', type: 'checkbox', 'data-dependent': { 'player_buttons_custom_items': ['screenshot'] }, }, range_speed_unlimit: { _tagName: 'input', label: 'Range speed unlimit', type: 'checkbox', 'data-dependent': { 'player_buttons_custom_items': ['range-speed'] }, }, } }); window.nova_plugins.push({ id: 'video-unblock-region', title: 'Try unblock if video not available in your country', 'title:zh': '尝试解锁您所在地区的视频', 'title:ja': 'お住まいの地域の動画のブロックを解除してみてください', 'title:ko': '해당 지역의 동영상 차단을 해제해 보세요', 'title:id': 'Coba buka blokir jika video tidak tersedia di negara Anda', 'title:es': 'Intenta desbloquear videos para tu región', 'title:pt': 'Tente desbloquear vídeos para sua região', 'title:fr': 'Débloquer la vidéo de la région', 'title:it': 'Prova a sbloccare se il video non è disponibile nel tuo paese', 'title:de': 'Versuchen Sie, Videos für Ihre Region zu entsperren', 'title:pl': 'Spróbuj odblokować, jeśli film nie jest dostępny w Twoim kraju', 'title:ua': 'Спробувати розблокувати якщо відео не доступне у країні', run_on_pages: 'watch, -mobile', section: 'player', desc: 'Attempt fix "is not available in your country"', 'desc:zh': '尝试修复“在您的国家不可用”', 'desc:ja': '「お住まいの国では利用できません」という修正を試みる', 'desc:ko': '수정 시도 "해당 국가에서는 사용할 수 없습니다"', 'desc:id': 'Coba perbaiki "tidak tersedia di negara Anda"', 'desc:es': 'Intento de corrección "no está disponible en su país"', 'desc:pt': 'Tentativa de correção "não está disponível em seu país"', 'desc:fr': 'Tentative de correction "n\'est pas disponible dans votre pays"', 'desc:it': 'Tentativo di correzione "non è disponibile nel tuo paese"', 'desc:de': 'Versuchen Sie, "ist in Ihrem Land nicht verfügbar" zu beheben', 'desc:pl': 'Próba naprawienia nie jest dostępna w Twoim kraju', 'desc:ua': 'Спроба розблокувати доступ до відео', _runtime: user_settings => { NOVA.waitSelector('ytd-watch-flexy[player-unavailable]', { destroy_if_url_changes: true }) .then(el => el.querySelector('yt-player-error-message-renderer #button.yt-player-error-message-renderer button') || redirect()); function redirect(new_tab_url) { if (new_tab_url) { window.open(`${location.protocol}//${user_settings.video_unblock_region_domain || 'hooktube.com'}${location.port ? ':' + location.port : ''}/watch?v=` + (NOVA.queryURL.get('v') || movie_player.getVideoData().video_id)); } else { location.hostname = user_settings.video_unblock_region_domain || 'hooktube.com'; } if (user_settings.video_unblock_region_open_map) { window.open(`https://watannetwork.com/tools/blocked/#url=${NOVA.queryURL.get('v')}:~:text=Allowed%20countries`); } } }, options: { video_unblock_region_domain: { _tagName: 'input', label: 'URL', type: 'text', list: 'video_unblock_region_domain_help_list', pattern: "^[a-zA-Z0-9-]{2,20}\.[a-zA-Z]{2,5}$", title: 'without "https://"', 'title:zh': '没有“https://”', 'title:ja': '「https://」なし', 'title:ko': '"https://" 없이', 'title:id': 'tanpa "https://"', 'title:es': 'sin "https://"', 'title:pt': 'sem "https://"', 'title:fr': 'sans "https://"', 'title:it': 'senza "https://"', 'title:de': 'ohne "https://"', 'title:pl': 'bez „https://”', 'title:ua': 'без "https://"', placeholder: 'hooktube.com', minlength: 5, maxlength: 20, required: true, }, video_unblock_region_domain_help_list: { _tagName: 'datalist', options: [ { label: 'hooktube.com', value: 'hooktube.com' }, { label: 'clipzag.com', value: 'clipzag.com' }, { label: 'piped.video', value: 'piped.video' }, { label: 'yewtu.be', value: 'yewtu.be' }, { label: 'nsfwyoutube.com', value: 'nsfwyoutube.com' }, { label: 'yout-ube.com', value: 'yout-ube.com' }, ], }, video_unblock_region_open_map: { _tagName: 'input', label: 'Open map with availability in regions', 'label:zh': '打开地图,显示区域可用性', 'label:ja': '地域で利用可能なマップを開く', 'label:ko': '지역에서 사용 가능한 지도 열기', 'label:id': 'Buka peta dengan ketersediaan di wilayah', 'label:es': 'Abrir mapa con disponibilidad en regiones', 'label:pt': 'Abrir mapa com disponibilidade nas regiões', 'label:fr': 'Carte ouverte avec disponibilité dans les régions', 'label:it': 'Apri la mappa con la disponibilità nelle regioni', 'label:de': 'Karte mit Verfügbarkeit in Regionen öffnen', 'label:pl': 'Otwórz mapę z dostępnością w regionach', 'label:ua': 'Відкрити карту з доступністю в регіонах', type: 'checkbox', }, } }); window.nova_plugins.push({ id: 'subtitle-style', title: 'Subtitles (captions) style', 'title:zh': '字幕样式', 'title:ja': '字幕スタイル', 'title:ko': '자막 스타일', 'title:id': 'Gaya subtitel', 'title:es': 'Estilo de subtítulos', 'title:pt': 'estilo de legenda', 'title:fr': 'Style de sous-titre', 'title:it': 'Stile dei sottotitoli', 'title:de': 'Untertitelstil', 'title:pl': 'Styl napisów', 'title:ua': 'Стиль субтитрів', run_on_pages: 'watch, embed, -mobile', section: 'player', _runtime: user_settings => { const SELECTOR = '.ytp-caption-segment'; let css = {} if (user_settings.subtitle_transparent) { css = { 'background': 'Transparent', 'text-shadow': `rgb(0, 0, 0) 0 0 .1em, rgb(0, 0, 0) 0 0 .2em, rgb(0, 0, 0) 0 0 .4em`, }; } if (user_settings.subtitle_bold) css['font-weight'] = 'bold'; if (user_settings.subtitle_fixed) { NOVA.css.push( `.caption-window { margin-bottom: 1px !important; bottom: 1% !important; }`); } if (user_settings.subtitle_selectable) { NOVA.watchElements({ selectors: [ SELECTOR, '#caption-window-1', ] .map(i => i + ':not(:empty)'), callback: el => { el.addEventListener('mousedown', evt => evt.stopPropagation(), true); el.setAttribute('draggable', 'false'); el.setAttribute('selectable', 'true'); el.style.userSelect = 'text'; el.style.WebkitUserSelect = 'text'; el.style.cursor = 'text'; } }); } if (user_settings.subtitle_font_size) { NOVA.css.push( `.ytp-caption-segment { font-size: ${+user_settings.subtitle_font_size}em !important; }`); } if (Object.keys(css).length) { NOVA.css.push(css, SELECTOR, 'important'); } }, options: { subtitle_transparent: { _tagName: 'input', label: 'Transparent', 'label:zh': '透明的', 'label:ja': '透明', 'label:ko': '투명한', 'label:id': 'Transparan', 'label:es': 'Transparentes', 'label:pt': 'Transparentes', 'label:fr': 'Transparents', 'label:it': 'Trasparenti', 'label:de': 'Transparente', 'label:pl': 'Przezroczyste', 'label:ua': 'Прозорі', type: 'checkbox', }, subtitle_bold: { _tagName: 'input', label: 'Bold text', 'label:zh': '粗体', 'label:ja': '太字', 'label:ko': '굵은 텍스트', 'label:id': 'Teks tebal', 'label:es': 'Texto en negrita', 'label:pt': 'Texto em negrito', 'label:fr': 'Texte en gras', 'label:it': 'Testo grassetto', 'label:de': 'Fetter Text', 'label:pl': 'Tekst pogrubiony', 'label:ua': 'Жирний текст', type: 'checkbox', }, subtitle_fixed: { _tagName: 'input', label: 'Fixed from below', 'label:zh': '从下方固定', 'label:ja': '下から固定', 'label:ko': '아래에서 고정', 'label:id': 'Diperbaiki dari bawah', 'label:es': 'Fijado desde abajo', 'label:pt': 'Fixo por baixo', 'label:fr': 'Fixé par le bas', 'label:it': 'Risolto dal basso', 'label:de': 'Von unten befestigt', 'label:pl': 'Przyklejone na dole', 'label:ua': 'Фіксація знизу', type: 'checkbox', title: 'Preventing captions jumping up/down when pause/resume', 'title:zh': '暂停/恢复时防止字幕跳上/跳下', 'title:ja': '一時停止/再開時にキャプションが上下にジャンプしないようにする', 'title:ko': '일시 중지/다시 시작 시 캡션이 위/아래로 점프하는 것을 방지', 'title:id': 'Mencegah teks melompat ke atas/bawah saat menjeda/melanjutkan', 'title:es': 'Evitar que los subtítulos salten hacia arriba/abajo al pausar/reanudar', 'title:pt': 'Evitando que as legendas subam/descem ao pausar/reiniciar', 'title:fr': "Empêcher les sous-titres de sauter vers le haut/bas lors d'une pause/reprise", 'title:it': 'Prevenire i sottotitoli che saltano su/giù durante la pausa/ripresa', 'title:de': 'Verhindern, dass Untertitel beim Anhalten/Fortsetzen nach oben/unten springen', 'title:pl': 'Zapobieganie przeskakiwaniu napisów w górę/w dół podczas pauzy/wznowienia', 'title:ua': 'Запобігання стрибкам титрів вгору/вниз під час паузи/продовження', }, subtitle_selectable: { _tagName: 'input', label: 'Make selectable', 'label:zh': '使字幕可选', 'label:ja': '字幕を選択可能にする', 'label:ko': '자막 선택 가능', 'label:id': 'Jadikan subtitle dapat dipilih', 'label:es': 'Hacer subtítulos seleccionables', 'label:pt': 'Tornar as legendas selecionáveis', 'label:fr': 'Rendre les sous-titres sélectionnables', 'label:it': 'Rendi i sottotitoli selezionabili', 'label:de': 'Untertitel auswählbar machen', 'label:pl': 'Ustaw napisy do wyboru', 'label:ua': 'Зробити субтитри доступними для виділення', type: 'checkbox', }, subtitle_font_size: { _tagName: 'input', label: 'Font size', type: 'number', title: '0 - disable', placeholder: '0-5', step: 1, min: 0, max: 5, value: 0, }, } }); window.nova_plugins.push({ id: 'video-stop-preload', title: 'Stop video preload', 'title:zh': '停止视频预加载', 'title:ja': 'ビデオのプリロードを停止します', 'title:ko': '비디오 미리 로드 중지', 'title:id': 'Hentikan pramuat video', 'title:es': 'Detener la precarga de video', 'title:pt': 'Parar o pré-carregamento de vídeo', 'title:fr': 'Arrêter le préchargement de la vidéo', 'title:it': 'Interrompi il precaricamento del video', 'title:de': 'Beenden Sie das Vorladen des Videos', 'title:pl': 'Zatrzymaj ładowanie wideo', 'title:ua': 'Зупинити передзавантаження відео', run_on_pages: 'watch, embed', section: 'player', desc: 'Prevent auto-buffering', _runtime: user_settings => { if (user_settings.stop_preload_embed && NOVA.currentPage != 'embed') return; if (location.hostname == 'youtube.googleapis.com') return; if (NOVA.queryURL.has('popup')) return; if (NOVA.currentPage == 'embed' && window.self !== window.top && ['0', 'false'].includes(NOVA.queryURL.get('autoplay')) ) { return; } NOVA.waitSelector('#movie_player') .then(async movie_player => { let disableStop; document.addEventListener('yt-navigate-start', () => disableStop = false); await NOVA.waitUntil(() => typeof movie_player === 'object' && typeof movie_player.stopVideo === 'function'); movie_player.stopVideo(); movie_player.addEventListener('onStateChange', onPlayerStateChange.bind(this)); function onPlayerStateChange(state) { if (user_settings.stop_preload_ignore_playlist && location.search.includes('list=')) return; if (user_settings.stop_preload_ignore_live && movie_player.getVideoData().isLive) return; if (!disableStop && state > 0 && state < 5) { movie_player.stopVideo(); } } document.addEventListener('keyup', ({ code }) => (code == 'Space') && disableHoldStop()); document.addEventListener('click', evt => { if (//movie_player.contains(document.activeElement) || evt.isTrusted && ['button[class*="play-button"]', '.ytp-cued-thumbnail-overlay-image', '.ytp-player-content' ].some(s => evt.srcElement.matches(s)) ) { disableHoldStop(); } }); function disableHoldStop() { if (!disableStop) { disableStop = true; movie_player.playVideo(); } } }); }, options: { stop_preload_ignore_playlist: { _tagName: 'input', label: 'Ignore playlist', 'label:zh': '忽略播放列表', 'label:ja': 'プレイリストを無視する', 'label:ko': '재생목록 무시', 'label:id': 'Abaikan daftar putar', 'label:es': 'Ignorar lista de reproducción', 'label:pt': 'Ignorar lista de reprodução', 'label:fr': 'Ignorer la liste de lecture', 'label:it': 'Ignora playlist', 'label:de': 'Wiedergabeliste ignorieren', 'label:pl': 'Zignoruj listę odtwarzania', 'label:ua': 'Ігнорувати список відтворення', type: 'checkbox', 'data-dependent': { 'stop_preload_embed': false }, }, stop_preload_ignore_live: { _tagName: 'input', label: 'Ignore live', 'label:ua': 'Ігнорувати живі трансляції', type: 'checkbox', 'data-dependent': { 'stop_preload_embed': false }, }, stop_preload_embed: { _tagName: 'select', label: 'Apply to video type', 'label:ua': 'Застосувати до відео', options: [ { label: 'all', value: false, selected: true, 'label:ua': 'всіх', }, { label: 'embed', value: 'on', 'label:ua': 'вбудованих', }, ], }, } }); window.nova_plugins.push({ id: 'player-resume-playback', title: 'Remember playback time', 'title:zh': '恢复播放时间状态', 'title:ja': '再生時間の位置を再開します', 'title:ko': '재생 시간 위치 재개', 'title:id': 'Lanjutkan posisi waktu pemutaran', 'title:es': 'Reanudar posición de tiempo de reproducción', 'title:pt': 'Retomar a posição do tempo de reprodução', 'title:fr': 'Reprendre la position de temps de lecture', 'title:it': 'Riprende la posizione del tempo di riproduzione', 'title:de': 'Wiedergabezeitposition fortsetzen', 'title:pl': 'Powrót do pozycji czasowej odtwarzania', 'title:ua': 'Запам`ятати час відтворення', run_on_pages: 'watch, embed', section: 'player', desc: 'On page reload - resume playback', 'desc:zh': '在页面重新加载 - 恢复播放', 'desc:ja': 'ページがリロードされると、再生が復元されます', 'desc:ko': '페이지 새로고침 시 - 재생 재개', 'desc:id': 'Muat ulang halaman - lanjutkan pemutaran', 'desc:es': 'En la recarga de la página - reanudar la reproducción', 'desc:pt': 'Recarregar na página - retomar a reprodução', 'desc:fr': 'Lors du rechargement de la page - reprendre la lecture', 'desc:it': 'Ricarica alla pagina: riprende la riproduzione', 'desc:de': 'Auf Seite neu laden - Wiedergabe fortsetzen', 'desc:pl': 'Przy ponownym załadowaniu strony - wznawiaj odtwarzanie', 'desc:ua': 'Після завантаження - продовжити відтворення', _runtime: user_settings => { if (!navigator.cookieEnabled && NOVA.currentPage == 'embed') return; const CACHE_PREFIX = 'nova-resume-playback-time', getCacheName = () => CACHE_PREFIX + ':' + (NOVA.queryURL.get('v') || movie_player.getVideoData().video_id); let cacheName; NOVA.waitSelector('video') .then(video => { cacheName = getCacheName(); resumePlayback.apply(video); video.addEventListener('loadeddata', resumePlayback.bind(video)); video.addEventListener('timeupdate', savePlayback.bind(video)); video.addEventListener('ended', () => sessionStorage.removeItem(cacheName)); if (user_settings.player_resume_playback_url_mark && NOVA.currentPage != 'embed') { if (NOVA.queryURL.has('t')) { document.addEventListener('yt-navigate-finish', connectSaveStateInURL.bind(video), { capture: true, once: true }); } else { connectSaveStateInURL.apply(video); } } }); function savePlayback() { if (this.currentTime > 5 && this.duration > 30 && !movie_player.classList.contains('ad-showing')) { sessionStorage.setItem(cacheName, ~~this.currentTime); } } async function resumePlayback() { if (NOVA.queryURL.has('t') || (user_settings['save-channel-state'] && await NOVA.storage_obj_manager.getParam('ignore-playback')) ) { return; } cacheName = getCacheName(); if ((time = +sessionStorage.getItem(cacheName)) && (time < (this.duration - 1)) ) { this.currentTime = time; } } function connectSaveStateInURL() { let delaySaveOnPauseURL; this.addEventListener('pause', () => { if (this.currentTime < (this.duration - 1) && this.currentTime > 5 && this.duration > 10) { delaySaveOnPauseURL = setTimeout(() => { NOVA.updateUrl(NOVA.queryURL.set({ 't': ~~this.currentTime + 's' })); }, 100); } }); this.addEventListener('play', () => { if (typeof delaySaveOnPauseURL === 'number') clearTimeout(delaySaveOnPauseURL); if (NOVA.queryURL.has('t')) NOVA.updateUrl(NOVA.queryURL.remove('t')); }); } }, options: { player_resume_playback_url_mark: { _tagName: 'input', label: 'Mark time in URL when paused', 'label:zh': '暂停时在 URL 中节省时间', 'label:ja': '一時停止したときにURLで時間を節約する', 'label:ko': '일시 중지 시 URL에 시간 표시', 'label:id': 'Tandai waktu di URL saat dijeda', 'label:es': 'Marcar tiempo en URL cuando está en pausa', 'label:pt': 'Marcar tempo no URL quando pausado', 'label:fr': "Marquer l'heure dans l'URL en pause", 'label:it': "Segna il tempo nell'URL quando è in pausa", 'label:de': 'Zeit in URL markieren, wenn pausiert', 'label:pl': 'Zaznacz czas w adresie URL po wstrzymaniu', 'label:ua': 'Маркувати час в URL-посиланні під час паузи', type: 'checkbox', title: 'Makes sense when saving bookmarks', 'title:zh': '保存书签时有意义', 'title:ja': 'ブックマークを保存するときに意味があります', 'title:ko': '북마크를 저장할 때 의미가 있습니다.', 'title:id': 'Masuk akal saat menyimpan bookmark', 'title:es': 'Tiene sentido al guardar marcadores', 'title:pt': 'Faz sentido ao salvar favoritos', 'title:fr': "Cela a du sens lors de l'enregistrement de signets", 'title:it': 'Ha senso quando si salvano i segnalibri', 'title:de': 'Sinnvoll beim Speichern von Lesezeichen', 'title:pl': 'Ma sens podczas zapisywania zakładek', 'title:ua': 'Має сенс при збереженні закладок', }, } }); window.nova_plugins.push({ id: 'video-quality', title: 'Video quality', 'title:zh': '视频质量', 'title:ja': 'ビデオ品質', 'title:ko': '비디오 품질', 'title:id': 'Kualitas video', 'title:es': 'Calidad de video', 'title:pt': 'Qualidade de vídeo', 'title:fr': 'Qualité vidéo', 'title:it': 'Qualità video', 'title:de': 'Videoqualität', 'title:pl': 'Jakość wideo', 'title:ua': 'Якість відео', run_on_pages: 'watch, embed', section: 'player', _runtime: user_settings => { const qualityFormatListWidth = { highres: 4320, hd2880: 2880, hd2160: 2160, hd1440: 1440, hd1080: 1080, hd720: 720, large: 480, medium: 360, small: 240, tiny: 144, }; let selectedQuality = user_settings.video_quality; NOVA.waitSelector('#movie_player') .then(movie_player => { if (user_settings.video_quality_manual_save_in_tab && NOVA.currentPage == 'watch' ) { movie_player.addEventListener('onPlaybackQualityChange', quality => { if (document.activeElement.getAttribute('role') == 'menuitemradio' && quality !== selectedQuality ) { console.info(`keep quality "${quality}" in the session`); selectedQuality = quality; user_settings.video_quality_for_music = false; user_settings.video_quality_for_fullscreen = false; } }); } if (user_settings['save-channel-state']) { NOVA.runOnPageInitOrTransition(async () => { if ((NOVA.currentPage == 'watch' || NOVA.currentPage == 'embed') && (userQuality = await NOVA.storage_obj_manager.getParam('quality')) ) { selectedQuality = userQuality; } }); } setQuality(); movie_player.addEventListener('onStateChange', setQuality); if (user_settings.video_quality_for_fullscreen) { let selectedQualityBackup = selectedQuality; document.addEventListener('fullscreenchange', () => { selectedQuality = NOVA.isFullscreen() ? user_settings.video_quality_for_fullscreen : selectedQualityBackup; movie_player.setPlaybackQualityRange(selectedQuality, selectedQuality); }); } }); async function setQuality(state) { if (!selectedQuality) return console.error('selectedQuality unavailable', selectedQuality); if (user_settings.video_quality_for_music && location.search.includes('list=') && NOVA.isMusic() ) { selectedQuality = user_settings.video_quality_for_music; } if ((1 == state || 3 == state) && !this.quality_lock) { this.quality_lock = true; let availableQualityLevels; await NOVA.waitUntil(() => (availableQualityLevels = movie_player.getAvailableQualityLevels()) && availableQualityLevels.length, 50) const maxWidth = (NOVA.currentPage == 'watch') ? window.screen.width : window.innerWidth; const maxQualityIdx = availableQualityLevels.findIndex(i => qualityFormatListWidth[i] <= (maxWidth * 1.3)); availableQualityLevels = availableQualityLevels.slice(maxQualityIdx); const availableQualityIdx = function () { let i = availableQualityLevels.indexOf(selectedQuality); if (i === -1) { const availableQuality = Object.keys(qualityFormatListWidth) .filter(v => availableQualityLevels.includes(v) || (v == selectedQuality)), nearestQualityIdx = availableQuality.findIndex(q => q === selectedQuality) - 1; i = availableQualityLevels[nearestQualityIdx] ? nearestQualityIdx : 0; } return i; }(); const newQuality = availableQualityLevels[availableQualityIdx]; if (movie_player.hasOwnProperty('setPlaybackQuality')) { movie_player.setPlaybackQuality(newQuality); } if (movie_player.hasOwnProperty('setPlaybackQualityRange')) { movie_player.setPlaybackQualityRange(newQuality, newQuality); } } else if (state <= 0) { this.quality_lock = false; } } NOVA.waitSelector('.ytp-error [class*="reason"]', { destroy_if_url_changes: true }) .then(error_reason_el => { if (alertText = error_reason_el.textContent) { throw alertText; } }); }, options: { video_quality: { _tagName: 'select', label: 'Default quality', 'label:zh': '默认视频质量', 'label:ja': 'デフォルトのビデオ品質', 'label:ko': '기본 비디오 품질', 'label:id': 'Kualitas bawaan', 'label:es': 'Calidad predeterminada', 'label:pt': 'Qualidade padrão', 'label:fr': 'Qualité par défaut', 'label:it': 'Qualità predefinita', 'label:de': 'Standardvideoqualität', 'label:pl': 'Domyślna jakość', 'label:ua': 'Звичайна якість', options: [ { label: '8K/4320p', value: 'highres' }, { label: '4K/2160p', value: 'hd2160' }, { label: 'QHD/1440p', value: 'hd1440' }, { label: 'FHD/1080p', value: 'hd1080', selected: true }, { label: 'HD/720p', value: 'hd720' }, { label: 'SD/480p', value: 'large' }, { label: 'SD/360p', value: 'medium' }, { label: 'SD/240p', value: 'small' }, { label: 'SD/144p', value: 'tiny' }, ], }, video_quality_manual_save_in_tab: { _tagName: 'input', label: 'Save manually selected for the same tab', 'label:zh': '手动选择的质量保存在当前选项卡中', 'label:ja': '手動で選択した品質が現在のタブに保存されます', 'label:ko': '동일한 탭에 대해 수동으로 선택한 저장', 'label:id': 'Simpan dipilih secara manual untuk tab yang sama', 'label:es': 'Guardar seleccionado manualmente para la misma pestaña', 'label:pt': 'Salvar selecionado manualmente para a mesma guia', 'label:fr': 'Enregistrer sélectionné manuellement pour le même onglet', 'label:it': 'Salva selezionato manualmente per la stessa scheda', 'label:de': 'Manuell für dieselbe Registerkarte ausgewählt speichern', 'label:pl': 'Właściwości dla obecnej karty', 'label:ua': 'Зберігати власноруч обрану якість для вкладки', type: 'checkbox', title: 'Affects to next videos', 'title:zh': '对下一个视频的影响', 'title:ja': '次の動画への影響', 'title:ko': '다음 동영상에 영향', 'title:id': 'Mempengaruhi video berikutnya', 'title:es': 'Afecta a los siguientes videos', 'title:pt': 'Afeta para os próximos vídeos', 'title:fr': 'Affecte aux prochaines vidéos', 'title:it': 'Influisce sui prossimi video', 'title:de': 'Beeinflusst die nächsten Videos', 'title:pl': 'Zmiany w następnych filmach', 'title:ua': 'Впливає на наступні відео', }, video_quality_for_music: { _tagName: 'select', label: 'For music (in playlists)', 'label:ua': 'Змінити якість музики у списках відтворення', title: 'to save traffic / increase speed', 'title:zh': '节省流量/提高速度', 'title:ja': 'トラフィックを節約/速度を上げる', 'title:ko': '트래픽 절약 / 속도 향상', 'title:id': 'untuk menghemat lalu lintas / meningkatkan kecepatan', 'title:es': 'para ahorrar tráfico / aumentar la velocidad', 'title:pt': 'para economizar tráfego / aumentar a velocidade', 'title:fr': 'économiser du trafic / augmenter la vitesse', 'title:it': 'per risparmiare traffico / aumentare la velocità', 'title:de': 'um Verkehr zu sparen / Geschwindigkeit zu erhöhen', 'title:pl': 'aby zaoszczędzić ruch / zwiększyć prędkość', 'title:ua': 'для економії трафіку / збільшення швидкості', options: [ { label: 'QHD/1440p', value: 'hd1440' }, { label: 'FHD/1080p', value: 'hd1080' }, { label: 'HD/720p', value: 'hd720' }, { label: 'SD/480p', value: 'large' }, { label: 'SD/360p', value: 'medium' }, { label: 'SD/240p', value: 'small' }, { label: 'SD/144p', value: 'tiny' }, { label: 'default', selected: true }, ], }, video_quality_for_fullscreen: { _tagName: 'select', label: 'For fullscreen', options: [ { label: '8K/4320p', value: 'highres' }, { label: '4K/2160p', value: 'hd2160' }, { label: 'QHD/1440p', value: 'hd1440' }, { label: 'FHD/1080p', value: 'hd1080' }, { label: 'HD/720p', value: 'hd720' }, { label: 'SD/480p', value: 'large' }, { label: 'SD/360p', value: 'medium' }, { label: 'default', selected: true }, ], }, } }); window.nova_plugins.push({ id: 'player-pin-scroll', title: 'Pin player while scrolling', 'title:zh': '滚动时固定播放器', 'title:ja': 'スクロール中にプレイヤーを固定する', 'title:ko': '스크롤하는 동안 플레이어 고정', 'title:id': 'Sematkan pemutar saat menggulir', 'title:es': 'Fijar jugador mientras se desplaza', 'title:pt': 'Fixar jogador enquanto rola', 'title:fr': 'Épingler le lecteur pendant le défilement', 'title:it': 'Blocca il lettore durante lo scorrimento', 'title:de': 'Pin-Player beim Scrollen', 'title:pl': 'Przypnij odtwarzacz podczas przewijania', 'title:ua': 'Закріпити відтворювач коли гортаєш сторінку', run_on_pages: 'watch, -mobile', section: 'player', desc: 'Show mini player when scrolling down', _runtime: user_settings => { if (!('IntersectionObserver' in window)) return alert('Nova\n\nPin player Error!\nIntersectionObserver not supported.'); const CLASS_VALUE = 'nova-player-pin', PINNED_SELECTOR = '.' + CLASS_VALUE, UNPIN_BTN_CLASS_VALUE = CLASS_VALUE + '-unpin-btn', UNPIN_BTN_SELECTOR = '.' + UNPIN_BTN_CLASS_VALUE; document.addEventListener('scroll', () => { NOVA.waitSelector('#ytd-player') .then(container => { new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { movie_player.classList.remove(CLASS_VALUE); drag.reset(); } else if (!movie_player.isFullscreen() && document.documentElement.scrollTop ) { movie_player.classList.add(CLASS_VALUE); drag?.storePos?.X && drag.setTranslate(drag.storePos); } window.dispatchEvent(new Event('resize')); }, { threshold: .5, }) .observe(container); }); }, { capture: true, once: true }); NOVA.waitSelector(PINNED_SELECTOR) .then(async player => { drag.init(player); await NOVA.waitUntil( () => (NOVA.videoElement?.videoWidth && !isNaN(NOVA.videoElement.videoWidth) && NOVA.videoElement?.videoHeight && !isNaN(NOVA.videoElement.videoHeight) ) , 500) initMiniStyles(); insertUnpinButton(player); document.addEventListener('fullscreenchange', () => NOVA.isFullscreen() && movie_player.classList.remove(CLASS_VALUE)); NOVA.waitSelector('#movie_player video') .then(video => { video.addEventListener('loadeddata', () => { if (NOVA.currentPage != 'watch') return; NOVA.waitSelector(PINNED_SELECTOR) .then(() => { const width = NOVA.aspectRatio.calculateWidth( movie_player.clientHeight, NOVA.aspectRatio.chooseAspectRatio({ 'width': NOVA.videoElement.videoWidth, 'height': NOVA.videoElement.videoHeight, 'layout': 'landscape', }), ); player.style.setProperty('--width', `${width}px !important;`); }); }); }); if (user_settings.player_float_scroll_after_fullscreen_restore_srcoll_pos) { let scrollPos = 0; document.addEventListener('fullscreenchange', () => { if (!NOVA.isFullscreen() && scrollPos && drag.storePos ) { window.scrollTo({ top: scrollPos, }); } }); document.addEventListener('yt-action', function (evt) { if (evt.detail?.actionName == 'yt-close-all-popups-action') { scrollPos = document.documentElement.scrollTop; } }); document.addEventListener('yt-navigate-start', () => scrollPos = 0); } }); function initMiniStyles() { const scrollbarWidth = (window.innerWidth - document.documentElement.clientWidth || 0) + 'px'; const miniSize = NOVA.aspectRatio.sizeToFit({ 'srcWidth': NOVA.videoElement.videoWidth, 'srcHeight': NOVA.videoElement.videoHeight, 'maxWidth': (window.innerWidth / user_settings.player_float_scroll_size_ratio), 'maxHeight': (window.innerHeight / user_settings.player_float_scroll_size_ratio), }); let initcss = { width: NOVA.aspectRatio.calculateWidth( miniSize.height, NOVA.aspectRatio.chooseAspectRatio({ 'width': miniSize.width, 'height': miniSize.height }) ) + 'px', height: miniSize.height + 'px', position: 'fixed', 'z-index': 'var(--zIndex)', 'box-shadow': '0 16px 24px 2px rgba(0, 0, 0, 0.14),' + '0 6px 30px 5px rgba(0, 0, 0, 0.12),' + '0 8px 10px -5px rgba(0, 0, 0, 0.4)', }; switch (user_settings.player_float_scroll_position) { case 'top-left': initcss.top = user_settings['header-unfixed'] ? 0 : (document.getElementById('masthead-container')?.offsetHeight || 0) + 'px'; initcss.left = 0; break; case 'top-right': initcss.top = user_settings['header-unfixed'] ? 0 : (document.getElementById('masthead-container')?.offsetHeight || 0) + 'px'; initcss.right = scrollbarWidth; break; case 'bottom-left': initcss.bottom = 0; initcss.left = 0; break; case 'bottom-right': initcss.bottom = 0; initcss.right = scrollbarWidth; break; } NOVA.css.push(initcss, PINNED_SELECTOR, 'important'); NOVA.css.push( PINNED_SELECTOR + `{ --height: ${initcss.height} !important; --width: ${initcss.width} !important; width: var(--width) !important; height: var(--height) !important; background-color: var(--yt-spec-base-background); ${user_settings['square-avatars'] ? '' : 'border-radius: 1em;'} margin: 1em 2em; } ${PINNED_SELECTOR} video { object-fit: contain !important; } ${PINNED_SELECTOR} .ytp-chrome-controls .nova-right-custom-button, ${PINNED_SELECTOR} .ytp-chrome-controls #nova-player-time-remaining, ${PINNED_SELECTOR} .ytp-chrome-controls button.ytp-size-button, ${PINNED_SELECTOR} .ytp-chrome-controls button.ytp-subtitles-button, ${PINNED_SELECTOR} .ytp-chrome-controls button.ytp-settings-button, ${PINNED_SELECTOR} .ytp-chrome-controls .ytp-chapter-container { display: none !important; }`); NOVA.css.push(` ${PINNED_SELECTOR} .ytp-preview, ${PINNED_SELECTOR} .ytp-scrubber-container, ${PINNED_SELECTOR} .ytp-hover-progress, ${PINNED_SELECTOR} .ytp-gradient-bottom { display:none !important; } ${PINNED_SELECTOR} .ytp-chrome-bottom { width: 96% !important; } ${PINNED_SELECTOR} .ytp-chapters-container { display: flex; }`); NOVA.css.push( `${PINNED_SELECTOR} video { width: var(--width) !important; height: var(--height) !important; left: 0 !important; top: 0 !important; } .ended-mode video { visibility: hidden; }`); } function insertUnpinButton(player = movie_player) { NOVA.css.push( PINNED_SELECTOR + ` { --zIndex: ${1 + Math.max( NOVA.css.getValue('#chat', 'z-index'), NOVA.css.getValue('.ytp-chrome-top .ytp-cards-button', 'z-index'), NOVA.css.getValue('#chat', 'z-index'), 601)}; } ${UNPIN_BTN_SELECTOR} { display: none; } ${PINNED_SELECTOR} ${UNPIN_BTN_SELECTOR} { display: initial !important; position: absolute; cursor: pointer; top: 10px; left: 10px; width: 28px; height: 28px; color: white; border: none; outline: none; opacity: .1; z-index: var(--zIndex); font-size: 24px; font-weight: bold; background-color: rgba(0, 0, 0, 0.8); } ${PINNED_SELECTOR}:hover ${UNPIN_BTN_SELECTOR} { opacity: .7; } ${UNPIN_BTN_SELECTOR}:hover { opacity: 1 !important; }`); const btnUnpin = document.createElement('button'); btnUnpin.className = UNPIN_BTN_CLASS_VALUE; btnUnpin.title = 'Unpin player'; btnUnpin.textContent = '×'; btnUnpin.addEventListener('click', () => { player.classList.remove(CLASS_VALUE); drag.reset('clear storePos'); window.dispatchEvent(new Event('resize')); }); player.append(btnUnpin); document.addEventListener('yt-navigate-start', () => { if (player.classList.contains(CLASS_VALUE)) { player.classList.remove(CLASS_VALUE); drag.reset(); } }); } const drag = { attrNametoLock: 'force-fix-preventDefault', reset(clear_storePos) { this.dragTarget?.style.removeProperty('transform'); if (clear_storePos) this.storePos = this.xOffset = this.yOffset = 0; else this.storePos = { 'X': this.xOffset, 'Y': this.yOffset }; }, init(el_target = required()) { this.log('drag init', ...arguments); if (!(el_target instanceof HTMLElement)) return console.error('el_target not HTMLElement:', el_target); this.dragTarget = el_target; document.addEventListener('mousedown', evt => { if (!el_target.classList.contains(CLASS_VALUE)) return; this.dragStart.apply(this, [evt]); }); document.addEventListener('mouseup', evt => { if (this.active) this.dragTarget.removeAttribute(this.attrNametoLock); this.dragEnd.apply(this, [evt]); }); document.addEventListener('mousemove', evt => { if (this.active && !this.dragTarget.hasAttribute(this.attrNametoLock)) { this.dragTarget.setAttribute(this.attrNametoLock, true); } this.draging.apply(this, [evt]); }); NOVA.css.push( `[${this.attrNametoLock}]:active { pointer-events: none; }`); }, dragStart(evt) { if (!this.dragTarget.contains(evt.target)) return; this.log('dragStart'); switch (evt.type) { case 'touchstart': this.initialX = evt.touches[0].clientX - (this.xOffset || 0); this.initialY = evt.touches[0].clientY - (this.yOffset || 0); break; case 'mousedown': this.initialX = evt.clientX - (this.xOffset || 0); this.initialY = evt.clientY - (this.yOffset || 0); break; } this.active = true; document.body.style.cursor = 'move'; }, dragEnd(evt) { if (!this.active) return; this.log('dragEnd'); this.initialX = this.currentX; this.initialY = this.currentY; this.active = false; document.body.style.cursor = 'default'; }, draging(evt) { if (!this.active) return; evt.preventDefault(); evt.stopImmediatePropagation(); this.log('draging'); switch (evt.type) { case 'touchmove': this.currentX = evt.touches[0].clientX - this.initialX; this.currentY = evt.touches[0].clientY - this.initialY; break; case 'mousemove': const rect = this.dragTarget.getBoundingClientRect(); if (rect.left >= document.body.clientWidth - this.dragTarget.offsetWidth) { this.currentX = Math.min( evt.clientX - this.initialX, document.body.clientWidth - this.dragTarget.offsetWidth - this.dragTarget.offsetLeft ); } else { this.currentX = Math.max(evt.clientX - this.initialX, 0 - this.dragTarget.offsetLeft); } if (rect.top >= window.innerHeight - this.dragTarget.offsetHeight) { this.currentY = Math.min( evt.clientY - this.initialY, window.innerHeight - this.dragTarget.offsetHeight - this.dragTarget.offsetTop ); } else { this.currentY = Math.max(evt.clientY - this.initialY, 0 - this.dragTarget.offsetTop); } break; } this.xOffset = this.currentX; this.yOffset = this.currentY; this.setTranslate({ 'X': this.currentX, 'Y': this.currentY }); }, setTranslate({ X = required(), Y = required() }) { this.log('setTranslate', ...arguments); this.dragTarget.style.transform = `translate3d(${X}px, ${Y}px, 0)`; }, log() { if (this.DEBUG && arguments.length) { console.groupCollapsed(...arguments); console.trace(); console.groupEnd(); } }, }; }, options: { player_float_scroll_size_ratio: { _tagName: 'input', label: 'Player size', 'label:zh': '播放器尺寸', 'label:ja': 'プレーヤーのサイズ', 'label:ko': '플레이어 크기', 'label:id': 'Ukuran pemain', 'label:es': 'Tamaño del jugador', 'label:pt': 'Tamanho do jogador', 'label:fr': 'Taille du joueur', 'label:it': 'Dimensioni del giocatore', 'label:de': 'Spielergröße', 'label:pl': 'Rozmiar odtwarzacza', 'label:ua': 'Розмір відтворювача', type: 'number', title: 'Less value - larger size', 'title:zh': '较小的值 - 较大的尺寸', 'title:ja': '小さい値-大きいサイズ', 'title:ko': '더 작은 값 - 더 큰 크기', 'title:id': 'Nilai lebih kecil - ukuran lebih besar', 'title:es': 'Valor más pequeño - tamaño más grande', 'title:pt': 'Valor menor - tamanho maior', 'title:fr': 'Plus petite valeur - plus grande taille', 'title:it': 'Meno valore - dimensioni maggiori', 'title:de': 'Kleiner Wert - größere Größe', 'title:pl': 'Mniejsza wartość - większy rozmiar', 'title:ua': 'Менше значення - більший розмір', placeholder: '2-5', step: 0.1, min: 1, max: 5, value: 2.5, }, player_float_scroll_position: { _tagName: 'select', label: 'Player position', 'label:zh': '球员位置', 'label:ja': 'プレイヤーの位置', 'label:ko': '선수 위치', 'label:id': 'Posisi pemain', 'label:es': 'Posición de jugador', 'label:pt': 'Posição do jogador', 'label:fr': 'La position du joueur', 'label:it': 'Posizione del giocatore', 'label:de': 'Spielerposition', 'label:pl': 'Pozycja odtwarzacza', 'label:ua': 'Позиція відтворювача', options: [ { label: 'Top left', value: 'top-left', }, { label: 'Top right', value: 'top-right', selected: true, }, { label: 'Bottom left', value: 'bottom-left', }, { label: 'Bottom right', value: 'bottom-right', }, ], }, player_float_scroll_after_fullscreen_restore_srcoll_pos: { _tagName: 'input', label: 'Restore scrolling back there after exiting fullscreen', type: 'checkbox', }, } }); window.nova_plugins.push({ id: 'video-autopause', title: 'Video autopause', 'title:zh': '视频自动暂停', 'title:ja': 'ビデオの自動一時停止', 'title:ko': '비디오 자동 일시 중지', 'title:id': 'Jeda otomatis video', 'title:es': 'Pausa automática de video', 'title:pt': 'Pausa automática de vídeo', 'title:fr': 'Pause automatique de la vidéo', 'title:it': 'Pausa automatica del video', 'title:de': 'Automatische Pause des Videos', 'title:pl': 'Automatyczne zatrzymanie wideo', 'title:ua': 'Автопауза', run_on_pages: 'watch, embed', restart_on_location_change: true, section: 'player', desc: 'Disable autoplay', 'desc:zh': '禁用自动播放', 'desc:ja': '自動再生を無効にする', 'desc:ko': '자동 재생 비활성화', 'desc:it': 'Nonaktifkan putar otomatis', 'desc:es': 'Deshabilitar reproducción automática', 'desc:pt': 'Desativar reprodução automática', 'desc:fr': 'Désactiver la lecture automatique', 'desc:it': 'Disabilita la riproduzione automatica', 'desc:de': 'Deaktiviere Autoplay', 'desc:pl': 'Wyłącz autoodtwarzanie', 'desc:ua': 'Вимкнути автовідтворення', _runtime: user_settings => { if (user_settings['video-stop-preload'] && !user_settings.stop_preload_embed) return; if (user_settings.video_autopause_embed && NOVA.currentPage != 'embed') return; if (NOVA.currentPage == 'embed' && window.self !== window.top && ['0', 'false'].includes(NOVA.queryURL.get('autoplay')) ) { return; } NOVA.waitSelector('#movie_player video') .then(video => { if (user_settings.video_autopause_ignore_live && movie_player.getVideoData().isLive) return; forceVideoPause.apply(video); }); function forceVideoPause() { if (user_settings.video_autopause_ignore_playlist && location.search.includes('list=')) return; this.pause(); const forceHoldPause = setInterval(() => { this.paused || movie_player.pauseVideo(); this.paused || this.pause(); }, 200); document.addEventListener('keyup', ({ code }) => (code == 'Space') && stopForceHoldPause()); document.addEventListener('click', evt => { if (//movie_player.contains(document.activeElement) || evt.isTrusted && ['button[class*="play-button"]', '.ytp-cued-thumbnail-overlay-image', '.ytp-player-content' ].some(s => evt.srcElement.matches(s)) ) { stopForceHoldPause(); } }); function stopForceHoldPause() { clearInterval(forceHoldPause); movie_player.playVideo(); } } }, options: { video_autopause_ignore_playlist: { _tagName: 'input', label: 'Ignore playlist', 'label:zh': '忽略播放列表', 'label:ja': 'プレイリストを無視する', 'label:ko': '재생목록 무시', 'label:id': 'Abaikan daftar putar', 'label:es': 'Ignorar lista de reproducción', 'label:pt': 'Ignorar lista de reprodução', 'label:fr': 'Ignorer la liste de lecture', 'label:it': 'Ignora playlist', 'label:de': 'Wiedergabeliste ignorieren', 'label:pl': 'Zignoruj listę odtwarzania', 'label:ua': 'Ігнорувати список відтворення', type: 'checkbox', 'data-dependent': { 'video_autopause_embed': false }, }, video_autopause_ignore_live: { _tagName: 'input', label: 'Ignore live', 'label:ua': 'Ігнорувти живі трансляції', type: 'checkbox', 'data-dependent': { 'video_autopause_embed': false }, }, video_autopause_embed: { _tagName: 'select', label: 'Apply to video type', 'label:ua': 'Застосувати до відео', options: [ { label: 'all', value: false, selected: true, 'label:ua': 'всіх', }, { label: 'embed', value: 'on', 'label:ua': 'вбудованих', }, ], }, } }); window.nova_plugins.push({ id: 'volume-wheel', title: 'Volume', 'title:zh': '体积', 'title:ja': '音量', 'title:ko': '용량', 'title:es': 'Volumen', 'title:fr': 'Le volume', 'title:de': 'Volumen', 'title:pl': 'Głośność', 'title:ua': 'Гучність', run_on_pages: 'watch, embed, -mobile', section: 'player', desc: 'With mouse wheel', 'desc:zh': '带鼠标滚轮', 'desc:ja': 'マウスホイール付き', 'desc:ko': '마우스 휠로', 'desc:id': 'Dengan roda mouse', 'desc:es': 'Con rueda de ratón', 'desc:pt': 'Com roda do mouse', 'desc:fr': 'Avec molette de la souris', 'desc:it': 'Con rotellina del mouse', 'desc:de': 'Mit mausrad', 'desc:pl': 'Za pomocą kółka myszy', 'desc:ua': 'З допомогою колеса мишки', _runtime: user_settings => { NOVA.waitSelector('#movie_player video') .then(video => { video.addEventListener('volumechange', function () { NOVA.triggerHUD(Math.round(this.volume * 100) + '%'); playerVolume.buildVolumeSlider(); if (user_settings.volume_mute_unsave) { playerVolume.saveInSession(movie_player.getVolume()); } }); if (user_settings.volume_loudness_normalization) { const { set } = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'volume'); Object.defineProperty(HTMLMediaElement.prototype, 'volume', { enumerable: true, configurable: true, set(new_value) { new_value = movie_player.getVolume() / 100; set.call(this, new_value); } }); } if (user_settings.volume_hotkey == 'keyboard') { document.addEventListener('keydown', evt => { if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return; if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) return; let delta; switch (evt.key) { case user_settings.volume_hotkey_custom_up: delta = 1; break; case user_settings.volume_hotkey_custom_down: delta = -1; break; } if (delta) { const rate = playerVolume.adjust(+user_settings.volume_step * Math.sign(delta)); } }); } else if (user_settings.volume_hotkey) { document.body.querySelector('.html5-video-container') .addEventListener('wheel', evt => { evt.preventDefault(); if (evt[user_settings.volume_hotkey] || (user_settings.volume_hotkey == 'none' && !evt.ctrlKey && !evt.altKey && !evt.shiftKey && !evt.metaKey)) { if (step = +user_settings.volume_step * Math.sign(evt.wheelDelta)) { playerVolume.adjust(step); } } }); } if (+user_settings.volume_level_default) { playerVolume.set(+user_settings.volume_level_default); } if (user_settings['save-channel-state']) { NOVA.runOnPageInitOrTransition(async () => { if ((NOVA.currentPage == 'watch' || NOVA.currentPage == 'embed') && (userVolume = await NOVA.storage_obj_manager.getParam('volume')) ) { video.addEventListener('canplay', () => playerVolume.set(userVolume), { capture: true, once: true }); } }); } }); const playerVolume = { adjust(delta) { const level = movie_player?.getVolume() + +delta; return user_settings.volume_unlimit ? this.unlimit(level) : this.set(level); }, set(level = 50) { if (typeof movie_player === 'undefined' || !movie_player.hasOwnProperty('getVolume')) return console.error('Error getVolume'); const newLevel = Math.max(0, Math.min(100, +level)); if (newLevel !== movie_player.getVolume()) { movie_player.isMuted() && movie_player.unMute(); movie_player.setVolume(newLevel); if (newLevel === movie_player.getVolume()) { this.saveInSession(newLevel); } else { console.error('setVolumeLevel error! Different: %s!=%s', newLevel, movie_player.getVolume()); } } return newLevel === movie_player.getVolume() && newLevel; }, saveInSession(level = required()) { const storageData = { creation: Date.now(), data: { 'volume': +level, 'muted': (level ? 'false' : 'true') }, }; try { localStorage['yt-player-volume'] = JSON.stringify( Object.assign({ expiration: Date.now() + 2592e6 }, storageData) ); sessionStorage['yt-player-volume'] = JSON.stringify(storageData); } catch (err) { console.warn(`${err.name}: save "volume" in sessionStorage failed. It seems that "Block third-party cookies" is enabled`, err.message); } }, unlimit(level = 300) { if (level > 100) { if (!this.audioCtx) { this.audioCtx = new AudioContext(); const source = this.audioCtx.createMediaElementSource(NOVA.videoElement); this.node = this.audioCtx.createGain(); this.node.gain.value = 1; source.connect(this.node); this.node.connect(this.audioCtx.destination); } if (this.node.gain.value < 7) this.node.gain.value += 1; NOVA.triggerHUD(movie_player.getVolume() * this.node.gain.value + '%'); } else { if (this.audioCtx && this.node.gain.value !== 1) { this.node.gain.value = 1; } this.set(level); } }, buildVolumeSlider(timeout_ms = 800) { if (volumeArea = movie_player?.querySelector('.ytp-volume-area')) { if (typeof this.showTimeout === 'number') clearTimeout(this.showTimeout); volumeArea.dispatchEvent(new Event('mouseover', { bubbles: true })); this.showTimeout = setTimeout(() => volumeArea.dispatchEvent(new Event('mouseout', { bubbles: true })) , timeout_ms); insertToHTML({ 'text': Math.round(movie_player.getVolume()), 'container': volumeArea, }); } function insertToHTML({ text = '', container = required() }) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); const SELECTOR_ID = 'nova-volume-text'; (document.getElementById(SELECTOR_ID) || (function () { const SELECTOR = '#' + SELECTOR_ID; NOVA.css.push(` ${SELECTOR} { display: none; text-indent: 2px; font-size: 110%; text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); cursor: default; } ${SELECTOR}:after { content: '%'; } .ytp-volume-control-hover:not([aria-valuenow="0"])+${SELECTOR} { display: block; }`) const el = document.createElement('span'); el.id = SELECTOR_ID; container.insertAdjacentElement('beforeend', el); return el; })()) .textContent = text; container.title = `${text} %`; } } }; }, options: { volume_level_default: { _tagName: 'input', label: 'Default level', 'label:zh': '默认音量', 'label:ja': 'デフォルトのボリューム', 'label:ko': '기본 볼륨', 'label:id': 'Tingkat default', 'label:es': 'Volumen predeterminado', 'label:pt': 'Volume padrão', 'label:fr': 'Volume par défaut', 'label:it': 'Livello predefinito', 'label:de': 'Standardlautstärke', 'label:pl': 'Poziom domyślny', 'label:ua': 'Базовий рівень', type: 'number', title: '0 - auto', placeholder: '%', step: 1, min: 0, max: 100, value: 100, }, volume_step: { _tagName: 'input', label: 'Step', 'label:zh': '步', 'label:ja': 'ステップ', 'label:ko': '단계', 'label:id': 'Melangkah', 'label:es': 'Paso', 'label:pt': 'Degrau', 'label:fr': 'Étape', 'label:it': 'Fare un passo', 'label:de': 'Schritt', 'label:pl': 'Krok', 'label:ua': 'Крок', type: 'number', title: 'in %', placeholder: '%', step: 5, min: 5, max: 30, value: 10, }, volume_hotkey: { _tagName: 'select', label: 'Hotkey', 'label:zh': '热键', 'label:ja': 'ホットキー', 'label:ko': '단축키', 'label:id': 'Tombol pintas', 'label:es': 'Tecla de acceso rápido', 'label:pt': 'Tecla de atalho', 'label:fr': 'Raccourci', 'label:it': 'Tasto di scelta rapida', 'label:de': 'Schnelltaste', 'label:pl': 'Klawisz skrótu', 'label:ua': 'Гаряча клавіша', options: [ { label: 'wheel', value: 'none', selected: true }, { label: 'shift+wheel', value: 'shiftKey' }, { label: 'ctrl+wheel', value: 'ctrlKey' }, { label: 'alt+wheel', value: 'altKey' }, { label: 'keyboard', value: 'keyboard' }, { label: 'disable', value: false }, ], }, volume_hotkey_custom_up: { _tagName: 'select', label: 'Hotkey up', options: [ { label: ']', value: ']', selected: true }, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '[', '+', '-', ',', '.', '/', '<', ';', '\\' ], 'data-dependent': { 'volume_hotkey': ['keyboard'] }, }, volume_hotkey_custom_down: { _tagName: 'select', label: 'Hotkey down', options: [ { label: '[', value: '[', selected: true }, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ']', '+', '-', ',', '.', '/', '<', ';', '\\' ], 'data-dependent': { 'volume_hotkey': ['keyboard'] }, }, volume_unlimit: { _tagName: 'input', label: 'Allow above 100%', 'label:zh': '允许超过 100%', 'label:ja': '100%以上を許可する', 'label:ko': '100% 이상 허용', 'label:id': 'Izinkan di atas 100%', 'label:es': 'Permitir por encima del 100%', 'label:pt': 'Permitir acima de 100%', 'label:fr': 'Autoriser au-dessus de 100 %', 'label:it': 'Consenti oltre il 100%', 'label:de': 'Über 100 % zulassen', 'label:pl': 'Zezwól powyżej 100%', 'label:ua': 'Дозволити більше 100%', type: 'checkbox', }, volume_mute_unsave: { _tagName: 'input', label: 'Not keep muted state', 'label:zh': '不保存静音模式', 'label:ja': 'マナーモードを保存しない', 'label:ko': '무음 모드를 저장하지 않음', 'label:id': 'Jangan simpan mode senyap', 'label:es': 'No guarde el modo silencioso', 'label:pt': 'Não salve o modo silencioso', 'label:fr': 'Ne pas enregistrer le mode silencieux', 'label:it': 'Non salvare la modalità silenziosa', 'label:de': 'Silent-Modus nicht speichern', 'label:pl': 'Nie zachowuj wyciszonego stanu', 'label:ua': 'Не зберігати беззвучний режим', type: 'checkbox', title: 'Only affects new tabs', 'title:zh': '只影响新标签', 'title:ja': '新しいタブにのみ影響します', 'title:ko': '새 탭에만 영향', 'title:id': 'Hanya memengaruhi tab baru', 'title:es': 'Solo afecta a las pestañas nuevas', 'title:pt': 'Afeta apenas novas guias', 'title:fr': "N'affecte que les nouveaux onglets", 'title:it': 'Riguarda solo le nuove schede', 'title:de': 'Wirkt sich nur auf neue Registerkarten aus', 'title:pl': 'Dotyczy tylko nowych kart', 'title:ua': 'Діє лише на нові вкладки', }, volume_loudness_normalization: { _tagName: 'input', label: 'Disable loudness normalization', type: 'checkbox', title: 'Boost volume level', }, } }); window.nova_plugins.push({ id: 'rate-wheel', title: 'Playback speed control', 'title:zh': '播放速度控制', 'title:ja': '再生速度制御', 'title:ko': '재생 속도 제어', 'title:id': 'Kontrol kecepatan pemutaran', 'title:es': 'Controle de velocidade de reprodução', 'title:pt': 'Controle de velocidade de reprodução', 'title:fr': 'Contrôle de la vitesse de lecture', 'title:it': 'Controllo della velocità di riproduzione', 'title:de': 'Steuerung der Wiedergabegeschwindigkeit', 'title:pl': 'Kontrola prędkości odtwarzania', 'title:ua': 'Контроль швидкості відтворення', run_on_pages: 'home, results, feed, channel, watch, embed, -mobile', section: 'player', desc: 'With mouse wheel', 'desc:zh': '带鼠标滚轮', 'desc:ja': 'マウスホイール付き', 'desc:ko': '마우스 휠로', 'desc:id': 'Dengan roda mouse', 'desc:es': 'Con rueda de ratón', 'desc:pt': 'Com roda do mouse', 'desc:fr': 'Avec molette de la souris', 'desc:it': 'Con rotellina del mouse', 'desc:de': 'Mit mausrad', 'desc:pl': 'Za pomocą kółka myszy', 'desc:ua': 'За допомогою колеса мишки', _runtime: user_settings => { if (+user_settings.rate_default !== 1) { reCalcTimeToOverlay(); } NOVA.waitSelector('#movie_player video') .then(video => { const sliderContainer = insertSlider.apply(video); video.addEventListener('ratechange', function () { NOVA.triggerHUD(this.playbackRate + 'x'); if (Object.keys(sliderContainer).length) { sliderContainer.slider.value = this.playbackRate; sliderContainer.slider.title = `Speed (${this.playbackRate})`; sliderContainer.sliderLabel.textContent = `Speed (${this.playbackRate})`; sliderContainer.sliderCheckbox.checked = (this.playbackRate === 1) ? false : true; } }); setDefaultRate.apply(video); video.addEventListener('loadeddata', setDefaultRate); if (Object.keys(sliderContainer).length) { sliderContainer.slider.addEventListener('input', ({ target }) => playerRate.set(target.value)); sliderContainer.slider.addEventListener('change', ({ target }) => playerRate.set(target.value)); sliderContainer.slider.addEventListener('wheel', evt => { evt.preventDefault(); const rate = playerRate.adjust(+user_settings.rate_step * Math.sign(evt.wheelDelta)); }); sliderContainer.sliderCheckbox.addEventListener('change', ({ target }) => { target.checked || playerRate.set(1) }); } NOVA.runOnPageInitOrTransition(async () => { if (NOVA.currentPage == 'watch' || NOVA.currentPage == 'embed') { if (user_settings['save-channel-state']) { if (userRate = await NOVA.storage_obj_manager.getParam('speed')) { playerRate.set(userRate); video.addEventListener('canplay', () => playerRate.set(userRate), { capture: true, once: true }); } } expandAvailableRatesMenu(); } }); }); if (user_settings.rate_hotkey == 'keyboard') { document.addEventListener('keydown', evt => { if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return; if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) return; let delta; switch (evt.key) { case user_settings.rate_hotkey_custom_up: delta = 1; break; case user_settings.rate_hotkey_custom_down: delta = -1; break; } if (delta) { const rate = playerRate.adjust(+user_settings.rate_step * Math.sign(delta)); } }); } else if (user_settings.rate_hotkey) { NOVA.waitSelector('.html5-video-container') .then(container => { container.addEventListener('wheel', evt => { evt.preventDefault(); if (evt[user_settings.rate_hotkey] || (user_settings.rate_hotkey == 'none' && !evt.ctrlKey && !evt.altKey && !evt.shiftKey && !evt.metaKey)) { const rate = playerRate.adjust(+user_settings.rate_step * Math.sign(evt.wheelDelta)); } }); }); } if (+user_settings.rate_default !== 1 && user_settings.rate_default_apply_music) { NOVA.waitSelector('#upload-info #channel-name .badge-style-type-verified-artist') .then(icon => playerRate.set(1)); NOVA.waitSelector('#upload-info #channel-name a[href]', { destroy_if_url_changes: true }) .then(channelName => { if (/(VEVO|Topic|Records|AMV)$/.test(channelName.textContent) || channelName.textContent.toUpperCase().includes('MUSIC') ) { playerRate.set(1); } }); } const playerRate = { testDefault: rate => (+rate % .25) === 0 && +rate <= 2 && +user_settings.rate_default <= 2 && (typeof movie_player !== 'undefined' && movie_player.hasOwnProperty('getPlaybackRate')), async set(level = 1) { this.log('set', ...arguments); if (this.testDefault(level)) { this.log('set:default'); movie_player.setPlaybackRate(+level) && this.saveInSession(level); } else { this.log('set:html5'); if (NOVA.videoElement) { NOVA.videoElement.playbackRate = +level; this.clearInSession(); } } }, adjust(rate_step = required()) { this.log('adjust', ...arguments); return this.testDefault(rate_step) ? this.default(+rate_step) : this.html5(+rate_step); }, default(playback_rate = required()) { this.log('default', ...arguments); const playbackRate = movie_player.getPlaybackRate(); const inRange = step => { const setRateStep = playbackRate + step; return (.1 <= setRateStep && setRateStep <= 2) && +setRateStep.toFixed(2); }; const newRate = inRange(+playback_rate); if (newRate && newRate != playbackRate) { movie_player.setPlaybackRate(newRate); if (newRate === movie_player.getPlaybackRate()) { this.saveInSession(newRate); } else { console.error('playerRate:default different: %s!=%s', newRate, movie_player.getPlaybackRate()); } } this.log('default return', newRate); return newRate === movie_player.getPlaybackRate() && newRate; }, html5(playback_rate = required()) { this.log('html5', ...arguments); if (!NOVA.videoElement) return console.error('playerRate > videoElement empty:', NOVA.videoElement); const playbackRate = NOVA.videoElement.playbackRate; const inRange = step => { const setRateStep = playbackRate + step; return (.1 <= setRateStep && setRateStep <= 3) && +setRateStep.toFixed(2); }; const newRate = inRange(+playback_rate); if (newRate && newRate != playbackRate) { NOVA.videoElement.playbackRate = newRate; if (newRate === NOVA.videoElement.playbackRate) { this.clearInSession(); } else { console.error('playerRate:html5 different: %s!=%s', newRate, NOVA.videoElement.playbackRate); } } this.log('html5 return', newRate); return newRate === NOVA.videoElement.playbackRate && newRate; }, saveInSession(level = required()) { try { sessionStorage['yt-player-playback-rate'] = JSON.stringify({ creation: Date.now(), data: level.toString(), }) this.log('playbackRate save in session:', ...arguments); } catch (err) { console.warn(`${err.name}: save "rate" in sessionStorage failed. It seems that "Block third-party cookies" is enabled`, err.message); } }, clearInSession() { const keyName = 'yt-player-playback-rate'; try { sessionStorage.hasOwnProperty(keyName) && sessionStorage.removeItem(keyName); this.log('playbackRate save in session:', ...arguments); } catch (err) { console.warn(`${err.name}: save "rate" in sessionStorage failed. It seems that "Block third-party cookies" is enabled`, err.message); } }, log() { if (this.DEBUG && arguments.length) { console.groupCollapsed(...arguments); console.trace(); console.groupEnd(); } }, }; function setDefaultRate() { if (+user_settings.rate_default !== 1) { const is_music = NOVA.isMusic(); if (this.playbackRate !== +user_settings.rate_default && (!user_settings.rate_default_apply_music || !is_music) && (!isNaN(this.duration) && this.duration > 25) ) { playerRate.set(user_settings.rate_default); } else if (this.playbackRate !== 1 && is_music) { playerRate.set(1); } } } function insertSlider() { const SELECTOR_ID = 'nova-rate-slider-menu', SELECTOR = '#' + SELECTOR_ID; NOVA.css.push( `${SELECTOR} [type="range"] { vertical-align: text-bottom; margin: '0 5px', } ${SELECTOR} [type="checkbox"] { appearance: none; outline: none; cursor: pointer; } ${SELECTOR} [type="checkbox"]:checked { background: #f00; } ${SELECTOR} [type="checkbox"]:checked:after { left: 20px; background-color: #fff; }`); const slider = document.createElement('input'); slider.className = 'ytp-menuitem-slider'; slider.type = 'range'; slider.min = +user_settings.rate_step; slider.max = Math.max(2, +user_settings.rate_default); slider.step = +user_settings.rate_step; slider.value = this.playbackRate; const sliderIcon = document.createElement('div'); sliderIcon.className = 'ytp-menuitem-icon'; const sliderLabel = document.createElement('div'); sliderLabel.className = 'ytp-menuitem-label'; sliderLabel.textContent = `Speed (${this.playbackRate})`; const sliderCheckbox = document.createElement('input'); sliderCheckbox.className = 'ytp-menuitem-toggle-checkbox'; sliderCheckbox.type = 'checkbox'; sliderCheckbox.title = 'Remember speed'; const out = {}; const right = document.createElement('div'); right.className = 'ytp-menuitem-content'; out.sliderCheckbox = right.appendChild(sliderCheckbox); out.slider = right.appendChild(slider); const speedMenu = document.createElement('div'); speedMenu.className = 'ytp-menuitem'; speedMenu.id = SELECTOR_ID; speedMenu.append(sliderIcon); out.sliderLabel = speedMenu.appendChild(sliderLabel); speedMenu.append(right); document.body.querySelector('.ytp-panel-menu') ?.append(speedMenu); return out; } function expandAvailableRatesMenu() { if (typeof _yt_player !== 'object') { return console.error('expandAvailableRatesMenu > _yt_player is empty', _yt_player); } if (Object.keys(_yt_player).length && (path = NOVA.seachInObjectBy.key({ 'obj': _yt_player, 'keys': 'getAvailablePlaybackRates', })?.path)) { setAvailableRates(_yt_player, 0, path.split('.')); } function setAvailableRates(path, idx, arr) { if (arr.length - 1 == idx) { path[arr[idx]] = () => [.25, .5, .75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 3.25, 3.5, 3.75, 4, 10]; } else { setAvailableRates(path[arr[idx]], idx + 1, arr); } } } function reCalcTimeToOverlay() { const ATTR_MARK = 'nova-thumb-overlay-time-recalc'; document.addEventListener('yt-action', evt => { if ([ 'yt-append-continuation-items-action', 'ytd-update-grid-state-action', 'yt-service-request', 'ytd-rich-item-index-update-action', ] .includes(evt.detail?.actionName) ) { switch (NOVA.currentPage) { case 'home': case 'results': case 'feed': case 'channel': case 'watch': document.body.querySelectorAll(`#thumbnail #overlays #text:not([${ATTR_MARK}])`) .forEach(overlay => { if ((timeLabelEl = overlay.textContent.trim()) //&& !timeLabelEl.startsWith('⚡') ) { overlay.setAttribute(ATTR_MARK, true); const timeSec = NOVA.timeFormatTo.hmsToSec(timeLabelEl); overlay.textContent = //'⚡' + NOVA.timeFormatTo.HMS.digit(timeSec / user_settings.rate_default); } }); break; } } }); } }, options: { rate_default: { _tagName: 'input', label: 'Speed at startup', 'label:zh': '启动速度', 'label:ja': '起動時の速度', 'label:ko': '시작 시 속도', 'label:id': 'Kecepatan saat startup', 'label:es': 'Velocidad al inicio', 'label:pt': 'Velocidade na inicialização', 'label:fr': 'Rapidité au démarrage', 'label:it': "Velocità all'avvio", 'label:de': 'Geschwindigkeit beim Start', 'label:pl': 'Prędkość przy uruchamianiu', 'label:ua': 'Звичайна швидкість', type: 'number', title: '1 - default', step: 0.05, min: 1, value: 1, }, rate_default_apply_music: { _tagName: 'select', label: 'For music genre', title: 'Extended detection - may trigger falsely', 'title:zh': '扩展检测 - 可能会错误触发', 'title:ja': '拡張検出-誤ってトリガーされる可能性があります', 'title:ko': '확장 감지 - 잘못 트리거될 수 있음', 'title:id': 'Deteksi diperpanjang - dapat memicu salah', 'title:pt': 'Detecção estendida - pode disparar falsamente', 'title:fr': 'Détection étendue - peut se déclencher par erreur', 'title:it': 'Rilevamento esteso - potrebbe attivarsi in modo errato', 'title:de': 'Erweiterte Erkennung - kann fälschlicherweise auslösen', 'title:pl': 'Rozszerzona detekcja - może działać błędnie', 'title:ua': 'Розширене виявлення - може спрацювати помилково', options: [ { label: 'skip', value: true, selected: true, 'label:zh': '跳过', 'label:ja': 'スキップ', 'label:ko': '건너 뛰기', 'label:id': 'merindukan', 'label:es': 'saltar', 'label:pt': 'pular', 'label:fr': 'sauter', 'label:it': 'Perdere', 'label:de': 'überspringen', 'label:pl': 'tęsknić', 'label:ua': 'пропустити', }, { label: 'force apply', value: false, 'label:zh': '施力', 'label:ja': '力を加える', 'label:ko': '강제 적용', 'label:id': 'berlaku paksa', 'label:es': 'aplicar fuerza', 'label:pt': 'aplicar força', 'label:fr': 'appliquer la force', 'label:it': 'applicare la forza', 'label:de': 'kraft anwenden', 'label:pl': 'zastosować siłę', 'label:ua': 'примусово активувати', }, ], 'data-dependent': { 'rate_default': '!1' }, }, rate_step: { _tagName: 'input', label: 'Step', 'label:zh': '步', 'label:ja': 'ステップ', 'label:ko': '단계', 'label:id': 'Melangkah', 'label:es': 'Paso', 'label:pt': 'Degrau', 'label:fr': 'Étape', 'label:it': 'Fare un passo', 'label:de': 'Schritt', 'label:pl': 'Krok', 'label:ua': 'Крок', type: 'number', title: '0.25 - default', placeholder: '0.1-1', step: 0.05, min: 0.05, max: 0.5, value: 0.25, }, rate_hotkey: { _tagName: 'select', label: 'Hotkey', 'label:zh': '热键', 'label:ja': 'ホットキー', 'label:ko': '단축키', 'label:id': 'Tombol pintas', 'label:es': 'Tecla de acceso rápido', 'label:pt': 'Tecla de atalho', 'label:fr': 'Raccourci', 'label:it': 'Tasto di scelta rapida', 'label:de': 'Schnelltaste', 'label:pl': 'Klawisz skrótu', 'label:ua': 'Гаряча клавіша', options: [ { label: 'alt+wheel', value: 'altKey', selected: true }, { label: 'shift+wheel', value: 'shiftKey' }, { label: 'ctrl+wheel', value: 'ctrlKey' }, { label: 'wheel', value: 'none' }, { label: 'keyboard', value: 'keyboard' }, { label: 'disable', value: false }, ], }, rate_hotkey_custom_up: { _tagName: 'select', label: 'Hotkey up', options: [ { label: ']', value: ']', selected: true }, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '[', '+', '-', ',', '.', '/', '<', ';', '\\' ], 'data-dependent': { 'rate_hotkey': ['keyboard'] }, }, rate_hotkey_custom_down: { _tagName: 'select', label: 'Hotkey down', options: [ { label: '[', value: '[', selected: true }, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ']', '+', '-', ',', '.', '/', '<', ';', '\\' ], 'data-dependent': { 'rate_hotkey': ['keyboard'] }, }, } }); window.nova_plugins.push({ id: 'pause-background-tab', title: 'Autopause when switching tabs', 'title:zh': '自动暂停除活动选项卡以外的所有选项卡', 'title:ja': 'アクティブなタブを除くすべてのタブを自動一時停止', 'title:ko': '활성 탭을 제외한 모든 탭 자동 일시 중지', 'title:id': 'Jeda otomatis semua tab latar belakang kecuali yang aktif', 'title:es': 'Pausar automáticamente todas las pestañas excepto la activa', 'title:pt': 'Pausar automaticamente todas as guias, exceto a ativa', 'title:fr': "Interrompt la lecture des vidéos dans d'autres onglets", 'title:it': 'Metti automaticamente in pausa tutte le schede in background tranne quella attiva', 'title:de': 'Alle Tabs außer dem aktiven automatisch pausieren', 'title:pl': 'Zatrzymanie kart w tle oprócz aktywnej', 'title:ua': 'Автопауза усіх фонових вкладок окрім активної', run_on_pages: 'watch, embed', section: 'player', desc: 'Autopause all background tabs except the active one', 'desc:ua': 'Автоматично призупинити всі фонові вкладки, крім активної. Підтримує iframe та інші вікна', _runtime: user_settings => { if (location.hostname.includes('youtube-nocookie.com')) { location.hostname = 'youtube.com'; return; } if (typeof window === 'undefined') return; const storeName = 'nova-playing-instanceIDTab', instanceID = String(Math.random()), removeStorage = () => localStorage.removeItem(storeName); NOVA.waitSelector('video') .then(video => { if (user_settings.pause_background_tab_autoplay_onfocus && user_settings.pause_background_tab_autopause_unfocus ) { } else { video.addEventListener('play', checkInstance); video.addEventListener('playing', checkInstance); ['pause', 'ended'].forEach(evt => video.addEventListener(evt, removeStorage)); window.addEventListener('beforeunload', removeStorage); window.addEventListener('storage', store => { if ((!document.hasFocus() || NOVA.currentPage == 'embed') && store.key === storeName && store.storageArea === localStorage && localStorage.hasOwnProperty(storeName) && localStorage.getItem(storeName) !== instanceID && 'PLAYING' == NOVA.getPlayerState() ) { video.pause(); } }); function checkInstance() { if (user_settings.pause_background_tab_autoplay_onfocus !== true && localStorage.hasOwnProperty(storeName) && localStorage.getItem(storeName) !== instanceID ) { video.pause(); } else { localStorage.setItem(storeName, instanceID); } } } if (user_settings.pause_background_tab_autoplay_onfocus) { window.addEventListener('focus', () => { if (!localStorage.hasOwnProperty(storeName) && localStorage.getItem(storeName) !== instanceID && ['UNSTARTED', 'PAUSED'].includes(NOVA.getPlayerState()) ) { video.play(); } }, user_settings.pause_background_tab_autoplay_onfocus == 'force' ? false : { capture: true, once: true }); } if (user_settings.pause_background_tab_autopause_unfocus) { window.addEventListener('blur', () => { if ('PLAYING' == NOVA.getPlayerState()) { video.pause(); } }); } }); }, options: { pause_background_tab_autoplay_onfocus: { _tagName: 'select', label: 'Autoplay on tab focus mode', 'label:zh': '在标签焦点上自动播放', 'label:ja': 'タブフォーカスでの自動再生', 'label:ko': '탭 포커스에서 자동 재생', 'label:id': 'Putar otomatis pada fokus tab', 'label:es': 'Reproducción automática en el enfoque de la pestaña', 'label:pt': 'Reprodução automática no foco da guia', 'label:fr': "Lecture automatique sur le focus de l'onglet", 'label:it': 'Riproduzione automatica su tab focus', 'label:de': 'Autoplay bei Tab-Fokus', 'label:pl': 'Autoodtwarzanie po wybraniu karty', 'label:ua': 'Автовідтворення при виборі вкладки', options: [ { label: 'disable', selected: true, }, { label: 'once for new tab', value: true, }, { label: 'always for not started', value: 'force', }, ], }, pause_background_tab_autopause_unfocus: { _tagName: 'input', label: 'Autopause if tab loses focus', 'label:zh': '如果选项卡失去焦点,则自动暂停视频', 'label:ja': 'タブがフォーカスを失った場合にビデオを自動一時停止', 'label:ko': '탭이 초점을 잃으면 비디오 자동 일시 중지', 'label:id': 'Jeda otomatis video jika tab kehilangan fokus', 'label:es': 'Pausa automática del video si la pestaña pierde el foco', 'label:pt': 'Pausar automaticamente o vídeo se a guia perder o foco', 'label:fr': "Pause automatique de la vidéo si l'onglet perd le focus", 'label:it': 'Metti automaticamente in pausa il video se la scheda perde la messa a fuoco', 'label:de': 'Video automatisch pausieren, wenn der Tab den Fokus verliert', 'label:pl': 'Automatycznie wstrzymaj wideo, jeśli karta straci ostrość', 'label:ua': 'Автопауза при зміні вкладки', type: 'checkbox', }, } }); window.nova_plugins.push({ id: 'sidebar-channel-links-patch', title: 'Fix channel links in sidebar', 'title:zh': '修复侧边栏中的频道链接', 'title:ja': 'サイドバーのチャネルリンクを修正', 'title:ko': '사이드바에서 채널 링크 수정', 'title:id': 'Perbaiki tautan saluran di bilah sisi', 'title:es': 'Arreglar enlaces de canales en la barra lateral', 'title:pt': 'Corrigir links de canais na barra lateral', 'title:fr': 'Correction des liens de chaîne dans la barre latérale', 'title:it': 'Correggi i collegamenti ai canali nella barra laterale', 'title:de': 'Korrigieren Sie die Kanallinks in der Seitenleiste', 'title:pl': 'Napraw linki do kanałów na pasku bocznym', 'title:ua': 'Виправити посилання на канали на бічній панелі', run_on_pages: 'watch, -mobile', section: 'sidebar', _runtime: user_settings => { document.addEventListener('mouseover', ({ target }) => { //console.debug('>', target); if (!target.matches('.ytd-channel-name')) return; if ((link = target.closest('a')) && target.__data?.text?.runs.length && target.__data?.text?.runs[0].navigationEndpoint?.commandMetadata?.webCommandMetadata?.webPageType == 'WEB_PAGE_TYPE_CHANNEL' ) { //const urlOrig = '/watch?v=' + link.data.watchEndpoint.videoId; const urlOrig = link.href; const url = target.__data.text.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url + '/videos'; link.href = url; link.data.commandMetadata.webCommandMetadata.url = url; link.data.commandMetadata.webCommandMetadata.webPageType = 'WEB_PAGE_TYPE_CHANNEL'; link.data.browseEndpoint = target.__data.text.runs[0].navigationEndpoint.browseEndpoint; link.data.browseEndpoint.params = encodeURIComponent(btoa(String.fromCharCode(0x12, 0x06) + 'videos')); //console.debug('patch link:', 1); target.addEventListener('mouseout', ({ target }) => { link.href = urlOrig; link.data.commandMetadata.webCommandMetadata.url = urlOrig; link.data.commandMetadata.webCommandMetadata.webPageType = 'WEB_PAGE_TYPE_WATCH'; //console.debug('restore link:', 2); }, { capture: true, once: true }); } }) }, }); window.nova_plugins.push({ id: 'livechat-visibility', title: 'Collapse live chat', 'title:zh': '隐藏实时聊天', 'title:ja': 'ライブチャットを非表示', 'title:ko': '실시간 채팅 숨기기', 'title:id': 'Sembunyikan obrolan langsung', 'title:es': 'Ocultar chat en vivo', 'title:pt': 'Ocultar livechat', 'title:fr': 'Masquer le chat en direct', 'title:it': 'Nascondi chat dal vivo', 'title:de': 'Livechat ausblenden', 'title:pl': 'Ukryj czat na żywo', 'title:ua': 'Приховати чат', run_on_pages: 'watch, -mobile', restart_on_location_change: true, section: 'sidebar', _runtime: user_settings => { if (user_settings.livechat_visibility_mode == 'disable') { NOVA.waitSelector('#chat', { destroy_if_url_changes: true }) .then(chat => { chat.remove(); }); } else { NOVA.waitSelector('#chat:not([collapsed]) #show-hide-button button', { destroy_if_url_changes: true }) .then(btn => { btn.click(); }); } }, options: { livechat_visibility_mode: { _tagName: 'select', label: 'Mode', 'label:zh': '模式', 'label:ja': 'モード', 'label:ko': '방법', 'label:es': 'Modo', 'label:pt': 'Modo', 'label:it': 'Mode', 'label:de': 'Modus', 'label:pl': 'Tryb', 'label:ua': 'Режим', options: [ { label: 'collapse', value: 'hide', selected: true, 'label:pl': 'zwiń', 'label:ua': 'приховати', }, { label: 'remove', value: 'disable', 'label:zh': '消除', 'label:ja': '削除', 'label:ko': '제거하다', 'label:id': 'menghapus', 'label:es': 'eliminar', 'label:pt': 'remover', 'label:fr': 'retirer', 'label:it': 'rimuovere', 'label:de': 'entfernen', 'label:pl': 'usunąć', 'label:ua': 'видалити', }, ], }, } }); window.nova_plugins.push({ id: 'playlist-collapse', title: 'Collapse playlist', 'title:zh': '播放列表自动折叠', 'title:ja': 'プレイリストの自動折りたたみ', 'title:ko': '재생목록 자동 축소', 'title:id': 'Penciutan otomatis daftar putar', 'title:es': 'Contraer automáticamente la lista de reproducción', 'title:pt': 'Recolhimento automático da lista de reprodução', 'title:fr': 'Réduction automatique de la liste de lecture', 'title:it': 'Comprimi automaticamente la playlist', 'title:de': 'Automatische Minimierung der Wiedergabeliste', 'title:pl': 'Automatyczne zwijanie listy odtwarzania', 'title:ua': 'Автоматичне згортання списку відтворення', run_on_pages: 'watch, -mobile', section: 'sidebar', _runtime: user_settings => { if (!location.search.includes('list=')) return; NOVA.waitSelector('#secondary #playlist:not([collapsed]) #expand-button button') .then(btn => { btn.click(); }); }, options: { } }); window.nova_plugins.push({ id: 'playlist-extended', title: 'Extended playlist length', 'title:ua': 'Розширена довжина списку відтворення', run_on_pages: 'watch, -mobile', section: 'sidebar', _runtime: user_settings => { NOVA.css.push( `ytd-watch-flexy:not([theater]) #secondary #playlist { --ytd-watch-flexy-panel-max-height: 90vh !important; }`); }, }); window.nova_plugins.push({ id: 'playlist-toggle-autoplay', title: 'Add playlist autoplay control button', 'title:zh': '播放列表自动播放控制', 'title:ja': 'プレイリストの自動再生コントロール', 'title:ko': '재생 목록 자동 재생 제어', 'title:id': 'Tombol kontrol putar otomatis daftar putar', 'title:es': 'Control de reproducción automática de listas de reproducción', 'title:pt': 'Controle de reprodução automática da lista de reprodução', 'title:fr': 'Contrôle de lecture automatique de la liste de lecture', 'title:it': 'Pulsante di controllo della riproduzione automatica della playlist', 'title:de': 'Steuerung der automatischen Wiedergabe von Wiedergabelisten', 'title:pl': 'Kontrola autoodtwarzania listy odtwarzania', 'title:ua': 'Кнопка керування автовідтворенням', run_on_pages: 'watch, -mobile', section: 'sidebar', _runtime: user_settings => { const SELECTOR_ID = 'nova-playlist-autoplay-btn', SELECTOR = '#' + SELECTOR_ID; let sesionAutoplayState = user_settings.playlist_autoplay; NOVA.css.push( `#playlist-action-menu .top-level-buttons { align-items: center; } ${SELECTOR}[type=checkbox] { --height: 1em; width: 2.2em; } ${SELECTOR}[type=checkbox]:after { transform: scale(1.5); } ${SELECTOR}[type=checkbox] { --opacity: .7; --color: #fff; height: var(--height); line-height: 1.6em; border-radius: 3em; background-color: var(--paper-toggle-button-unchecked-bar-color, #000000); appearance: none; -webkit-appearance: none; position: relative; cursor: pointer; outline: 0; border: none; } ${SELECTOR}[type=checkbox]:after { position: absolute; top: 0; left: 0; content: ''; width: var(--height); height: var(--height); border-radius: 50%; background-color: var(--color); box-shadow: 0 0 .25em rgba(0, 0, 0, .3); } ${SELECTOR}[type=checkbox]:checked:after { left: calc(100% - var(--height)); --color: var(--paper-toggle-button-checked-button-color, var(--primary-color)); } ${SELECTOR}[type=checkbox]:focus, input[type=checkbox]:focus:after { transition: all 200ms ease-in-out; } ${SELECTOR}[type=checkbox]:disabled { opacity: .3; }`); NOVA.runOnPageInitOrTransition(() => { if (location.search.includes('list=') && NOVA.currentPage == 'watch') { insertButton(); } }); function insertButton() { NOVA.waitSelector('ytd-watch-flexy.ytd-page-manager:not([hidden]) ytd-playlist-panel-renderer:not([collapsed]) #playlist-action-menu .top-level-buttons:not([hidden]), #secondary #playlist #playlist-action-menu #top-level-buttons-computed', { destroy_if_url_changes: true }) .then(el => renderCheckbox(el)); function renderCheckbox(container = required()) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); document.getElementById(SELECTOR_ID)?.remove(); const checkboxBtn = document.createElement('input'); checkboxBtn.id = SELECTOR_ID; checkboxBtn.type = 'checkbox'; checkboxBtn.title = 'Playlist toggle autoplay'; checkboxBtn.addEventListener('change', ({ target }) => { sesionAutoplayState = target.checked; setAssociatedAutoplay(); }); container.append(checkboxBtn); checkboxBtn.checked = sesionAutoplayState; setAssociatedAutoplay(); function setAssociatedAutoplay() { if (manager = document.body.querySelector('yt-playlist-manager')) { manager.interceptedForAutoplay = true; manager.canAutoAdvance_ = checkboxBtn.checked; checkboxBtn.checked = manager?.canAutoAdvance_; checkboxBtn.title = `Playlist Autoplay is ${manager?.canAutoAdvance_ ? 'ON' : 'OFF'}`; if (checkboxBtn.checked) checkHiddenVideo(); } else console.error('Error playlist-autoplay. Playlist manager is', manager); async function checkHiddenVideo() { const ytdWatch = document.body.querySelector('ytd-watch-flexy'); let vids_list; await NOVA.waitUntil(() => { if ((vids_list = ytdWatch?.data?.contents?.twoColumnWatchNextResults?.playlist?.playlist?.contents || ytdWatch?.data?.playlist?.playlist?.contents ) && vids_list.length) return true; }, 1000); const currentIndex = movie_player.getPlaylistIndex(), lastAvailableIdx = vids_list.findIndex(i => i.hasOwnProperty('messageRenderer')) - 1; if (currentIndex === lastAvailableIdx) { manager.canAutoAdvance_ = false; alert('Nova [playlist-toggle-autoplay]:\nPlaylist has hide video. Playlist autoplay disabled'); checkboxBtn.checked = false; } } } } } }, options: { playlist_autoplay: { _tagName: 'select', label: 'Default state', 'label:zh': '默认状态', 'label:ja': 'デフォルト状態', 'label:ko': '기본 상태', 'label:es': 'Estado predeterminado', 'label:pt': 'Estado padrão', 'label:fr': 'État par défaut', 'label:it': 'Stato predefinito', 'label:de': 'Standardzustand', 'label:pl': 'Stan domyślny', 'label:ua': 'Cтан за замовчуваням', options: [ { label: 'play', value: true, selected: true, 'label:ua': 'грати', }, { label: 'stop', value: false, 'label:ua': 'зупинити', }, ], }, } }); window.nova_plugins.push({ id: 'playlist-reverse', title: 'Add playlist reverse order button', 'title:zh': '添加按钮反向播放列表顺序', 'title:ja': 'ボタンの逆プレイリストの順序を追加', 'title:ko': '버튼 역 재생 목록 순서 추가', 'title:id': 'Tambahkan tombol urutan terbalik daftar putar', 'title:es': 'Agregar orden de lista de reproducción inverso', 'title:pt': 'Adicionar ordem inversa da lista de reprodução', 'title:fr': 'Ajouter un ordre de lecture inversé', 'title:it': "Aggiungi il pulsante dell'ordine inverso della playlist", 'title:de': 'Umgekehrte Playlist-Reihenfolge hinzufügen', 'title:pl': 'Dodaj przycisk odtwarzania w odwrotnej kolejności', 'title:ua': 'Кнопка додавання списку відтворення у зворотному порядку', run_on_pages: 'watch, -mobile', section: 'sidebar', _runtime: user_settings => { const SELECTOR_ID = 'nova-playlist-reverse-btn', SELECTOR = '#' + SELECTOR_ID, CLASS_NAME_ACTIVE = 'nova-playlist-reverse-on'; window.nova_playlistReversed; NOVA.css.push( SELECTOR + ` { background: none; border: 0; } yt-icon-button { width: 40px; height: 40px; padding: 10px; } ${SELECTOR} svg { fill: white; fill: var(--yt-spec-text-secondary); } ${SELECTOR}:hover svg { fill: #66afe9; } ${SELECTOR}:active svg, ${SELECTOR}.${CLASS_NAME_ACTIVE} svg { fill: #2196f3; }`); if (user_settings.playlist_reverse_auto_enabled && !window.nova_playlistReversed) { window.nova_playlistReversed = true; } NOVA.runOnPageInitOrTransition(async () => { if (location.search.includes('list=') && NOVA.currentPage == 'watch') { reverseControl(); await NOVA.delay(1000); insertButton(); } }); function insertButton() { NOVA.waitSelector('ytd-watch-flexy.ytd-page-manager:not([hidden]) ytd-playlist-panel-renderer:not([collapsed]) #playlist-action-menu .top-level-buttons:not([hidden]), #secondary #playlist #playlist-action-menu #top-level-buttons-computed', { destroy_if_url_changes: true }) .then(el => createButton(el)); function createButton(container = required()) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); document.getElementById(SELECTOR_ID)?.remove(); const reverseBtn = document.createElement('div'), renderTitle = () => reverseBtn.title = `Reverse playlist order is ${window.nova_playlistReversed ? 'ON' : 'OFF'}`; if (window.nova_playlistReversed) reverseBtn.className = CLASS_NAME_ACTIVE; reverseBtn.id = SELECTOR_ID; renderTitle(); reverseBtn.innerHTML = `<yt-icon-button> <svg viewBox="0 0 381.399 381.399" height="100%" width="100%"> <g> <path d="M233.757,134.901l-63.649-25.147v266.551c0,2.816-2.286,5.094-5.104,5.094h-51.013c-2.82,0-5.099-2.277-5.099-5.094 V109.754l-63.658,25.147c-2.138,0.834-4.564,0.15-5.946-1.669c-1.389-1.839-1.379-4.36,0.028-6.187L135.452,1.991 C136.417,0.736,137.91,0,139.502,0c1.576,0,3.075,0.741,4.041,1.991l96.137,125.061c0.71,0.919,1.061,2.017,1.061,3.109 c0,1.063-0.346,2.158-1.035,3.078C238.333,135.052,235.891,135.735,233.757,134.901z M197.689,378.887h145.456v-33.62H197.689 V378.887z M197.689,314.444h145.456v-33.622H197.689V314.444z M197.689,218.251v33.619h145.456v-33.619H197.689z"/> </g> </svg> </yt-icon-button>`; reverseBtn.addEventListener('click', () => { reverseBtn.classList.toggle(CLASS_NAME_ACTIVE); window.nova_playlistReversed = !window.nova_playlistReversed; if (window.nova_playlistReversed) { reverseControl(); renderTitle(); fixConflictPlugins(); } else location.reload(); }); container.append(reverseBtn); } } function fixConflictPlugins() { document.getElementById('nova-playlist-duration').innerHTML = ' [out of reach] '; if (autoplayBtn = document.getElementById('nova-playlist-autoplay-btn')) { autoplayBtn.disabled = true; autoplayBtn.title = 'out of reach'; } } async function reverseControl() { if (!window.nova_playlistReversed) return; if ((ytdWatch = await NOVA.waitSelector('ytd-watch-flexy', { destroy_if_url_changes: true })) && (data = await NOVA.waitUntil(() => ytdWatch?.data?.contents?.twoColumnWatchNextResults)) && (playlist = data.playlist?.playlist) && (autoplay = data.autoplay?.autoplay) ) { playlist.contents.reverse(); playlist.currentIndex = (playlist.totalVideos - playlist.currentIndex) - 1; playlist.localCurrentIndex = (playlist.contents.length - playlist.localCurrentIndex) - 1; for (const i of autoplay.sets) { i.autoplayVideo = i.previousButtonVideo; i.previousButtonVideo = i.nextButtonVideo; i.nextButtonVideo = i.autoplayVideo; } ytdWatch.updatePageData_(data); if ((manager = document.body.querySelector('yt-playlist-manager')) && (ytdPlayer = document.getElementById('ytd-player')) ) { ytdPlayer.updatePlayerComponents(null, autoplay, null, playlist); manager.autoplayData = autoplay; manager.setPlaylistData(playlist); ytdPlayer.updatePlayerPlaylist_(playlist); } } scrollToElement(document.body.querySelector('#secondary #playlist-items[selected], ytm-playlist .item[selected=true]')); } function scrollToElement(targetEl = required()) { if (!(targetEl instanceof HTMLElement)) return console.error('targetEl not HTMLElement:', targetEl); const container = targetEl.parentElement; container.scrollTop = targetEl.offsetTop - container.offsetTop; } }, options: { playlist_reverse_auto_enabled: { _tagName: 'input', label: 'Default enabled state', type: 'checkbox', }, }, }); window.nova_plugins.push({ id: 'playlist-duration', title: 'Show playlist duration', 'title:zh': '显示播放列表持续时间', 'title:ja': 'プレイリストの期間を表示', 'title:ko': '재생목록 재생시간 표시', 'title:id': 'Tampilkan durasi daftar putar', 'title:es': 'Mostrar duración de la lista de reproducción', 'title:pt': 'Mostrar duração da lista de reprodução', 'title:fr': 'Afficher la durée de la liste de lecture', 'title:it': 'Mostra la durata della playlist', 'title:de': 'Wiedergabelistendauer anzeigen', 'title:pl': 'Pokaż czas trwania playlisty', 'title:ua': 'Показувати тривалість списку відтворення', run_on_pages: 'watch, playlist, -mobile', restart_on_location_change: true, section: 'sidebar', _runtime: user_settings => { const SELECTOR_ID = 'nova-playlist-duration', playlistId = NOVA.queryURL.get('list'); if (!playlistId) return; switch (NOVA.currentPage) { case 'playlist': NOVA.waitSelector('#owner-text a') .then(el => { if (duration = getPlaylistDuration()) { insertToHTML({ 'container': el, 'text': duration }); } else { getPlaylistDurationFromThumbnails('#primary #thumbnail #overlays #text:not(:empty)') .then(duration => insertToHTML({ 'container': el, 'text': duration })); } function getPlaylistDuration() { const vids_list = (document.body.querySelector('ytd-app')?.data?.response || window.ytInitialData) .contents.twoColumnBrowseResultsRenderer ?.tabs[0].tabRenderer?.content?.sectionListRenderer ?.contents[0].itemSectionRenderer ?.contents[0].playlistVideoListRenderer?.contents || document.body.querySelector('ytd-watch-flexy')?.__data.playlistData?.contents || document.body.querySelector('ytd-watch-flexy')?.data?.playlist?.playlist?.contents ; const duration = vids_list?.reduce((acc, vid) => acc + (isNaN(vid.playlistVideoRenderer?.lengthSeconds) ? 0 : parseInt(vid.playlistVideoRenderer.lengthSeconds)), 0); if (duration) { return outFormat(duration); } } }); break; case 'watch': NOVA.waitSelector('#secondary .index-message-wrapper', { destroy_if_url_changes: true }) .then(el => { const waitPlaylist = setInterval(() => { const playlistLength = movie_player.getPlaylist()?.length, playlistList = document.body.querySelector('yt-playlist-manager')?.currentPlaylistData_?.contents .filter(e => e.playlistPanelVideoRenderer?.lengthText?.simpleText) .map(e => NOVA.timeFormatTo.hmsToSec(e.playlistPanelVideoRenderer.lengthText.simpleText)); console.assert(playlistList?.length === playlistLength, 'playlist loading:', playlistList?.length + '/' + playlistLength); if (playlistList?.length === playlistLength) { clearInterval(waitPlaylist); if (duration = getPlaylistDuration(playlistList)) { insertToHTML({ 'container': el, 'text': duration }); } else if (!user_settings.playlist_duration_progress_type) { getPlaylistDurationFromThumbnails('#playlist #playlist-items #unplayableText[hidden]') .then(duration => insertToHTML({ 'container': el, 'text': duration })); } } }, 2000); function getPlaylistDuration(total_list) { const currentIndex = movie_player.getPlaylistIndex(); let elapsedList = [...total_list]; switch (user_settings.playlist_duration_progress_type) { case 'done': elapsedList.splice(currentIndex); break; case 'left': elapsedList.splice(0, currentIndex); break; } const sumArr = arr => arr.reduce((acc, time) => acc + +time, 0); return outFormat( sumArr(elapsedList), user_settings.playlist_duration_percentage ? sumArr(total_list) : false ); } }); break; } function getPlaylistDurationFromThumbnails(items_selector = required()) { if (container && !(container instanceof HTMLElement)) { return console.error('container not HTMLElement:', container); } return new Promise(resolve => { let forcePlaylistRun = false; const waitThumbnails = setInterval(() => { const timeStampList = document.body.querySelectorAll(items_selector), playlistLength = movie_player.getPlaylist()?.length || document.body.querySelector('ytd-player')?.player_?.getPlaylist()?.length || timeStampList.length, duration = getTotalTime(timeStampList); console.assert(timeStampList.length === playlistLength, 'playlist loading:', timeStampList.length + '/' + playlistLength); if (+duration && timeStampList.length && (timeStampList.length === playlistLength || forcePlaylistRun) ) { clearInterval(waitThumbnails); resolve(outFormat(duration)); } else if (!forcePlaylistRun) { setTimeout(() => forcePlaylistRun = true, 1000 * 3); } }, 500); }); function getTotalTime(nodes) { const arr = [...nodes] .map(e => NOVA.timeFormatTo.hmsToSec(e.textContent)) .filter(Number); return arr.length && arr.reduce((acc, time) => acc + +time, 0); } } function outFormat(duration = 0, total) { let outArr = [ NOVA.timeFormatTo.HMS.digit( (NOVA.currentPage == 'watch' && NOVA.videoElement?.playbackRate) ? (duration / NOVA.videoElement.playbackRate) : duration ) ]; if (total) { outArr.push(`(${~~(duration * 100 / total) + '%'})`); if (user_settings.playlist_duration_progress_type) { outArr.push(user_settings.playlist_duration_progress_type); } } return ' - ' + outArr.join(' '); } function insertToHTML({ text = '', container = required() }) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); (container.querySelector(`#${SELECTOR_ID}`) || (function () { const el = document.createElement('span'); el.id = SELECTOR_ID; return container.appendChild(el); })()) .textContent = ' ' + text; } }, options: { playlist_duration_progress_type: { _tagName: 'select', label: 'Mode', 'label:zh': '模式', 'label:ja': 'モード', 'label:ko': '방법', 'label:es': 'Modo', 'label:pt': 'Modo', 'label:it': 'Modalità', 'label:de': 'Modus', 'label:pl': 'Tryb', 'label:ua': 'Режим', label: 'Time display mode', 'title:zh': '时间显示方式', 'title:ja': '時間表示モード', 'title:ko': '시간 표시 모드', 'title:id': 'Mode tampilan waktu', 'title:es': 'Modo de visualización de la hora', 'title:pt': 'Modo de exibição de tempo', 'title:fr': "Mode d'affichage de l'heure", 'title:it': "Modalità di visualizzazione dell'ora", 'title:de': 'Zeitanzeigemodus', 'title:pl': 'Tryb wyświetlania czasu', 'title:ua': 'Режим відображення часу', options: [ { label: 'done', value: 'done', 'label:zh': '结束', 'label:ja': '終わり', 'label:ko': '보았다', 'label:es': 'hecho', 'label:pt': 'feito', 'label:fr': 'regardé', 'label:de': 'fertig', 'label:pl': 'zakończone', 'label:ua': 'завершено', }, { label: 'left', value: 'left', 'label:zh': '剩下', 'label:ja': '残り', 'label:ko': '왼쪽', 'label:es': 'izquierda', 'label:pt': 'deixou', 'label:fr': 'à gauche', 'label:de': 'links', 'label:pl': 'pozostało', 'label:ua': 'залишилось', }, { label: 'total', value: false, selected: true, 'label:zh': '全部的', 'label:ja': '全て', 'label:ko': '총', 'label:fr': 'le total', 'label:de': 'gesamt', 'label:pl': 'w sumie', 'label:ua': 'загалом', }, ], }, playlist_duration_percentage: { _tagName: 'input', label: 'Add %', 'label:zh': '显示百分比', 'label:ja': 'パーセンテージを表示', 'label:ko': '백분율 추가', 'label:id': 'Tambahkan persentase', 'label:es': 'Agregar porcentaje', 'label:pt': 'Adicionar porcentagem', 'label:fr': 'Ajouter un pourcentage', 'label:it': 'Aggiungi percentuale', 'label:de': 'Prozent hinzufügen', 'label:pl': 'Pokaż procenty', 'label:ua': 'Показати %', type: 'checkbox', 'data-dependent': { 'playlist_duration_progress_type': ['done', 'left'] }, }, } }); window.nova_plugins.push({ id: 'related-visibility', title: 'Collapse related section', 'title:zh': '收起相关栏目', 'title:ja': '関連セクションを折りたたむ', 'title:ko': '관련 섹션 축소', 'title:id': 'Ciutkan bagian terkait', 'title:es': 'Ocultar sección relacionada', 'title:pt': 'Recolher seção relacionada', 'title:fr': 'Réduire la section associée', 'title:it': 'Comprimi la sezione relativa', 'title:de': 'Zugehörigen Abschnitt minimieren', 'title:pl': 'Zwiń powiązaną sekcję', 'title:ua': 'Згорнути розділ "пов`язано"', run_on_pages: 'watch, -mobile', section: 'sidebar', _runtime: user_settings => { NOVA.collapseElement({ selector: '#secondary #related', label: 'related', remove: (user_settings.related_visibility_mode == 'disable') ? true : false, }); }, options: { related_visibility_mode: { _tagName: 'select', label: 'Mode', 'label:zh': '模式', 'label:ja': 'モード', 'label:ko': '방법', 'label:es': 'Modo', 'label:pt': 'Modo', 'label:it': 'Mode', 'label:de': 'Modus', 'label:pl': 'Tryb', 'label:ua': 'Режим', options: [ { label: 'collapse', value: 'hide', selected: true, 'label:pl': 'zwiń', 'label:ua': 'приховати', }, { label: 'remove', value: 'disable', 'label:zh': '消除', 'label:ja': '削除', 'label:ko': '제거하다', 'label:id': 'menghapus', 'label:es': 'eliminar', 'label:pt': 'remover', 'label:fr': 'retirer', 'label:it': 'rimuovere', 'label:de': 'entfernen', 'label:pl': 'usunąć', 'label:ua': 'видалити', }, ], }, } }); window.nova_plugins.push({ id: 'move-in-sidebar', title: 'Move to sidebar', run_on_pages: 'watch, -mobile', section: 'sidebar', _runtime: user_settings => { if (location.search.includes('list=')) return; const SELECTOR_CONTAINER = 'ytd-watch-flexy:not([fullscreen])', SELECTOR_BELOW = `${SELECTOR_CONTAINER} #below`, SELECTOR_SECONDARY = `${SELECTOR_CONTAINER} #secondary`; switch (user_settings.move_in_sidebar_target) { case 'info': moveChannelInfo(); break; case 'description': if (user_settings['description-popup']) return; NOVA.waitSelector(`${SELECTOR_BELOW} #description.ytd-watch-metadata`, { destroy_if_url_changes: true }) .then(description => { NOVA.waitSelector(`${SELECTOR_SECONDARY}-inner`, { destroy_if_url_changes: true }) .then(async secondary => { if (document.body.querySelector('#chat:not([collapsed])')) return; secondary.prepend(description); moveChannelInfo(); if (!user_settings['description-popup'] && !user_settings['video-date-format']) { document.body.querySelector(`${SELECTOR_BELOW} ytd-watch-metadata #title`) ?.append(document.body.querySelector(`${SELECTOR_SECONDARY} #info-container`)); } else { document.body.querySelector(`${SELECTOR_SECONDARY} #info-container`)?.remove(); } NOVA.css.push( SELECTOR_SECONDARY + ` #owner { margin: 0; } ${SELECTOR_SECONDARY} #description.ytd-watch-metadata { height: fit-content !important; max-height: 80vh !important; overflow-y: auto; } ${SELECTOR_SECONDARY} #description #collapse { display: none; } #info-container a { display: none; }`); document.body.querySelector(`${SELECTOR_SECONDARY} #description #expand`)?.click(); }); }); moveSidebar(); break; case 'comments': if (user_settings.comments_visibility_mode == 'disable' || user_settings['comments-popup'] ) { return; } NOVA.waitSelector(`${SELECTOR_BELOW} #comments`, { destroy_if_url_changes: true }) .then(comments => { if (document.body.querySelector('#chat:not([collapsed])')) return; document.body.querySelector(`${SELECTOR_SECONDARY}`)?.appendChild(comments); Object.assign(comments.style, { height: '100vh', 'overflow-y': 'auto', }); }); moveSidebar(); break; } function moveSidebar() { NOVA.waitSelector(`${SELECTOR_SECONDARY} #related`, { destroy_if_url_changes: true }) .then(related => { if (document.body.querySelector('#chat:not([collapsed])')) return; document.body.querySelector('#below')?.appendChild(related); }); } function moveChannelInfo() { NOVA.waitSelector(`${SELECTOR_SECONDARY}-inner`, { destroy_if_url_changes: true }) .then(secondary => { NOVA.waitSelector(`${SELECTOR_BELOW} ytd-watch-metadata #owner`, { destroy_if_url_changes: true }) .then(channelInfo => { secondary.prepend(channelInfo); }); }); } }, options: { move_in_sidebar_target: { _tagName: 'select', label: 'Target of movement', options: [ { label: 'info', value: 'info' }, { label: 'info + description', value: 'description', selected: true }, { label: 'comments', value: 'comments' }, ], }, }, }); const Plugins = { run: ({ user_settings, app_ver }) => { if (!window.nova_plugins?.length) return console.error('nova_plugins empty', window.nova_plugins); if (!user_settings) return console.error('user_settings empty', user_settings); NOVA.currentPage = (function () { const pathnameArray = location.pathname.split('/').filter(Boolean); const [page, channelTab] = [pathnameArray[0], pathnameArray.pop()]; NOVA.channelTab = ['featured', 'videos', 'shorts', 'streams', 'playlists', 'community', 'channels', 'about', 'search'].includes(channelTab) ? channelTab : false; return (!page?.includes('live_chat')) && (['channel', 'c', 'user'].includes(page) || page?.startsWith('@') || /[A-Z\d_]/.test(page) || NOVA.channelTab ) ? 'channel' : (page == 'clip') ? 'watch' : page || 'home'; })(); NOVA.isMobile = location.host == 'm.youtube.com'; let logTableArray = [], logTableStatus, logTableTime; window.nova_plugins?.forEach(plugin => { const pagesAllowList = plugin?.run_on_pages?.split(',').map(p => p.trim().toLowerCase()).filter(Boolean); logTableTime = 0; logTableStatus = false; if (!pluginChecker(plugin)) { console.error('Plugin invalid\n', plugin); alert('Plugin invalid: ' + plugin?.id); logTableStatus = 'INVALID'; } else if (plugin.was_init && !plugin.restart_on_location_change) { logTableStatus = 'skiped'; } else if (!user_settings.hasOwnProperty(plugin.id)) { logTableStatus = 'off'; } else if ( ( pagesAllowList?.includes(NOVA.currentPage) || (pagesAllowList?.includes('*') && !pagesAllowList?.includes('-' + NOVA.currentPage)) ) && (!NOVA.isMobile || (NOVA.isMobile && !pagesAllowList?.includes('-mobile'))) ) { try { const startTableTime = performance.now(); plugin.was_init = true; plugin._runtime(user_settings); logTableTime = (performance.now() - startTableTime).toFixed(2); logTableStatus = true; } catch (err) { console.groupEnd('plugins status'); console.error(`[ERROR PLUGIN] ${plugin.id}\n${err.stack}\n\nPlease report the bug: https://github.com/raingart/Nova-YouTube-extension/issues/new?body=` + encodeURIComponent(app_ver + ' | ' + navigator.userAgent)); if (user_settings.report_issues) { _pluginsCaptureException({ 'trace_name': plugin.id, 'err_stack': err.stack, 'app_ver': app_ver, 'confirm_msg': `ERROR in Nova YouTube™\n\nCrash plugin: "${plugin.title || plugin.id}"\nPlease report the bug or disable the plugin\n\nSend the bug raport to developer?`, }); } console.groupCollapsed('plugins status'); logTableStatus = 'ERROR'; } } logTableArray.push({ 'launched': logTableStatus, 'name': plugin?.id, 'time init (ms)': logTableTime, }); }); console.table(logTableArray); console.groupEnd('plugins status'); function pluginChecker(plugin) { const result = plugin?.id && plugin.run_on_pages && 'function' === typeof plugin._runtime; if (!result) { console.error('plugin invalid:\n', { 'id': plugin?.id, 'run_on_pages': plugin?.run_on_pages, '_runtime': 'function' === typeof plugin?._runtime, }); } return result; } }, } console.log('%c /• %s •/', 'color:#0096fa; font-weight:bold;', GM_info.script.name + ' v.' + GM_info.script.version); const configPage = 'https://raingart.github.io/options.html', configStoreName = 'user_settings', user_settings = GM_getValue(configStoreName, null); if (user_settings?.exclude_iframe && (window.frameElement || window.self !== window.top)) { return console.warn(GM_info.script.name + ': processed in the iframe disable'); } const keyRenameTemplate = { 'video_quality_in_music_quality': 'video_quality_for_music', 'volume_normalization': 'volume_loudness_normalization', } for (const oldKey in user_settings) { if (newKey = keyRenameTemplate[oldKey]) { console.log(oldKey, '=>', newKey); delete Object.assign(user_settings, { [newKey]: user_settings[oldKey] })[oldKey]; } GM_setValue(configStoreName, user_settings); } registerMenuCommand(); if (location.hostname === new URL(configPage).hostname) setupConfigPage(); else { if ((window.self !== window.top) && !location.pathname.startsWith('/embed')) { return console.warn('iframe skiped:', location.pathname); } if (!user_settings?.disable_setting_button) insertSettingButton(); if (!user_settings || !Object.keys(user_settings).length) { if (confirm('Active plugins undetected. Open the settings page now?')) GM_openInTab(configPage); user_settings['report_issues'] = 'on'; GM_setValue(configStoreName, user_settings); } else { landerPlugins(); const exportedSettings = Object.assign({}, user_settings); delete exportedSettings['user-api-key']; delete exportedSettings['sponsor_block']; delete exportedSettings['sponsor_block_category']; delete exportedSettings['sponsor_block_url']; delete exportedSettings['thumb_filter_title_blocklist']; delete exportedSettings['search_filter_channel_blocklist']; delete exportedSettings['streamed_disable_channels_exception']; unsafeWindow.window.nova_settings = exportedSettings; } } function setupConfigPage() { document.addEventListener('submit', event => { event.preventDefault(); let obj = {}; for (const [key, value] of new FormData(event.target)) { if (obj.hasOwnProperty(key)) { obj[key] += ',' + value; obj[key] = obj[key].split(','); } else { switch (value) { case 'true': obj[key] = true; break; case 'false': obj[key] = false; break; case 'undefined': delete obj[key]; break; default: obj[key] = value; } }; } console.debug(`update ${configStoreName}:`, obj); GM_setValue(configStoreName, obj); }); window.addEventListener('DOMContentLoaded', () => { localizePage(user_settings?.lang_code); storeData = user_settings; unsafeWindow.window.nova_plugins = window.nova_plugins; }); window.addEventListener('load', () => { document.body?.classList?.remove('preload'); document.body.querySelector('a[href$="issues/new"]') .addEventListener('click', ({ target }) => { target.href += '?body=' + encodeURIComponent(GM_info.script.version + ' | ' + navigator.userAgent); }); }); } function landerPlugins() { processLander(); function processLander() { const plugins_lander = setInterval(() => { const domLoaded = document?.readyState != 'loading'; if (!domLoaded) return console.debug('waiting, page loading..'); clearInterval(plugins_lander); console.groupCollapsed('plugins status'); Plugins.run({ 'user_settings': user_settings, 'app_ver': GM_info.script.version, }); }, 500); } let prevURL = location.href; const isURLChanged = () => prevURL == location.href ? false : prevURL = location.href; if (isMobile = (location.host == 'm.youtube.com')) { window.addEventListener('transitionend', ({ target }) => target.id == 'progress' && isURLChange() && processLander()); } else { document.addEventListener('yt-navigate-start', () => isURLChanged() && processLander()); } } function registerMenuCommand() { GM_registerMenuCommand('Settings', () => GM_openInTab(configPage)); GM_registerMenuCommand('Import settings', () => { if (navigator.userAgent.match(/firefox|fxios/i)) { if (json = JSON.parse(prompt('Enter json file context'))) { GM_setValue(configStoreName, json); alert('Settings imported'); location.reload(); } } else { const f = document.createElement('input'); f.type = 'file'; f.accept = 'application/JSON'; f.style.display = 'none'; f.addEventListener('change', function () { if (f.files.length !== 1) return alert('file empty'); const rdr = new FileReader(); rdr.addEventListener('load', function () { try { GM_setValue(configStoreName, JSON.parse(rdr.result)); alert('Settings imported successfully!'); location.reload(); } catch (err) { alert(`Error parsing settings\n${err.name}: ${err.message}`); } }); rdr.addEventListener('error', error => alert('Error loading file\n' + rdr?.error || error)); rdr.readAsText(f.files[0]); }); document.body.append(f); f.click(); f.remove(); } }); GM_registerMenuCommand('Export settings', () => { let d = document.createElement('a'); d.style.display = 'none'; d.download = 'nova-settings.json'; d.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(user_settings)); document.body.append(d); d.click(); d.remove(); }); } function insertSettingButton() { NOVA.waitSelector('#masthead #end') .then(menu => { const titleMsg = 'Nova Settings', a = document.createElement('a'), SETTING_BTN_ID = 'nova_settings_button'; a.id = SETTING_BTN_ID; a.href = configPage + '?tabs=tab-plugins'; a.target = '_blank'; a.innerHTML = `<yt-icon-button class="style-scope ytd-button-renderer style-default size-default"> <svg viewBox="-4 0 20 16"> <radialGradient id="nova-gradient" gradientUnits="userSpaceOnUse" cx="6" cy="22" r="18.5"> <stop class="nova-gradient-start" offset="0"/> <stop class="nova-gradient-stop" offset="1"/> </radialGradient> <g fill="deepskyblue"> <polygon points="0,16 14,8 0,0"/> </g> </svg> </yt-icon-button>`; a.addEventListener('click', () => { setTimeout(() => document.body.click(), 200); }); a.title = titleMsg; const tooltip = document.createElement('tp-yt-paper-tooltip'); tooltip.className = 'style-scope ytd-topbar-menu-button-renderer'; tooltip.textContent = titleMsg; a.appendChild(tooltip); menu.prepend(a); NOVA.css.push( `#${SETTING_BTN_ID}[tooltip]:hover:after { position: absolute; top: 50px; transform: translateX(-50%); content: attr(tooltip); text-align: center; min-width: 3em; max-width: 21em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 1.8ch 1.2ch; border-radius: 0.6ch; background-color: #616161; box-shadow: 0 1em 2em -0.5em rgb(0 0 0 / 35%); color: #fff; z-index: 1000; } #${SETTING_BTN_ID} { position: relative; opacity: .3; transition: opacity .3s ease-out; } #${SETTING_BTN_ID}:hover { opacity: 1 !important; } #${SETTING_BTN_ID} path, #${SETTING_BTN_ID} polygon { fill: url(#nova-gradient); } #${SETTING_BTN_ID} .nova-gradient-start, #${SETTING_BTN_ID} .nova-gradient-stop { transition: .6s; stop-color: #7a7cbd; } #${SETTING_BTN_ID}:hover .nova-gradient-start { stop-color: #0ff; } #${SETTING_BTN_ID}:hover .nova-gradient-stop { stop-color: #0095ff; }`); }); } function _pluginsCaptureException({ trace_name, err_stack, confirm_msg, app_ver }) { if (confirm(confirm_msg || `Error in ${GM_info.script.name}. Send the bug raport to developer?`)) { openBugReport(); } function openBugReport() { GM_openInTab( 'https://docs.google.com/forms/u/0/d/e/1FAIpQLScfpAvLoqWlD5fO3g-fRmj4aCeJP9ZkdzarWB8ge8oLpE5Cpg/viewform' + '?entry.35504208=' + encodeURIComponent(trace_name) + '&entry.151125768=' + encodeURIComponent(err_stack) + '&entry.744404568=' + encodeURIComponent(location.href) + '&entry.1416921320=' + encodeURIComponent(app_ver + ' | ' + navigator.userAgent + ' [' + window.navigator.language + ']') ); } } window.addEventListener('unhandledrejection', err => { if (user_settings.report_issues && (err.reason?.stack || err.stack)?.includes('Nova')) { console.error('[ERROR PROMISE]\n', err.reason, '\nPlease report the bug: https://github.com/raingart/Nova-YouTube-extension/issues/new?body=' + encodeURIComponent(GM_info.script.version + ' | ' + navigator.userAgent)); _pluginsCaptureException({ 'trace_name': 'unhandledrejection', 'err_stack': err.reason.stack || err.stack, 'app_ver': GM_info.script.version, 'confirm_msg': `Failure when async-call of one "${GM_info.script.name}" plugin.\nDetails in the console\n\nOpen tab to report the bug?`, }); } });