您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Stickies YouTube moderator and owner comments above the comment box.
// ==UserScript== // @name YouTube Special Comment Sticker // @namespace http://tampermonkey.net/ // @version 20230320 // @description Stickies YouTube moderator and owner comments above the comment box. // @author AsobiTaizen // @match https://www.youtube.com/live_chat* // @grant none // ==/UserScript== (() => { let IS_DARK = document.querySelector('html').hasAttribute('dark'); let IS_POPOUT = document.location.href.indexOf('popout') > -1; let CHAT_ID = IS_POPOUT ? document.location.href.split('=')[2] : document.location.href.split('=')[1].slice(8, 120); let canScroll = true; let chat; let chatSettingsButton; let chatSettingsStickyButton; let historiesSelected; let history; let historyButton; let itemScroller; let menu; let menuButton; let notice; let scrollButton; let settings; let settingsButton; let settingsData; let sticky; let stickyItemObjects; let stickyItemObjectsMap; let stickyItems; let stickyItemsContainer; let toggleSticky; let toggleStickyText; let blacklist; let whitelist; let moderator; let owner; let cancel; let save; let showSticky = () => { if (sticky.style.display === 'none') { chat = document.querySelector('yt-live-chat-app'); itemScroller = chat.querySelector('#item-scroller'); sticky.style.display = 'inherit'; chat.setAttribute('style', `max-height: ${chat.offsetHeight - sticky.offsetHeight}px;`); itemScroller.scrollTop = itemScroller.scrollHeight; } }; let scrollSticky = (force) => { if (canScroll || force) { stickyItems.scrollTop = stickyItems.scrollHeight; } }; let isSettingsListMatch = (list, text) => { let settingsList = settingsData[list]; let match; if (settingsList.text.raw) { match = !!new RegExp(settingsList.text.escaped).exec(text); } else if (settingsList.regex.raw) { match = !!new RegExp(settingsList.regex.escaped, settingsList.regex.flags).exec(text); } else { return false; } return list === 'whitelist' ? !match : match; }; let isSpecial = (author) => { let name = author.innerText || author.content.author.name; if (isSettingsListMatch('blacklist', name)) { return false; } else if (isSettingsListMatch('whitelist', name)) { return false; } else if (settingsData.moderator) { return author.classList ? (author.classList.contains('moderator') || author.classList.contains('owner')) : author.content.author.classes.indexOf('moderator') > -1; } else if (settingsData.owner) { return author.classList ? author.classList.contains('owner') : author.content.author.classes.indexOf('owner') > -1; } return true; }; let isUnique = (node) => { return !stickyItemObjectsMap[`${node.querySelector('#timestamp').innerText}-${node.querySelector('#author-name').innerText}-${node.querySelector('#message').innerText}`]; }; let reachedLocalStorageWarningLimit = () => { return getLocalStorageUsage() / 5 >= 0.9; }; let showSavedStickyNotice = (error) => { if (notice.style.display === 'none' && (!notice.touched || (notice.touched && error && notice.type === 'warn')) && (error || reachedLocalStorageWarningLimit())) { let tooltip = notice.querySelector('#tooltip'); let text = notice.querySelector('#text'); let detailsText = notice.querySelector('#details-text'); if (error) { notice.type = 'error'; tooltip.classList.add('error'); text.innerText = 'Sticky history limit reached.'; detailsText.innerText = 'Sticky messages will be tracked but not saved until space is available. Delete sticky histories to free up space and resume saving sticky messages.'; } else { notice.type = 'warn'; tooltip.classList.remove('error'); text.innerText = 'Sticky history limit nearing.'; detailsText.innerText = 'Sticky messages will be tracked but not saved upon reaching the limit. Delete sticky histories to free up space.'; } notice.style.display = 'initial'; notice.querySelector('tp-yt-iron-dropdown').style.display = 'initial'; } }; let saveStickyItem = (stickyItem) => { let stickyItemObject = { authorPhoto: { img: { src: stickyItem.querySelector('#img').getAttribute('src') } }, content: { timestamp: stickyItem.querySelector('.timestamp').innerText, author: { classes: stickyItem.querySelector('.author-name').getAttribute('class').replace('author-name ', ''), name: stickyItem.querySelector('.author-name').innerText }, message: Array.from(stickyItem.querySelector('#message').childNodes).reduce((result, node) => { if (node.nodeName === '#text') { result.push({ type: 'text', value: node.data }); } else if (node.nodeName === 'IMG') { result.push({ type: 'image', value: { alt: node.alt, src: node.src } }); } else if (node.nodeName === 'A') { result.push({ type: 'link', value: { href: node.href, text: node.innerText } }); } return result; }, []) } }; stickyItemObjects.push(stickyItemObject); try { localStorage.setItem(`youtubespecialcommentsticker-sticky-items-${CHAT_ID}`, JSON.stringify(stickyItemObjects)); showSavedStickyNotice(); } catch (e) { showSavedStickyNotice(e); } }; let stickItem = (node) => { let authorElement = node.querySelector('#author-name'); if (authorElement && isSpecial(authorElement) && isUnique(node)) { let stickyItem = document.createElement('div'); let authorPhoto = document.createElement('div'); let content = document.createElement('div'); let timestamp = document.createElement('span'); let author = document.createElement('div'); let authorName = document.createElement('span'); content.id = 'content'; stickyItem.classList.add('sticky-item'); content.classList.add('style-scope', 'yt-live-chat-text-message-renderer'); timestamp.classList.add('timestamp'); author.classList.add('author'); authorName.classList.add('author-name'); authorElement.classList.contains('moderator') ? authorName.classList.add('moderator') : null; authorElement.classList.contains('owner') ? authorName.classList.add('moderator', 'owner') : null; authorPhoto.innerHTML = node.querySelector('#author-photo').outerHTML.replace(/(\<|\/)(yt\-img\-shadow)/g, '$1div'); timestamp.innerText = node.querySelector('#timestamp').innerText; authorName.innerText = authorElement.innerText; author.append(authorName); content.append(timestamp); content.append(author); content.append(node.querySelector('#message').cloneNode(true)); stickyItem.append(authorPhoto.firstChild); stickyItem.append(content); stickyItems.append(stickyItem); showSticky(); scrollSticky(); saveStickyItem(stickyItem); } }; let monitor = () => { let chatItems = document.querySelector('#items.style-scope.yt-live-chat-item-list-renderer'); let observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach(stickItem); }); }); chatItems.querySelectorAll('yt-live-chat-text-message-renderer').forEach(stickItem); observer.observe(chatItems, { childList: true }); }; let setUpCss = () => { let style = document.createElement('style'); document.head.appendChild(style); style.sheet.insertRule('#sticky {position: relative;}'); style.sheet.insertRule('#sticky #menu.sticky-panel #contentWrapper, #sticky #menu.sticky-panel #contentWrapper #items {outline: none;}'); style.sheet.insertRule('#sticky #menu.sticky-panel #items ytd-menu-service-item-renderer {cursor: pointer;}'); style.sheet.insertRule('#sticky #menu.sticky-panel #items ytd-menu-service-item-renderer tp-yt-paper-item:hover, #sticky #menu.sticky-panel #items ytd-menu-service-item-renderer tp-yt-paper-item:focus:before {background-color: var(--yt-spec-10-percent-layer);}'); style.sheet.insertRule('#sticky #overflow, #sticky #history-button {position: absolute; top: 3px; visibility: visible; padding: 0; z-index: 1; height: 25px; width: 25px;}'); style.sheet.insertRule('#sticky #overflow {right: 16px;}'); style.sheet.insertRule('#sticky #history-button {right: 46px;}'); style.sheet.insertRule('#sticky #history ytd-menu-popup-renderer #items {max-height: 526px; width: 384px; padding: 0px 0px 8px 0px; overflow: hidden auto; outline: none;}'); style.sheet.insertRule('#sticky #history ytd-menu-popup-renderer #items #history-data {position: sticky; top: 0; z-index: 1; flex-direction: row; justify-content: space-between; align-items: center; padding-top: 8px; background-color: var(--yt-spec-brand-background-primary); outline: none;}'); style.sheet.insertRule('#sticky #history ytd-menu-popup-renderer #items .history-item-header {display: flex; flex-direction: row;}'); style.sheet.insertRule('#sticky #history ytd-menu-popup-renderer #items .history-item-header tp-yt-paper-item {padding: 0px 0px 0px 16px;}'); style.sheet.insertRule('#sticky #history ytd-menu-popup-renderer #items .history-item-chat {border-top: #e0e0e0 solid 1px;}'); style.sheet.insertRule('#sticky #history ytd-menu-popup-renderer #items .history-item-edit yt-live-chat-text-input-field-renderer {max-width: 66px}'); style.sheet.insertRule('#sticky #history ytd-menu-popup-renderer #items .history-item-edit yt-live-chat-text-input-field-renderer #input {max-height: 18px; overflow-y: hidden;}'); style.sheet.insertRule('#sticky #history ytd-menu-popup-renderer #items tp-yt-paper-button {line-height: initial; padding: 5px 11px 5px 11px;}'); style.sheet.insertRule('#sticky #history ytd-menu-popup-renderer #items tp-yt-paper-button.history-control-button {padding: 0px; min-width: 26px;}'); style.sheet.insertRule('#sticky #history ytd-menu-popup-renderer #items tp-yt-paper-button.history-affirmative-button yt-icon {padding: 1px;}'); style.sheet.insertRule('#sticky .sticky-panel {outline: none; position: absolute; z-index: 1; right: 8px; top: 30px;}'); style.sheet.insertRule('#sticky .sticky-panel #items {padding: 8px 0px;}'); style.sheet.insertRule('#sticky .sticky-panel #items ytd-menu-service-item-renderer {cursor: default;}'); style.sheet.insertRule('#sticky .sticky-panel tp-yt-paper-item.disabled {opacity: 0.5;}'); style.sheet.insertRule('#sticky .sticky-panel tp-yt-paper-item.disabled yt-live-chat-text-input-field-renderer #input {visibility: hidden;}'); style.sheet.insertRule('#sticky .sticky-panel tp-yt-paper-item {padding: 0px 16px;}'); style.sheet.insertRule('#sticky .sticky-panel tp-yt-paper-item:hover, #sticky .sticky-panel tp-yt-paper-item:focus:before {background-color: transparent;}'); style.sheet.insertRule('#sticky .sticky-panel tp-yt-paper-item yt-formatted-string {user-select: none; margin-right: inherit;}'); style.sheet.insertRule('#sticky .sticky-panel tp-yt-paper-item yt-live-chat-text-input-field-renderer {width: 180px; color: var(--yt-live-chat-primary-text-color);}'); style.sheet.insertRule('#sticky .sticky-panel #blacklist yt-formatted-string, #sticky .sticky-panel #whitelist yt-formatted-string {width: 60px;}'); style.sheet.insertRule('#sticky .sticky-panel #types, #sticky .sticky-panel #controls {align-items: flex-end;}'); style.sheet.insertRule('#sticky .sticky-panel #controls tp-yt-paper-button {line-height: initial; padding: 5px 11px 5px 11px;}'); style.sheet.insertRule('#sticky .sticky-panel #controls #cancel tp-yt-paper-button {padding: 4px 11px 4px 11px;}'); style.sheet.insertRule('#sticky .sticky-panel #toggle {display: flex; flex-direction: row; align-items: center; cursor: pointer;}'); style.sheet.insertRule('#sticky .sticky-panel #toggle[checked]:not([disabled]) .toggle-bar.paper-toggle-button {opacity: 0.5; background-color: var(--paper-toggle-button-checked-bar-color, var(--primary-color));}'); style.sheet.insertRule('#sticky .sticky-panel #toggle[checked]:not([disabled]) .toggle-button.paper-toggle-button {background-color: var(--paper-toggle-button-checked-button-color, var(--primary-color));}'); style.sheet.insertRule('#sticky .sticky-panel #toggle[checked] .toggle-button.paper-toggle-button {transform: translate(16px, 0);}'); style.sheet.insertRule('#sticky .sticky-panel #toggle .toggle-container.paper-toggle-button {display: inline-block; position: relative; width: 36px; height: 14px; margin: 4px 1px;}'); style.sheet.insertRule('#sticky .sticky-panel #toggle .toggle-bar.paper-toggle-button {position: absolute; height: 100%; width: 100%; border-radius: 8px; pointer-events: none; opacity: 0.4; transition: background-color linear .08s; background-color: var(--paper-toggle-button-unchecked-bar-color, #000000);}'); style.sheet.insertRule('#sticky .sticky-panel #toggle .toggle-button.paper-toggle-button {position: absolute; top: -3px; left: 0; right: auto; height: 20px; width: 20px; border-radius: 50%; box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.6); transition: transform linear .08s, background-color linear .08s; will-change: transform; background-color: var(--paper-toggle-button-unchecked-button-color, var(--paper-grey-50));}'); style.sheet.insertRule('#sticky #settings.sticky-panel tp-yt-paper-item > *:not(:first-child):not(:nth-child(2)) {margin-left: 5px;}'); style.sheet.insertRule('#sticky #show-hide-button {border-bottom: #e0e0e0 solid 1px; font-size: 11px;}'); style.sheet.insertRule('#sticky #show-hide-button div {font-size: 11px;}'); style.sheet.insertRule('#sticky #show-hide-button div a {color: #11111199; display: flex;}'); style.sheet.insertRule('#sticky #show-hide-button div a:hover {color: #030303;}'); style.sheet.insertRule('#sticky #show-hide-button div a #button {border-radius: 0; margin: 0; padding: 8px 24px; width: 100%;}'); style.sheet.insertRule('#sticky #show-hide-button div a #button #text {font-weight: 500;}'); style.sheet.insertRule('#sticky #chat-header {box-sizing: border-box; display: flex; border-bottom: #e0e0e0 solid 1px; background-color: #fffffffa; height: 150px; padding: 0 0 0 4px; transition: all .15s cubic-bezier(0.0, 0.0, 0.2, 1);}'); style.sheet.insertRule('#sticky yt-icon-button {z-index: 0; transition: all .15s cubic-bezier(0.0, 0.0, 0.2, 1); visibility: hidden;}'); style.sheet.insertRule('#sticky #primary-content {height: inherit; overflow-x: hidden;}'); style.sheet.insertRule('#sticky #primary-content::-webkit-scrollbar, #sticky #history #items::-webkit-scrollbar {content: "";}'); style.sheet.insertRule('#sticky #primary-content::-webkit-scrollbar-track, #sticky #history #items::-webkit-scrollbar-track {background-color: #f1f1f1;}'); style.sheet.insertRule('#sticky #primary-content::-webkit-scrollbar-thumb, #sticky #history #items::-webkit-scrollbar-thumb {border: 2px solid #f1f1f1; min-height: 30px;}'); style.sheet.insertRule('#sticky #primary-content .sticky-item {padding: 4px; font-size: 13px; display: flex;}'); style.sheet.insertRule('#sticky #primary-content .sticky-item #author-photo {display: inline-table; align-self: center;}'); style.sheet.insertRule('#sticky #primary-content .sticky-item #img {max-width: inherit;}'); style.sheet.insertRule('#sticky #primary-content .timestamp {display: inline; color: #11111166; font-size: 11px; margin-right: 8px;}'); style.sheet.insertRule('#sticky #primary-content .author {display: inline-flex; margin-right: 8px;}'); style.sheet.insertRule('#sticky #primary-content .author .author-name {color: #11111199; font-weight: 500;}'); style.sheet.insertRule('#sticky #primary-content .author .author-name.moderator {color: #5f84f1;}'); style.sheet.insertRule('#sticky #primary-content .author .author-name.moderator.owner {color: #000000de; background-color: #ffd600; padding: 2px 4px; border-radius: 2px;}'); style.sheet.insertRule('#sticky #primary-content #message {display: inline; line-height: 17px;}'); style.sheet.insertRule('#sticky #notice yt-tooltip-renderer.error {color: #ffffff; background-color: var(--yt-spec-brand-button-background);}'); style.sheet.insertRule('#sticky #notice yt-tooltip-renderer.error::before {border-color: transparent transparent var(--yt-spec-brand-button-background) transparent;}'); style.sheet.insertRule('#sticky.dark #history ytd-menu-popup-renderer #items .history-item-chat {border-color: #303030;}'); style.sheet.insertRule('#sticky.dark #show-hide-button {border-color: #303030; background-color: #181818;}'); style.sheet.insertRule('#sticky.dark #show-hide-button div a {color: #ffffffb3;}'); style.sheet.insertRule('#sticky.dark #show-hide-button div a:hover {color: #ffffff;}'); style.sheet.insertRule('#sticky.dark #chat-header {border-color: #303030; background-color: #202020;}'); style.sheet.insertRule('#sticky.dark #primary-content::-webkit-scrollbar-track, #sticky.dark #history #items::-webkit-scrollbar-track {background-color: #0f0f0f;}'); style.sheet.insertRule('#sticky.dark #primary-content::-webkit-scrollbar-thumb, #sticky.dark #history #items::-webkit-scrollbar-thumb {background-color: hsla(0, 0%, 53.3%, .2); border-color: #0f0f0f;'); style.sheet.insertRule('#sticky.dark #primary-content .timestamp {color: #ffffff8a;}'); style.sheet.insertRule('#sticky.dark #primary-content .author .author-name {color: #ffffffb3;}'); style.sheet.insertRule('#sticky.dark #primary-content .author .author-name.moderator {color: #5e84f1;}'); style.sheet.insertRule('#sticky.dark #primary-content .author .author-name.moderator.owner {color: #111111;}'); style.sheet.insertRule('#sticky.hide-timestamps #primary-content .timestamp {display: none;}'); }; let setUpMenu = () => { menu.querySelector('#contentWrapper').innerHTML = '<ytd-menu-popup-renderer slot="dropdown-content" class="style-scope yt-live-chat-app" tabindex="0" use-icons="" style="outline: none; box-sizing: border-box; max-width: 212.063px; max-height: 88px;"><tp-yt-paper-listbox id="items" class="style-scope ytd-menu-popup-renderer" role="listbox" tabindex="0"></tp-yt-paper-listbox></ytd-menu-popup-renderer>'; let items = menu.querySelector('#items'); items.innerHTML = `<ytd-menu-service-item-renderer id="menu-settings" class="style-scope ytd-menu-popup-renderer" role="menuitem" use-icons="" tabindex="0" aria-selected="false" data='{"text": {"runs": [{"text":"Settings"}]}, "icon": {"iconType": "TUNE"}, "serviceEndpoint": {}}'></ytd-menu-service-item-renderer>`; items.innerHTML += `<ytd-menu-service-item-renderer id="menu-history" class="style-scope ytd-menu-popup-renderer" role="menuitem" use-icons="" tabindex="0" aria-selected="false" data='{"text": {"runs": [{"text":"History"}]}, "icon": {"iconType": "CLARIFY"}, "serviceEndpoint": {}}'></ytd-menu-service-item-renderer>`; settingsButton = sticky.querySelector('#menu-settings'); historyButton = sticky.querySelector('#menu-history'); }; let resetHistoryMenu = () => { history.querySelectorAll('ytd-menu-service-item-renderer').forEach(historyItem => historyItem.remove()); }; let selectAllHistories = () => { let selector = history.querySelector('#history-select-all').hasAttribute('checked') ? ':not([checked])' : '[checked]'; history.querySelectorAll(`.history-item-checkbox${selector}`).forEach((checkbox) => checkbox.click()); }; let toggleDeleteHistoriesModal = (event) => { let historyDeleteModal = history.querySelector('#history-delete-confirm'); let historyItems = history.querySelectorAll('.history-item'); let historySelectAll = history.querySelector('#history-select-all'); let historyDeleteButton = history.querySelector('#history-delete-button'); let historyExportButton = history.querySelector('#history-export-button'); let historyImport = history.querySelector('#history-import'); let historyStorageUsage = history.querySelector('#history-storage-usage'); let visible = Number(historyDeleteModal.style.opacity); historyDeleteModal.style.opacity = visible ? 0 : 1; historyDeleteModal.style.zIndex = visible ? -1 : 1; historyDeleteModal.style.pointerEvents = visible ? 'none' : 'initial'; historySelectAll.style.display = visible ? 'initial' : 'none'; historyDeleteButton.style.opacity = event && event.currentTarget.id === 'cancel' ? 1 : 0; historyDeleteButton.style.position = event && event.currentTarget.id === 'cancel' ? 'initial' : 'absolute'; historyDeleteButton.style.pointerEvents = event && event.currentTarget.id === 'cancel' ? 'initial' : 'none'; historyExportButton.style.opacity = event && event.currentTarget.id === 'cancel' ? 1 : 0; historyExportButton.style.position = event && event.currentTarget.id === 'cancel' ? 'initial' : 'absolute'; historyExportButton.style.pointerEvents = event && event.currentTarget.id === 'cancel' ? 'initial' : 'none'; historyImport.style.opacity = !event ? 1 : 0; historyImport.style.pointerEvents = !event ? 'inherit' : 'none'; historyImport.style.position = !event ? 'relative' : 'absolute'; historyStorageUsage.style.opacity = visible ? 1 : 0; historyItems.forEach(historyItem => historyItem.style.display = visible ? 'inherit' : 'none'); }; let exportHistoryItems = () => { let savedStickyIndex = getSavedStickyIndex(); let historyItemsToExport = Object.keys(historiesSelected).map((key) => { let savedStickyIndexDetails = savedStickyIndex[key] || { author: 'Unknown', icon: 'https://yt3.ggpht.com/a/default-user=s32-c-k-c0x00ffffff-no-rj', id: null, title: 'Click here or visit the associated video with chat enabled to update history information.', valid: false }; return { ...savedStickyIndexDetails, items: { id: key, messages: getHistory(key) } }; }); navigator.clipboard.writeText(JSON.stringify(historyItemsToExport, null, 2)).then(() => { let historyExportButtonIcon = history.querySelector('#history-export-button yt-icon'); historyExportButtonIcon.setAttribute('icon', 'check'); setTimeout(() => historyExportButtonIcon.setAttribute('icon', 'offline_download'), 1000); }); }; let handleNoHistories = () => { let histories = getHistories(); if (!histories.length) { history.querySelector('#history-data').style.flexDirection = 'column-reverse'; history.querySelector('#history-delete-controls').style.display = 'none'; history.querySelector('#history-import').style.marginLeft = 'initial'; history.querySelector('#history-import #input').style.width = ''; history.querySelector('#history-data-container').style.display = 'none'; history.querySelector('#history-delete-confirm').style.pointerEvents = 'none'; history.querySelector('#history-none').style.display = 'flex'; } }; let handleHistoryNotice = () => { if (notice.touched && !reachedLocalStorageWarningLimit()) { notice.touched = false; } }; let deleteHistories = () => { Object.keys(historiesSelected).forEach((key) => { localStorage.removeItem(`youtubespecialcommentsticker-sticky-items-${key}`); history.querySelector(`#${key}`).remove(); delete historiesSelected[key]; }); history.querySelector('#history-storage-usage').innerText = getLocalStorageUsageText(); toggleDeleteHistoriesModal(); handleNoHistories(); handleHistoryNotice(); }; let isValidHistoryItemImport = (historyItem) => { return ['author', 'icon', 'id', 'title', 'valid', 'items'].every((property) => property in historyItem); }; let reportInvalidHistoryItemImport = () => { let historyImportButtonIcon = history.querySelector('#history-import yt-icon'); let historyImportInput = history.querySelector('#history-import #input'); historyImportButtonIcon.setAttribute('icon', 'close'); setTimeout(() => historyImportButtonIcon.setAttribute('icon', 'upload'), 1000); if (Number(historyImportInput.style.opacity)) { hideShowHistoryImportControls(); } history.removeAttribute('importing'); }; let importHistoryItem = (savedStickyIndex, historyItem) => { if (isValidHistoryItemImport(historyItem)) { let historyItemsId = `youtubespecialcommentsticker-sticky-items-${historyItem.items.id}`; savedStickyIndex[historyItem.items.id] = { author: historyItem.author, icon: historyItem.icon, id: historyItem.id, title: historyItem.title, valid: historyItem.valid }; localStorage.setItem('youtubespecialcommentsticker-sticky-index', JSON.stringify(savedStickyIndex)); if (!localStorage.getItem(historyItemsId)) { localStorage.setItem(historyItemsId, JSON.stringify(historyItem.items.messages)); } } else { reportInvalidHistoryItemImport(); } }; let importHistoryItems = (text) => { try { let historyItems = JSON.parse(text); let savedStickyIndex = getSavedStickyIndex(); if (Array.isArray(historyItems)) { historyItems.forEach(historyItem => importHistoryItem(savedStickyIndex, historyItem)); } else { importHistoryItem(savedStickyIndex, historyItems); } refreshHistory(); history.removeAttribute('importing'); history.focus(); } catch (error) { reportInvalidHistoryItemImport(); } }; let hideShowHistoryImportControls = () => { let historyImportButton = history.querySelector('#history-import #history-import-button'); let historyImportInput = history.querySelector('#history-import yt-live-chat-text-input-field-renderer'); let historyImportInputButton = history.querySelector('#history-import #cancel'); let historyOtherHistoryHeaderItems = history.querySelectorAll('#history-data tp-yt-paper-item:not(#history-import):not(#history-none)'); let historyHistoryItems = history.querySelectorAll('.history-item'); let visible = historyImportButton.style.opacity === '' || Number(historyImportButton.style.opacity); historyImportButton.style.opacity = visible ? 0 : 1; historyImportButton.style.pointerEvents = visible ? 'none' : 'inherit'; historyImportButton.style.position = visible ? 'absolute' : 'initial'; historyImportInput.style.opacity = visible ? 1 : 0; historyImportInput.style.pointerEvents = visible ? 'inherit' : 'none'; historyImportInput.style.position = visible ? 'inherit' : 'absolute'; historyImportInputButton.style.opacity = visible ? 1 : 0; historyImportInputButton.style.pointerEvents = visible ? 'inherit' : 'none'; historyImportInputButton.style.position = visible ? 'inherit' : 'absolute'; if (visible) { historyImportInput.removeAttribute('has-text'); historyImportInput.querySelector('#input').innerText = ''; } [...historyOtherHistoryHeaderItems, ...historyHistoryItems].forEach((element) => { element.style.opacity = visible ? 0.5 : 1; element.style.pointerEvents = visible ? 'none' : 'initial'; }); }; let handleHistoryItemsImport = () => { history.setAttribute('importing', ''); if (navigator.clipboard.readText) { navigator.clipboard.readText().then(importHistoryItems, reportInvalidHistoryItemImport); } else { hideShowHistoryImportControls(); } }; let handleHistoryImportInputPaste = (event) => { handlePaste(event.currentTarget); importHistoryItems(event.currentTarget.innerText); }; let handleHistoryImportInputKeyUp = (event) => { if (event.currentTarget.innerText) { event.currentTarget.parentElement.setAttribute('has-text', ''); } else { event.currentTarget.parentElement.removeAttribute('has-text'); } }; let handleHistoryImportInputButton = () => { hideShowHistoryImportControls(); history.removeAttribute('importing'); }; let getObjectSize = (data) => { return new Blob(data).size / 1024 / 1024; }; let getLocalStorageUsage = () => { return getObjectSize(Object.values(localStorage)); }; let getLocalStorageUsageText = () => { return `${(getLocalStorageUsage()).toFixed(2)} MB / 5 MB`; }; let setUpHistoryHeader = () => { let histories = getHistories(); let items = history.querySelector('#items'); let itemsTemp = document.createElement('div'); items.parentNode.insertBefore(itemsTemp, items); itemsTemp.innerHTML = `<ytd-menu-service-item-renderer id="history-data" class="style-scope ytd-menu-popup-renderer" role="menuitem" tabindex="-1" aria-selected="false" data='{"serviceEndpoint": {}}'></ytd-menu-service-item-renderer>`; let historyItem = itemsTemp.querySelector('#history-data'); historyItem.querySelector('tp-yt-paper-item').remove(); let historyDeleteControls = document.createElement('tp-yt-paper-item'); historyItem.appendChild(historyDeleteControls); historyDeleteControls.id = 'history-delete-controls'; historyDeleteControls.innerHTML = '<ytd-button-renderer id="history-delete-button" class="style-scope ytd-video-secondary-info-renderer style-destructive size-default" style="opacity: 0; pointer-events: none; position: absolute; margin-right: 16px;" button-renderer="" use-keyboard-focused="" is-paper-button-with-icon="" is-paper-button=""></ytd-button-renderer>'; let historyDeleteButton = historyDeleteControls.querySelector('#history-delete-button'); historyDeleteButton.innerHTML = '<tp-yt-paper-button id="button" class="style-scope ytd-button-renderer style-suggestive size-small history-control-button history-affirmative-button" role="button" tabindex="0" animated="" elevation="0" aria-disabled="false" aria-label="Sign in" title="Delete"><yt-icon icon="delete" class="style-scope yt-live-chat-header-renderer"></yt-icon></tp-yt-paper-button>'; historyDeleteButton.onclick = toggleDeleteHistoriesModal; let historyExportButton = historyDeleteButton.cloneNode(true); historyDeleteControls.appendChild(historyExportButton); historyExportButton.id = 'history-export-button'; historyExportButton.style.margin = 0; historyExportButton.innerHTML = '<tp-yt-paper-button id="button" class="style-scope ytd-button-renderer style-suggestive size-small history-control-button history-affirmative-button" role="button" tabindex="0" animated="" elevation="0" aria-disabled="false" aria-label="Sign in" title="Export"><yt-icon icon="offline_download" class="style-scope yt-live-chat-header-renderer"></yt-icon></tp-yt-paper-button>'; historyExportButton.onclick = exportHistoryItems; let historySelectAll = document.createElement('tp-yt-paper-checkbox'); historyDeleteControls.insertBefore(historySelectAll, historyDeleteButton); historySelectAll.id = 'history-select-all'; historySelectAll.style.setProperty('--paper-checkbox-ink-size', '32px'); historySelectAll.style.paddingRight = '16px'; historySelectAll.onclick = selectAllHistories; let historySelectAllLabel = historySelectAll.querySelector('#checkboxLabel'); historySelectAllLabel.style.setProperty('font-size', 'var(--ytd-user-comment_-_font-size)'); historySelectAllLabel.style.userSelect = 'none'; historySelectAllLabel.innerText = 'Select All'; let historyImportContainer = document.createElement('tp-yt-paper-item'); historyItem.appendChild(historyImportContainer); historyImportContainer.id = 'history-import'; historyImportContainer.style.padding = 0; historyImportContainer.style.marginLeft = 'auto'; historyImportContainer.innerHTML = '<ytd-button-renderer id="history-import-button" class="style-scope ytd-video-secondary-info-renderer style-destructive size-default" button-renderer="" use-keyboard-focused="" is-paper-button-with-icon="" is-paper-button=""></ytd-button-renderer>'; historyImportContainer.innerHTML += `<yt-live-chat-text-input-field-renderer id="input" class="style-scope yt-live-chat-message-input-renderer" disabled="" emoji-manager='{"emojiShortcutMap": {}}' title="Paste history data." style="width: 85px;"></yt-live-chat-text-input-field-renderer>`; historyImportContainer.innerHTML += '<ytd-button-renderer id="cancel" class="style-scope ytd-masthead style-suggestive size-small" button-renderer="" use-keyboard-focused="" is-paper-button-with-icon="" is-paper-button="" style="padding-left: 4px;"></ytd-button-renderer>'; let historyImportButton = historyImportContainer.querySelector('#history-import-button'); historyImportButton.innerHTML = '<tp-yt-paper-button id="button" class="style-scope ytd-button-renderer style-suggestive size-small history-control-button history-affirmative-button" role="button" tabindex="0" animated="" elevation="0" aria-disabled="false" aria-label="Sign in" title="Import"><yt-icon icon="upload" class="style-scope yt-live-chat-header-renderer"></yt-icon></tp-yt-paper-button>'; historyImportButton.onclick = handleHistoryItemsImport; let historyImportInput = historyImportContainer.querySelector('yt-live-chat-text-input-field-renderer'); historyImportInput.style.opacity = 0; historyImportInput.style.pointerEvents = 'none'; historyImportInput.style.position = 'absolute'; historyImportInput.style.userSelect = 'none'; let historyImportInputLabel = historyImportInput.querySelector('#label'); historyImportInputLabel.style.overflowX = 'hidden'; historyImportInputLabel.style.textOverflow = 'ellipsis'; historyImportInputLabel.style.width = 'inherit'; historyImportInputLabel.innerText = historyImportInput.title; let historyImportInputInput = historyImportInput.querySelector('#input'); historyImportInputInput.style.maxHeight = '24px'; historyImportInputInput.onpaste = handleHistoryImportInputPaste; historyImportInputInput.onkeyup = handleHistoryImportInputKeyUp; let historyImportInputButton = historyImportContainer.querySelector('#cancel'); historyImportInputButton.style.opacity = 0; historyImportInputButton.style.pointerEvents = 'none'; historyImportInputButton.style.position = 'absolute'; historyImportInputButton.innerHTML = '<tp-yt-paper-button id="button" class="style-scope ytd-button-renderer style-suggestive size-small history-control-button" role="button" tabindex="0" animated="" elevation="0" aria-disabled="false" aria-label="Sign in" title="Cancel"><yt-icon icon="close" class="style-scope yt-live-chat-header-renderer"></yt-icon></tp-yt-paper-button>'; historyImportInputButton.onclick = handleHistoryImportInputButton; let historyData = document.createElement('tp-yt-paper-item'); historyItem.appendChild(historyData); historyData.id = 'history-data-container'; historyData.innerHTML = '<yt-formatted-string id="history-storage-usage" class="style-scope ytd-menu-service-item-renderer" title="Local Storage Space Used/Capacity"></yt-formatted-string>'; historyData.querySelector('yt-formatted-string').innerText = getLocalStorageUsageText(); historyData.querySelector('yt-formatted-string').removeAttribute('is-empty'); historyItem.appendChild(historyData); historyItem.remove(); items.appendChild(historyItem); itemsTemp.innerHTML = `<ytd-menu-service-item-renderer id="history-delete-confirm" class="style-scope ytd-menu-popup-renderer" role="menuitem" tabindex="-1" aria-selected="false" data='{"serviceEndpoint": {}}'></ytd-menu-service-item-renderer>`; let historyDeleteModal = itemsTemp.querySelector('#history-delete-confirm'); historyDeleteModal.style.opacity = 0; historyDeleteModal.style.position = 'absolute'; historyDeleteModal.style.zIndex = -1; historyDeleteModal.style.width = '384px'; historyDeleteModal.style.pointerEvents = 'none'; let historyDeleteModalContainer = historyDeleteModal.querySelector('tp-yt-paper-item'); historyDeleteModalContainer.querySelector('yt-icon').remove() historyDeleteModalContainer.querySelector('yt-formatted-string').remove() historyDeleteModalContainer.style.alignItems = 'center'; historyDeleteModalContainer.style.justifyContent = 'space-evenly'; historyDeleteModalContainer.innerHTML += '<ytd-button-renderer id="cancel" class="style-scope ytd-masthead style-suggestive size-small" button-renderer="" use-keyboard-focused="" is-paper-button-with-icon="" is-paper-button=""></ytd-button-renderer>'; historyDeleteModalContainer.innerHTML += '<ytd-button-renderer id="delete" class="style-scope ytd-video-secondary-info-renderer style-destructive size-default" button-renderer="" use-keyboard-focused="" is-paper-button-with-icon="" is-paper-button=""></ytd-button-renderer>'; let historyDeleteModalCancel = historyDeleteModalContainer.querySelector('#cancel'); let historyDeleteModalDelete = historyDeleteModalContainer.querySelector('#delete'); historyDeleteModalCancel.onclick = toggleDeleteHistoriesModal; historyDeleteModalDelete.onclick = deleteHistories; historyDeleteModalCancel.innerHTML = '<tp-yt-paper-button id="button" class="style-scope ytd-button-renderer style-suggestive size-small" role="button" tabindex="0" animated="" elevation="0" aria-disabled="false" aria-label="Sign in"><yt-formatted-string id="text" class="style-scope ytd-button-renderer style-suggestive size-small"></yt-formatted-string></tp-yt-paper-button>'; historyDeleteModalDelete.innerHTML = '<tp-yt-paper-button id="button" class="style-scope ytd-button-renderer style-destructive size-default" role="button" tabindex="0" animated="" elevation="0" aria-disabled="false" aria-label="Subscribe"><yt-formatted-string id="text" class="style-scope ytd-button-renderer style-destructive size-default"></yt-formatted-string></tp-yt-paper-button>'; historyDeleteModalCancel.querySelector('yt-formatted-string').innerText = 'Cancel'; historyDeleteModalCancel.querySelector('yt-formatted-string').removeAttribute('is-empty'); historyDeleteModalDelete.querySelector('yt-formatted-string').innerText = 'Delete'; historyDeleteModalDelete.querySelector('yt-formatted-string').removeAttribute('is-empty'); historyDeleteModalCancel.querySelector('tp-yt-paper-button').style.padding = '4px 11px'; historyDeleteModal.remove(); historyItem.insertBefore(historyDeleteModal, historyDeleteControls); let historyNone = document.createElement('tp-yt-paper-item'); historyItem.appendChild(historyNone); historyNone.id = 'history-none'; historyNone.style.display = 'none'; historyNone.innerHTML = '<yt-formatted-string class="style-scope ytd-menu-service-item-renderer"></yt-formatted-string>'; historyNone.querySelector('yt-formatted-string').innerText = 'No Histories'; historyNone.querySelector('yt-formatted-string').removeAttribute('is-empty'); if (!histories.length) { historyItem.style.flexDirection = 'column-reverse'; historyDeleteControls.style.display = 'none'; historyDeleteModal.style.display = 'none'; historyImportContainer.style.marginLeft = 'initial'; historyImportInput.style.width = ''; historyData.style.display = 'none'; historyNone.style.display = 'flex'; } itemsTemp.remove(); }; let getHistoryRaw = (id) => { return localStorage.getItem(`youtubespecialcommentsticker-sticky-items-${id}`); }; let getHistory = (id) => { return JSON.parse(getHistoryRaw(id)); }; let getHistories = () => { return Object.keys(localStorage).filter(key => key.indexOf('youtubespecialcommentsticker-sticky-items-') > -1); }; let getDetailedHistories = () => { let index = getSavedStickyIndex(); let histories = getHistories(); return histories.map((history) => { let id = history.substr(42); return { id: id, details: index[id] || {}, size: { text: getHistorySizeText(id), title: getHistorySizeTextTitle(id) } } }).sort((history, otherHistory) => { if (history.details.author === otherHistory.details.author) { return history.size.title < otherHistory.size.title ? 1 : -1; } else if (!history.details.author || !otherHistory.details.author) { return !history.details.author && !otherHistory.details.author ? 0 : (!history.details.author ? 1 : -1); } return history.details.author < otherHistory.details.author ? -1 : 1; }); }; let selectHistory = (event) => { let histories = getHistories(); if (event.currentTarget.hasAttribute('checked')) { historiesSelected[event.currentTarget.dataset.id] = true; } else { delete historiesSelected[event.currentTarget.dataset.id]; } let selectedCount = Object.keys(historiesSelected).length; let historyDeleteButton = history.querySelector('#history-delete-button'); let historyExportButton = history.querySelector('#history-export-button'); let historyImport = history.querySelector('#history-import'); historyDeleteButton.style.opacity = selectedCount ? 1 : 0; historyDeleteButton.style.pointerEvents = selectedCount ? 'inherit' : 'none'; historyDeleteButton.style.position = selectedCount ? 'initial' : 'absolute'; historyExportButton.style.opacity = selectedCount ? 1 : 0; historyExportButton.style.pointerEvents = selectedCount ? 'inherit' : 'none'; historyExportButton.style.position = selectedCount ? 'initial' : 'absolute'; historyImport.style.opacity = selectedCount ? 0 : 1; historyImport.style.pointerEvents = selectedCount ? 'none' : 'inherit'; historyImport.style.position = selectedCount ? 'absolute' : 'relative'; if (selectedCount === histories.length) { history.querySelector('#history-select-all').setAttribute('active', ''); } else { history.querySelector('#history-select-all').removeAttribute('active'); } }; let toggleEditHistoryModal = (event) => { let historyId = event.currentTarget.dataset.id; let historyData = history.querySelectorAll('#history-data > tp-yt-paper-item'); let historyItem = history.querySelector(`#items #${historyId}`); let historyItemEdit = historyItem.querySelector('.history-item-edit'); let historyItemHeader = historyItem.querySelector('.history-item-header'); let historyItemHeaderShowHideButton = historyItemHeader.querySelector('yt-icon[icon="collapse"]'); let historyOtherHistoryItems = history.querySelectorAll(`.history-item:not(#${historyId})`); let visible = Number(historyItemEdit.style.opacity); if (visible) { historyItemEdit.querySelectorAll('yt-live-chat-text-input-field-renderer').forEach((input) => { input.removeAttribute('has-text'); input.querySelector('#input').innerText = ''; }); historyItem.removeAttribute('style'); history.removeAttribute('editing'); } else { history.setAttribute('editing', ''); } if (historyItemHeaderShowHideButton) { historyItemHeaderShowHideButton.click(); } [...historyData, ...historyOtherHistoryItems].forEach((element) => { if (element.style.opacity !== '0') { element.style.opacity = visible ? 1 : 0.5; element.style.pointerEvents = visible ? 'initial' : 'none'; } }); historyItemEdit.style.opacity = visible ? 0 : 1; historyItemEdit.style.zIndex = visible ? -1 : 1; historyItemEdit.style.position = visible ? 'absolute' : 'initial'; historyItemHeader.style.height = visible ? 'initial' : '0px'; historyItemHeader.style.overflow = visible ? 'initial' : 'hidden'; }; let refreshHistory = () => { history.setAttribute('refreshing', ''); setUpHistory(); history.focus(); history.removeAttribute('refreshing'); }; let updateHistoryItem = (event) => { let savedStickyIndex = getSavedStickyIndex(); let historyId = event.currentTarget.dataset.id; let historyItem = history.querySelector(`#items #${historyId}`); let historyItemEdit = historyItem.querySelector('.history-item-edit'); let historyItemEditAuthor = historyItemEdit.querySelector('yt-live-chat-text-input-field-renderer.author'); let historyItemEditIcon = historyItemEdit.querySelector('yt-live-chat-text-input-field-renderer.icon'); let historyItemEditID = historyItemEdit.querySelector('yt-live-chat-text-input-field-renderer.id'); let historyItemEditTitle = historyItemEdit.querySelector('yt-live-chat-text-input-field-renderer.title'); if (savedStickyIndex[historyId]) { savedStickyIndex[historyId] = { author: historyItemEditAuthor.innerText, icon: historyItemEditIcon.innerText.replace(/=s\d+-/, '=s32-'), id: historyItemEditID.innerText, title: historyItemEditTitle.innerText, valid: true }; localStorage.setItem('youtubespecialcommentsticker-sticky-index', JSON.stringify(savedStickyIndex)); } toggleEditHistoryModal(event); refreshHistory(); }; let handleHistoryEditModalInputPaste = (event) => { handlePaste(event.currentTarget); }; let handleHistoryEditModalInputHasText = (event) => { if (event.currentTarget.innerText) { event.currentTarget.parentElement.setAttribute('has-text', ''); } else { event.currentTarget.parentElement.removeAttribute('has-text'); } }; let isEditModalInputValid = (input) => { let field = input.parentElement.classList[2]; let required = (input) => !!input.innerText; let isIconFormat = (input) => /https:\/\/[a-zA-Z\d]+\.[a-zA-Z\d]+\.com\/[a-zA-Z\d]+\/.+=s\d+-.+/.test(input.innerText); let isIdFormat = (input) => /[a-zA-Z0-9_-]{11}/.test(input.innerText); return { author: [required], icon: [required, isIconFormat], id: [required, isIdFormat], title: [required] }[field].every((validation) => validation(input)); }; let validateEditModalInput = (event) => { let input = event.currentTarget; let parent = input.parentElement; if (isEditModalInputValid(input)) { parent.setAttribute('valid', ''); } else { parent.removeAttribute('valid'); } }; let validateEditModalForm = (event) => { let form = event.currentTarget.parentElement.parentElement; let submit = form.querySelector('#submit'); let valid = [...form.querySelectorAll('yt-live-chat-text-input-field-renderer')].every(input => input.hasAttribute('valid')); valid ? submit.removeAttribute('disabled') : submit.setAttribute('disabled', ''); submit.style.pointerEvents = valid ? 'initial' : 'none'; }; let handleHistoryEditModalInputKeyUp = (event) => { handleHistoryEditModalInputHasText(event); validateEditModalInput(event); validateEditModalForm(event); }; let getHistorySizeText = (id) => { let historySize = getObjectSize(Object.values(getHistoryRaw(id))); return historySize >= 0.01 ? `${historySize.toFixed(2)} MB` : '< 10 KB'; }; let getHistorySizeTextTitle = (id) => { return `${getObjectSize(Object.values(getHistoryRaw(id))).toFixed(10)} MB`; }; let updateHistorySizeText = (id) => { let historySizeText = history.querySelector(`#items #${id} .history-size #${id}`); historySizeText.innerText = getHistorySizeText(id); historySizeText.title = getHistorySizeTextTitle(id); }; let setHistoryItemsScroll = (historyItemChat) => { if (historyItemChat.offsetHeight === 151) { let historyItems = history.querySelector('#items'); let historyItemsRect = historyItems.getBoundingClientRect(); let historyItemRect = historyItemChat.getBoundingClientRect(); if (historyItemRect.bottom > historyItemsRect.bottom) { historyItems.scrollTop += historyItemRect.bottom - historyItemsRect.bottom + 8; } } else { setTimeout(() => setHistoryItemsScroll(historyItemChat)); } }; let showHideHistoryItem = (event) => { let historyItem = event.currentTarget; let historyItemId = historyItem.dataset.id; let historyItemIcon = historyItem.querySelector('yt-icon'); let historyItemChat = history.querySelector(`#${historyItemId} .history-item-chat`); let historyItemChatHeader = historyItemChat.querySelector('#chat-header'); let historyItemChatItems = historyItemChatHeader.querySelector('#primary-content'); if (!historyItemChat.offsetHeight) { let historyItems = getHistory(historyItemId); historyItems.forEach((item) => historyItemChatItems.appendChild(getStickyItem(item))); historyItemIcon.setAttribute('icon', 'collapse'); historyItemChat.removeAttribute('style'); historyItemChatHeader.removeAttribute('style'); historyItemChatItems.style.height = 'initial'; setHistoryItemsScroll(historyItemChat); } else { historyItemChatItems.querySelectorAll('.sticky-item').forEach(historyItemChatItem => historyItemChatItem.remove()); historyItemIcon.setAttribute('icon', 'expand'); historyItemChat.style.border = 'none'; historyItemChatHeader.style.height = '0px'; historyItemChatHeader.style.border = 'none'; historyItemChatItems.style.height = '0px'; } updateHistorySizeText(historyItemId); }; let setHistoryItem = (histories, items, itemsTemp) => { if (histories.length) { let history = histories[0]; let historyId = history.id; let historyDetails = history.details; itemsTemp.innerHTML = `<ytd-menu-service-item-renderer id="${historyId}" class="style-scope ytd-menu-popup-renderer history-item" role="menuitem" tabindex="-1" aria-selected="false" data='{"serviceEndpoint": {}}'></ytd-menu-service-item-renderer>`; let historyItem = itemsTemp.querySelector(`#${historyId}`); historyItem.remove(); let historyItemHeader = document.createElement('div'); historyItemHeader.classList.add('history-item-header'); let historyDelete = document.createElement('tp-yt-paper-item'); itemsTemp.appendChild(historyDelete); historyDelete.remove(); let historyCheckbox = document.createElement('tp-yt-paper-checkbox'); historyDelete.append(historyCheckbox); historyCheckbox.classList.add('history-item-checkbox'); historyCheckbox.style.setProperty('--paper-checkbox-ink-size', '32px'); historyCheckbox.style.width = '20px'; historyCheckbox.dataset.id = historyId; historyCheckbox.onclick = selectHistory; let historyIcon = document.createElement('tp-yt-paper-item'); itemsTemp.appendChild(historyIcon); let thumbnail = JSON.stringify({"thumbnails": [{"url": historyDetails.icon || 'https://yt3.ggpht.com/a/default-user=s32-c-k-c0x00ffffff-no-rj', "width": 48, "height": 48}]}); historyIcon.innerHTML = `<yt-img-shadow id="avatar" class="style-scope ytd-video-owner-renderer no-transition" style="border-radius: 50%; overflow: hidden; width: 32px; user-select: none;" loaded="" thumbnail='${thumbnail}'></yt-img-shadow>`; historyIcon.querySelector('img').title = historyDetails.author || 'Unknown'; historyIcon.remove(); let historyName = historyItem.querySelector('tp-yt-paper-item'); historyName.style.overflow = 'hidden'; let historyNameText = historyName.querySelector('yt-formatted-string'); historyNameText.style.overflow = 'hidden'; historyNameText.removeAttribute('is-empty'); if (historyDetails.valid) { let historyNameLink = document.createElement('a'); historyNameLink.className = 'yt-simple-endpoint style-scope yt-formatted-string'; historyNameLink.href = `https://www.youtube.com/watch?v=${historyDetails.id}`; historyNameLink.innerText = historyDetails.title; historyNameLink.style.display = 'block'; historyNameLink.style.overflow = 'hidden'; historyNameLink.style.textOverflow = 'ellipsis'; historyNameText.title = historyNameLink.innerText; historyNameText.appendChild(historyNameLink); } else { historyNameText.innerText = historyDetails.title || 'Click here or visit the associated video with chat enabled to update history information.'; historyNameText.title = historyNameText.innerText; historyNameText.style.textOverflow = 'ellipsis'; historyNameText.style.cursor = 'pointer'; historyNameText.dataset.id = historyId; historyNameText.onclick = toggleEditHistoryModal; } historyName.remove(); let historySize = document.createElement('tp-yt-paper-item'); itemsTemp.appendChild(historySize); historySize.className = 'style-scope ytd-menu-service-item-renderer history-size'; historySize.style.marginLeft = 'auto'; let historySizeText = document.createElement('yt-formatted-string'); historySize.appendChild(historySizeText); historySizeText.className = 'style-scope ytd-menu-service-item-renderer'; historySizeText.id = historyId historySizeText.innerText = history.size.text; historySizeText.title = history.size.title; historySizeText.removeAttribute('is-empty'); historySize.remove(); let historyItemShowHide = document.createElement('tp-yt-paper-item'); historyItemShowHide.style.paddingRight = '16px'; historyItemShowHide.innerHTML += `<yt-icon-button class="style-scope yt-live-chat-header-renderer" style="visibility: inherit;" data-id="${historyId}"><button id="button" class="style-scope yt-icon-button"><yt-icon icon="expand" class="style-scope yt-live-chat-header-renderer"></yt-icon></button><paper-ripple class="style-scope yt-icon-button circle"><div id="background" class="style-scope paper-ripple"></div><div id="waves" class="style-scope paper-ripple"></div></paper-ripple></button></yt-icon-button>`; let historyItemShowHideButton = historyItemShowHide.querySelector('yt-icon-button'); historyItemShowHideButton.classList.add('history-item-show-hide-button'); historyItemShowHideButton.onclick = showHideHistoryItem; historyItemHeader.appendChild(historyDelete); historyItemHeader.appendChild(historyIcon); historyItemHeader.appendChild(historyName); historyItemHeader.appendChild(historySize); historyItemHeader.appendChild(historyItemShowHide); if (!historyDetails.valid) { let historyEditModal = document.createElement('tp-yt-paper-item'); historyEditModal.style.opacity = 0; historyEditModal.style.justifyContent = 'space-between'; historyEditModal.style.position = 'absolute'; historyEditModal.style.zIndex = -1; historyEditModal.style.userSelect = 'none'; historyEditModal.classList.add('history-item-edit'); itemsTemp.appendChild(historyEditModal); [{ id: 'author', title: 'The channel name.' }, { id: 'icon', title: 'The channel icon: Right-Click Channel Icon > Copy Image Address.' }, { id: 'id', title: 'The video ID: https://www.youtube.com/watch?v=<ID>.' }, { id: 'title', title: 'The video title.' }].forEach((input) => { historyEditModal.innerHTML += `<yt-live-chat-text-input-field-renderer id="input" class="style-scope yt-live-chat-message-input-renderer ${input.id}" disabled="" emoji-manager='{"emojiShortcutMap": {}}' title="${input.title}"></yt-live-chat-text-input-field-renderer>`; }); historyEditModal.innerHTML += `<ytd-button-renderer id="cancel" class="style-scope ytd-masthead style-suggestive size-small" button-renderer="" use-keyboard-focused="" is-paper-button-with-icon="" is-paper-button="" data-id="${historyId}"></ytd-button-renderer>`; historyEditModal.innerHTML += `<ytd-button-renderer id="submit" class="style-scope ytd-video-secondary-info-renderer style-destructive size-default" button-renderer="" use-keyboard-focused="" is-paper-button-with-icon="" is-paper-button="" data-id="${historyId}" disabled style="pointer-events: none;"></ytd-button-renderer>`; let historyEditModalCancel = historyEditModal.querySelector('#cancel'); let historyEditModalSubmit = historyEditModal.querySelector('#submit'); historyEditModalCancel.onclick = toggleEditHistoryModal; historyEditModalSubmit.onclick = updateHistoryItem; historyEditModalCancel.innerHTML = '<tp-yt-paper-button id="button" class="style-scope ytd-button-renderer style-suggestive size-small history-control-button" role="button" tabindex="0" animated="" elevation="0" aria-disabled="false" aria-label="Sign in" title="Cancel"><yt-icon icon="close" class="style-scope yt-live-chat-header-renderer"></yt-icon></tp-yt-paper-button>'; historyEditModalSubmit.innerHTML = '<tp-yt-paper-button id="button" class="style-scope ytd-button-renderer style-destructive size-default history-control-button history-affirmative-button" role="button" tabindex="0" animated="" elevation="0" aria-disabled="false" aria-label="Subscribe" title="Submit"><yt-icon icon="check" class="style-scope yt-live-chat-header-renderer"></yt-icon></tp-yt-paper-button>'; let historyEditModalAuthor = historyEditModal.querySelector('.author'); let historyEditModalIcon = historyEditModal.querySelector('.icon'); let historyEditModalId = historyEditModal.querySelector('.id'); let historyEditModalTitle = historyEditModal.querySelector('.title'); historyEditModalAuthor.querySelector('#label').innerText = 'Author'; historyEditModalIcon.querySelector('#label').innerText = 'Icon'; historyEditModalId.querySelector('#label').innerText = 'ID'; historyEditModalTitle.querySelector('#label').innerText = 'Title'; historyEditModalAuthor.querySelector('#input').onpaste = handleHistoryEditModalInputPaste; historyEditModalIcon.querySelector('#input').onpaste = handleHistoryEditModalInputPaste; historyEditModalId.querySelector('#input').onpaste = handleHistoryEditModalInputPaste; historyEditModalTitle.querySelector('#input').onpaste = handleHistoryEditModalInputPaste; historyEditModalAuthor.querySelector('#input').onkeyup = handleHistoryEditModalInputKeyUp; historyEditModalIcon.querySelector('#input').onkeyup = handleHistoryEditModalInputKeyUp; historyEditModalId.querySelector('#input').onkeyup = handleHistoryEditModalInputKeyUp; historyEditModalTitle.querySelector('#input').onkeyup = handleHistoryEditModalInputKeyUp; historyEditModal.remove(); historyItem.appendChild(historyEditModal); } let historyItemChat = document.createElement('div'); historyItemChat.style.border = 'none'; historyItemChat.classList.add('history-item-chat'); historyItemChat.innerHTML = '<div id="chat-header" role="heading" class="style-scope yt-live-chat-renderer" style="height: 0px; border: none;"><div id="primary-content" class="style-scope yt-live-chat-header-renderer"></div></div>'; itemsTemp.appendChild(historyItemChat); historyItem.appendChild(historyItemHeader); historyItem.appendChild(historyItemChat); items.appendChild(historyItem); setTimeout(() => setHistoryItem(histories.slice(1), items, itemsTemp)); } else { itemsTemp.remove(); historiesSelected = {}; toggleHistoryItems(); } }; let toggleHistoryItems = () => { let items = history.querySelector('#items'); if (Number(items.style.opacity)) { items.removeAttribute('style'); } else { items.style.opacity = 0.5; items.style.pointerEvents = 'none'; } }; let setUpHistoryItems = () => { let histories = getDetailedHistories(); let items = history.querySelector('#items'); let itemsTemp = document.createElement('div'); itemsTemp.style.position = 'absolute'; items.parentNode.insertBefore(itemsTemp, items); toggleHistoryItems(); setTimeout(() => setHistoryItem(histories, items, itemsTemp)); }; let setUpHistory = () => { resetHistoryMenu(); setUpHistoryHeader(); setUpHistoryItems(); }; let isSettingsListSet = (list) => { return settingsData[list].text.raw || settingsData[list].regex.raw; }; let setUpSettingsItemsStates = (open) => { if ((!open && isSettingsListSet('blacklist')) || blacklist.querySelector('#input > div').innerText.trim()) { whitelist.classList.add('disabled'); } else { whitelist.classList.remove('disabled'); } if ((!open && isSettingsListSet('whitelist')) || whitelist.querySelector('#input > div').innerText.trim()) { blacklist.classList.add('disabled'); } else { blacklist.classList.remove('disabled'); } }; let getEscapedTextRegex = (text) => { return text ? `(${text.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').split(',').filter(Boolean).join('|')})` : text; }; let setSettingsList = (element, name, visible) => { if (visible) { element.querySelector('#input > div').innerText = settingsData[name].text.raw || settingsData[name].regex.raw; } else { let innerText = element.querySelector('#input > div').innerText.trim(); let match = innerText.match(/^\/(.+?)\/([igmsuy]{0,6}?)$/); if (match) { settingsData[name].text = { raw: null, escaped: null }; settingsData[name].regex = { raw: match[0], escaped: match[1], flags: match[2] }; } else { settingsData[name].regex = { raw: null, escaped: null, flags: null }; settingsData[name].text = { raw: innerText, escaped: getEscapedTextRegex(innerText) }; } } }; let getChatSettingsButton = () => { let iconButton = document.querySelector('#chat-messages #overflow'); let liveChatButton = document.querySelector('#chat-messages #live-chat-header-context-menu'); return (!iconButton || iconButton.hidden) ? liveChatButton : iconButton; }; let setUpSettingsItems = () => { let items = settings.querySelector('#items'); let types; let controls; chatSettingsButton = getChatSettingsButton(); ['blacklist', 'whitelist', 'types', 'controls'].forEach((id) => { items.innerHTML += `<ytd-menu-service-item-renderer id="${id}" class="style-scope ytd-menu-popup-renderer" role="menuitem" tabindex="-1" aria-selected="false" data='{"serviceEndpoint": {}}'></ytd-menu-service-item-renderer>`; }); blacklist = items.querySelector('#blacklist tp-yt-paper-item'); blacklist.innerHTML += `<yt-live-chat-text-input-field-renderer id="input" class="style-scope yt-live-chat-message-input-renderer" disabled="" emoji-manager='{"emojiShortcutMap": {}}'></yt-live-chat-text-input-field-renderer>`; blacklist.querySelector('yt-formatted-string').innerText = 'Blacklist'; blacklist.querySelector('yt-formatted-string').removeAttribute('is-empty'); setSettingsList(blacklist, 'blacklist', true); whitelist = items.querySelector('#whitelist tp-yt-paper-item'); whitelist.innerHTML += '<yt-live-chat-text-input-field-renderer id="input" class="style-scope yt-live-chat-message-input-renderer" disabled=""></yt-live-chat-text-input-field-renderer>'; whitelist.querySelector('yt-formatted-string').innerText = 'Whitelist'; whitelist.querySelector('yt-formatted-string').removeAttribute('is-empty'); setSettingsList(whitelist, 'whitelist', true); types = items.querySelector('#types tp-yt-paper-item'); types.innerHTML += '<div id="toggle" class="style-scope ytd-compact-autoplay-renderer" role="button" checked=""><div class="toggle-container style-scope paper-toggle-button"><div id="toggleBar" class="toggle-bar style-scope paper-toggle-button"></div><div id="toggleButton" class="toggle-button style-scope paper-toggle-button"></div></div></div>'; types.append(types.querySelector('yt-formatted-string').cloneNode()); types.innerHTML += '<div id="toggle" class="style-scope ytd-compact-autoplay-renderer" role="button" checked=""><div class="toggle-container style-scope paper-toggle-button"><div id="toggleBar" class="toggle-bar style-scope paper-toggle-button"></div><div id="toggleButton" class="toggle-button style-scope paper-toggle-button"></div></div></div>'; types.querySelector('yt-formatted-string').innerText = 'Moderators'; types.querySelector('yt-formatted-string').removeAttribute('is-empty'); types.querySelector('yt-formatted-string:nth-of-type(2)').innerText = 'Owner'; types.querySelector('yt-formatted-string:nth-of-type(2)').removeAttribute('is-empty'); moderator = types.querySelector('#toggle'); owner = types.querySelector('#toggle:nth-of-type(2)'); settingsData.moderator ? moderator.setAttribute('checked', '') : moderator.removeAttribute('checked'); settingsData.owner ? owner.setAttribute('checked', '') : owner.removeAttribute('checked'); controls = items.querySelector('#controls tp-yt-paper-item'); controls.innerHTML += '<ytd-button-renderer id="cancel" class="style-scope ytd-masthead style-suggestive size-small" button-renderer="" use-keyboard-focused="" is-paper-button-with-icon="" is-paper-button=""></ytd-button-renderer>'; controls.innerHTML += '<ytd-button-renderer id="save" class="style-scope ytd-video-secondary-info-renderer style-destructive size-default" button-renderer="" use-keyboard-focused="" is-paper-button-with-icon="" is-paper-button=""></ytd-button-renderer>'; cancel = controls.querySelector('#cancel'); save = controls.querySelector('#save'); cancel.innerHTML = '<tp-yt-paper-button id="button" class="style-scope ytd-button-renderer style-suggestive size-small" role="button" tabindex="0" animated="" elevation="0" aria-disabled="false" aria-label="Sign in"><yt-formatted-string id="text" class="style-scope ytd-button-renderer style-suggestive size-small"></yt-formatted-string></tp-yt-paper-button>'; save.innerHTML = '<tp-yt-paper-button id="button" class="style-scope ytd-button-renderer style-destructive size-default" role="button" tabindex="0" animated="" elevation="0" aria-disabled="false" aria-label="Subscribe"><yt-formatted-string id="text" class="style-scope ytd-button-renderer style-destructive size-default"></yt-formatted-string></tp-yt-paper-button>'; cancel.querySelector('yt-formatted-string').innerText = 'Cancel'; cancel.querySelector('yt-formatted-string').removeAttribute('is-empty'); save.querySelector('yt-formatted-string').innerText = 'Save'; save.querySelector('yt-formatted-string').removeAttribute('is-empty'); setUpSettingsItemsStates(); }; let setUpSettingsData = () => { let savedSettingsData = localStorage.getItem('youtubespecialcommentsticker-settings'); if (savedSettingsData) { settingsData = JSON.parse(savedSettingsData); } if (!settingsData || typeof settingsData.blacklist === 'string' || typeof settingsData.whitelist === 'string') { let blacklistText = settingsData ? settingsData.blacklist : null; let whitelistText = settingsData ? settingsData.whitelist : null; settingsData = { blacklist: { text: { raw: blacklistText, escaped: getEscapedTextRegex(blacklistText) }, regex: { raw: null, escaped: null } }, whitelist: { text: { raw: whitelistText, escaped: getEscapedTextRegex(whitelistText) }, regex: { raw: null, escaped: null } }, moderator: true, owner: true }; localStorage.setItem('youtubespecialcommentsticker-settings', JSON.stringify(settingsData)); } }; let setUpSettings = () => { setUpSettingsData(); setUpSettingsItems(); }; let setUpGlobalSavedStickyIndex = () => { let savedStickyIndex = getSavedStickyIndex(); if (!Object.keys(savedStickyIndex).length) { let histories = getHistories(); histories.forEach((history) => { let id = history.substr(42); if (!savedStickyIndex[id]) { savedStickyIndex[id] = { author: 'Unknown', icon: 'https://yt3.ggpht.com/a/default-user=s32-c-k-c0x00ffffff-no-rj', id: id.length > 11 ? null : id, title: 'Click here or visit the associated video with chat enabled to update history information.', valid: false }; } }); localStorage.setItem('youtubespecialcommentsticker-sticky-index', JSON.stringify(savedStickyIndex)); } }; let getSavedStickyIndex = () => { let savedStickyIndex = localStorage.getItem('youtubespecialcommentsticker-sticky-index'); return savedStickyIndex ? JSON.parse(savedStickyIndex) : {}; }; let getYtInitialData = () => { let ytInitialDataScript = [...window.top.document.querySelectorAll('script')].find(script => script.text.indexOf('["ytInitialData"]') > -1); return JSON.parse(ytInitialDataScript.text.substr(26, ytInitialDataScript.text.length - 27)); }; let getMicroFormatPlayerData = () => { let microFormatScript = window.top.document.querySelector('#microformat script'); return JSON.parse(microFormatScript.text); }; let getSavedStickyIndexDetails = (savedStickyIndex) => { if (IS_POPOUT) { let stickyIndexDetails = Object.values(savedStickyIndex).find(stickyIndex => stickyIndex.id === CHAT_ID); if (stickyIndexDetails) { return stickyIndexDetails; } let ytInitialData = getYtInitialData(); return { author: ytInitialData.contents.liveChatRenderer.participantsList.liveChatParticipantsListRenderer.participants[0].liveChatParticipantRenderer.authorName.simpleText, icon: ytInitialData.contents.liveChatRenderer.participantsList.liveChatParticipantsListRenderer.participants[0].liveChatParticipantRenderer.authorPhoto.thumbnails[0].url, id: CHAT_ID, title: `https://www.youtube.com/watch?v=${CHAT_ID}`, valid: true }; } let videoId = window.top.document.location.href.split('=')[1]; let microFormatPlayerData = getMicroFormatPlayerData(); let existingStickyIndexDetails = savedStickyIndex[CHAT_ID]; if (microFormatPlayerData.embedUrl.substr(-11) !== videoId || (existingStickyIndexDetails && existingStickyIndexDetails.id && existingStickyIndexDetails.id !== videoId)) { throw new Error('Incorrect CHAT_ID.'); } return { author: microFormatPlayerData.author || 'Unknown', icon: window.top.document.querySelector('#avatar img[src*="http"]').src.replace('=s48', '=s32') || 'https://yt3.ggpht.com/a/default-user=s32-c-k-c0x00ffffff-no-rj', id: videoId, title: microFormatPlayerData.name || 'Unknown', valid: true }; }; let setUpLocalSavedStickyIndex = () => { try { let savedStickyIndex = getSavedStickyIndex(); savedStickyIndex[CHAT_ID] = getSavedStickyIndexDetails(savedStickyIndex); if (!IS_POPOUT && savedStickyIndex[savedStickyIndex[CHAT_ID].id]) { savedStickyIndex[savedStickyIndex[CHAT_ID].id] = savedStickyIndex[CHAT_ID]; } localStorage.setItem('youtubespecialcommentsticker-sticky-index', JSON.stringify(savedStickyIndex)); } catch (e) { if (e.message !== 'Incorrect CHAT_ID.') { setTimeout(setUpLocalSavedStickyIndex); } } }; let setUpSavedStickyIndex = () => { setUpGlobalSavedStickyIndex(); setUpLocalSavedStickyIndex(); }; let getStickyItem = (savedItem) => { let stickyItem = document.createElement('div'); let message = savedItem.content.message.reduce((result, node) => { if (node.type === 'text') { result += node.value; } else if (node.type === 'image') { result += `<img class="emoji style-scope yt-live-chat-text-message-renderer" src="${node.value.src}" alt="${node.value.alt}">`; } else { result += `<a class="yt-simple-endpoint style-scope yt-live-chat-text-message-renderer" href="${node.value.href}" rel="nofollow" target="_blank">${node.value.text}</a>`; } return result; }, ''); stickyItem.classList.add('sticky-item'); stickyItem.innerHTML = `<div id="author-photo" class="no-transition style-scope yt-live-chat-text-message-renderer" height="24" width="24" style="background-color: transparent;"><img id="img" class="style-scope yt-img-shadow" alt="" height="24" width="24" src="${savedItem.authorPhoto.img.src}"></div>`; stickyItem.innerHTML += `<div id="content" class="style-scope yt-live-chat-text-message-renderer"><span class="timestamp">${savedItem.content.timestamp}</span><div class="author"><span class="author-name ${savedItem.content.author.classes}">${savedItem.content.author.name}</span></div><span id="message" dir="auto" class="style-scope yt-live-chat-text-message-renderer">${message}</span></div>`; return stickyItem; }; let stickSavedItem = (savedItem) => { if (isSpecial(savedItem)) { let stickyItem = getStickyItem(savedItem); stickyItems.append(stickyItem); stickyItemObjectsMap[`${savedItem.content.timestamp}-${savedItem.content.author.name}-${stickyItem.querySelector('#message').innerText}`] = true; showSticky(); scrollSticky(); } }; let setUpSavedStickyItems = () => { let savedStickyItems = localStorage.getItem(`youtubespecialcommentsticker-sticky-items-${CHAT_ID}`); stickyItemObjectsMap = {}; stickyItemObjects = savedStickyItems ? JSON.parse(savedStickyItems) : []; stickyItemObjects.forEach(stickSavedItem); }; let setUpSticky = () => { let hideTimestamp = document.querySelector('yt-live-chat-renderer').hasAttribute('hide-timestamps'); let stickyAnchor = document.querySelector('yt-live-chat-app'); sticky = document.createElement('div'); sticky.id = 'sticky'; sticky.setAttribute('class', `style-scope ytd-watch-flexy ${IS_DARK ? 'dark' : ''} ${hideTimestamp ? 'hide-timestamps': ''}`); sticky.style.display = 'none'; sticky.innerHTML = '<tp-yt-iron-dropdown id="menu" horizontal-align="auto" vertical-align="top" aria-disabled="false" class="style-scope yt-live-chat-app sticky-panel" prevent-autonav="true" aria-hidden="true" tabindex="0"><div id="contentWrapper" class="style-scope tp-yt-iron-dropdown"></div></tp-yt-iron-dropdown>'; sticky.innerHTML += '<yt-icon-button id="overflow" class="style-scope yt-live-chat-header-renderer"><button id="button" class="style-scope yt-icon-button"><yt-icon icon="more_vert" class="style-scope yt-live-chat-header-renderer"><svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope yt-icon" style="pointer-events: none; display: block; width: 100%; height: 100%;"><g class="style-scope yt-icon"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" class="style-scope yt-icon"></path></g></svg></yt-icon></button><paper-ripple class="style-scope yt-icon-button circle"><div id="background" class="style-scope paper-ripple" style="opacity: 0;"></div><div id="waves" class="style-scope paper-ripple"></div></paper-ripple></yt-icon-button>'; sticky.innerHTML += '<div id="history" horizontal-align="auto" vertical-align="top" aria-disabled="false" class="style-scope yt-live-chat-app sticky-panel" prevent-autonav="true" style="display: none;" aria-hidden="true" tabindex="0"><div id="contentWrapper" class="style-scope iron-dropdown"><ytd-menu-popup-renderer slot="dropdown-content" class="style-scope yt-live-chat-app" tabindex="-1" use-icons_=""></ytd-menu-popup-renderer></div></div>'; sticky.innerHTML += '<div id="settings" horizontal-align="auto" vertical-align="top" aria-disabled="false" class="style-scope yt-live-chat-app sticky-panel" prevent-autonav="true" style="display: none;" aria-hidden="true" tabindex="0"><div id="contentWrapper" class="style-scope iron-dropdown"><ytd-menu-popup-renderer slot="dropdown-content" class="style-scope yt-live-chat-app" tabindex="-1" use-icons_=""></ytd-menu-popup-renderer></div></div>'; sticky.innerHTML += '<div id="show-hide-button" class="style-scope ytd-live-chat-frame"><div class="style-scope ytd-live-chat-frame" use-keyboard-focused="" is-paper-button="" button-renderer="true"><a class="yt-simple-endpoint style-scope ytd-toggle-button-renderer" tabindex="-1"><tp-yt-paper-button id="button" class="style-scope ytd-toggle-button-renderer" role="button" tabindex="0" animated="" elevation="0" aria-disabled="false"><yt-formatted-string id="text" class="style-scope ytd-toggle-button-renderer"></yt-formatted-string><paper-ripple class="style-scope tp-yt-paper-button"><div id="background" class="style-scope paper-ripple"></div><div id="waves" class="style-scope paper-ripple"></div></paper-ripple></tp-yt-paper-button></a></div></div>' sticky.innerHTML += '<div id="chat-header" role="heading" class="style-scope yt-live-chat-renderer"><div id="primary-content" class="style-scope yt-live-chat-header-renderer"></div>'; sticky.innerHTML += '<yt-icon-button id="show-more" class="style-scope yt-live-chat-item-list-renderer" style="transform: translateY(42px);"><button id="button" class="style-scope yt-icon-button"><button id="button" class="style-scope yt-icon-button" aria-label="More comments below"><yt-icon icon="down_arrow" class="style-scope yt-live-chat-item-list-renderer"><svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope yt-icon" style="pointer-events: none; display: block; width: 100%; height: 100%;"><g class="style-scope yt-icon"><path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z" class="style-scope yt-icon"></path></g></svg></yt-icon></button></button><paper-ripple class="style-scope yt-icon-button circle"><div id="background" class="style-scope paper-ripple" style="opacity: 0.006272;"></div><div id="waves" class="style-scope paper-ripple"></div></paper-ripple></yt-icon-button>' sticky.innerHTML += '<ytd-popup-container id="notice" class="style-scope ytd-app" style="display: none;"><tp-yt-iron-dropdown horizontal-align="right" vertical-align="top" aria-disabled="false" class="style-scope ytd-popup-container" force-close-on-outside-click="true" style="outline: none; position: fixed; left: 101px; top: 44px; z-index: 2202;"></tp-yt-iron-dropdown></ytd-popup-container>'; menu = sticky.querySelector('#menu'); menuButton = sticky.querySelector('#overflow'); history = sticky.querySelector('#history'); settings = sticky.querySelector('#settings'); stickyItemsContainer = sticky.querySelector('#chat-header'); toggleSticky = sticky.querySelector('#show-hide-button'); toggleStickyText = toggleSticky.querySelector('#text'); scrollButton = sticky.querySelector('#show-more'); scrollButton.querySelector('#button').style.height = 'auto'; notice = sticky.querySelector('#notice'); stickyAnchor.parentNode.insertBefore(sticky, stickyAnchor); stickyAnchor.style.position = 'relative'; toggleStickyText.innerText = 'Hide Sticky'; toggleStickyText.removeAttribute('is-empty'); sticky.querySelectorAll('#title, #view-selector, #action-buttons, #items, #footer').forEach(element => element.parentNode.removeChild(element)); stickyItems = sticky.querySelector('#primary-content'); history.querySelector('ytd-menu-popup-renderer').innerHTML = '<div id="items" class="style-scope ytd-menu-popup-renderer" role="listbox" tabindex="0"></div>'; settings.querySelector('ytd-menu-popup-renderer').innerHTML = '<div id="items" class="style-scope ytd-menu-popup-renderer" role="listbox" tabindex="0"></div>'; notice.querySelector('#contentWrapper').innerHTML = '<yt-tooltip-renderer id="tooltip" slot="dropdown-content" class="style-scope ytd-popup-container" tabindex="-1" has-buttons_="" style="outline: none; box-sizing: border-box; max-width: 294.406px; max-height: 120px;" position-type="OPEN_POPUP_POSITION_BOTTOMLEFT"></yt-tooltip-renderer>'; setUpMenu(); setUpSettings(); setUpSavedStickyIndex(); setUpSavedStickyItems(); }; let monitorDarkTheme = () => { let observer = new MutationObserver(() => { sticky.classList.toggle('dark'); }); observer.observe(document.querySelector('html'), { attributes: true, attributeFilter: ['dark'] }); }; let monitorTimestampToggle = () => { let observer = new MutationObserver(() => { sticky.classList.toggle('hide-timestamps'); scrollSticky(); }); observer.observe(document.querySelector('yt-live-chat-renderer'), { attributes: true, attributeFilter: ['hide-timestamps'] }); }; let monitorSettings = () => { let config = { characterData: true, subtree: true }; let blacklistObserver = new MutationObserver((mutations) => { if (mutations[0].target.data) { whitelist.classList.add('disabled'); } else { whitelist.classList.remove('disabled'); } }); let whitelistObserver = new MutationObserver((mutations) => { if (mutations[0].target.data) { blacklist.classList.add('disabled'); } else { blacklist.classList.remove('disabled'); } }); blacklistObserver.observe(blacklist, config); whitelistObserver.observe(whitelist, config); }; let restoreSettingsData = () => { setSettingsList(blacklist, 'blacklist', true); setSettingsList(whitelist, 'whitelist', true); settingsData.moderator ? moderator.setAttribute('checked', '') : moderator.removeAttribute('checked'); settingsData.owner ? owner.setAttribute('checked', '') : owner.removeAttribute('checked'); settings.style.display = 'none'; }; let handlePaste = (element) => { let paste = event.clipboardData.getData('Text'); let selection = window.getSelection(); let position = selection.anchorOffset; element.innerText = `${element.innerText.slice(0, position)}${paste}${element.innerText.slice(position)}`; try { selection.collapse(element.lastChild, position + paste.length); } catch (error) { selection.collapse(element.lastChild, 0); } }; let filterStickyItems = () => { Array.from(stickyItems.children).forEach((stickyItem) => { if (!isSpecial(stickyItem.querySelector('.author-name'))){ stickyItem.setAttribute('style', 'display: none;'); } else { stickyItem.removeAttribute('style'); } }); }; let setUpEvents = () => { stickyItems.addEventListener('scroll', () => { setTimeout(() => { canScroll = stickyItems.scrollHeight - stickyItems.scrollTop === stickyItems.clientHeight; scrollButton.style.transform = `translateY(${canScroll ? 42 : 0}px)`; scrollButton.style.visibility = canScroll ? 'hidden' : 'visible'; }); }); scrollButton.addEventListener('click', () => { canScroll = true; scrollSticky(); }); toggleSticky.addEventListener('click', () => { if (stickyItemsContainer.offsetHeight > 0) { chat.setAttribute('style', `max-height: ${chat.offsetHeight + 150}px;`); toggleStickyText.innerText = 'Show Sticky'; stickyItemsContainer.setAttribute('style', 'border: none;'); stickyItemsContainer.style.height = '0px'; scrollSticky(true); } else { chat.setAttribute('style', `max-height: ${chat.offsetHeight - 150}px;`); itemScroller.scrollTop += 150; toggleStickyText.innerText = 'Hide Sticky'; stickyItemsContainer.setAttribute('style', ''); stickyItemsContainer.style.height = '150px'; } }); chatSettingsButton.addEventListener('click', () => { if (sticky.style.display === 'none') { let chatSettings = document.querySelector('yt-live-chat-app > tp-yt-iron-dropdown #items'); if (!chatSettings.querySelector('#chatSettingsStickyButton')) { chatSettingsStickyButton = document.createElement('div'); chatSettingsStickyButton.innerHTML = `<ytd-menu-service-item-renderer id="chatSettingsStickyButton" class="style-scope ytd-menu-popup-renderer" role="menuitem" use-icons="" tabindex="-1" aria-selected="false" data='{"text": {"runs": [{"text":"Show Sticky"}]}, "icon": {"iconType": "PLAYLIST_ADD_CHECK"}, "serviceEndpoint": {}}'></ytd-menu-service-item-renderer>`; chatSettingsStickyButton = chatSettingsStickyButton.firstChild; chatSettings.append(chatSettingsStickyButton); chatSettingsStickyButton.addEventListener('click', () => { chatSettingsButton.click(); showSticky(); chatSettingsStickyButton.style.display = 'none'; }); } } }); menuButton.addEventListener('click', () => { menu.style.display = menu.style.display === 'none' ? 'initial' : 'none'; if (menu.style.display === 'initial') { menu.focus(); } }); menu.addEventListener('focusout', (event) => { if (!menu.contains(event.relatedTarget) && !menuButton.contains(event.relatedTarget)) { menu.style.display = 'none'; } }); historyButton.addEventListener('click', () => { setUpHistory(); history.style.display = history.style.display === 'none' ? 'initial' : 'none'; if (history.style.display === 'initial') { history.focus(); } }); history.addEventListener('focusout', (event) => { if (!history.contains(event.relatedTarget) && !history.hasAttribute('editing') && !history.hasAttribute('refreshing') && !history.hasAttribute('importing')) { history.style.display = 'none'; } }); settingsButton.addEventListener('click', () => { settings.style.display = settings.style.display === 'none' ? 'initial' : 'none'; if (settings.style.display === 'initial') { settings.focus(); } }); settings.addEventListener('focusout', (event) => { if (!settings.contains(event.relatedTarget)) { restoreSettingsData(); setUpSettingsItemsStates(); } }); blacklist.querySelector('#input > div').addEventListener('paste', () => { handlePaste(blacklist.querySelector('#input > div')); setUpSettingsItemsStates(true); }); whitelist.querySelector('#input > div').addEventListener('paste', () => { handlePaste(whitelist.querySelector('#input > div')); setUpSettingsItemsStates(true); }); settings.querySelectorAll('#toggle').forEach((toggle) => { toggle.addEventListener('click', (event) => { if (event.currentTarget.hasAttribute('checked')) { event.currentTarget.removeAttribute('checked'); } else { event.currentTarget.setAttribute('checked', ''); } }); }); cancel.addEventListener('click', () => { restoreSettingsData(); setUpSettingsItemsStates(); }); save.addEventListener('click', () => { setSettingsList(blacklist, 'blacklist'); setSettingsList(whitelist, 'whitelist'); settingsData.moderator = moderator.hasAttribute('checked'); settingsData.owner = owner.hasAttribute('checked'); localStorage.setItem('youtubespecialcommentsticker-settings', JSON.stringify(settingsData)); settings.style.display = 'none'; filterStickyItems(); }); notice.addEventListener('mouseleave', () => { notice.touched = true; notice.style.display = 'none'; }); window.addEventListener('beforeunload', () => { let savedSticky = getHistory(CHAT_ID); if (!savedSticky) { let savedStickyIndex = getSavedStickyIndex(); if (savedStickyIndex[CHAT_ID]) { delete savedStickyIndex[CHAT_ID]; localStorage.setItem('youtubespecialcommentsticker-sticky-index', JSON.stringify(savedStickyIndex)); } } }, false); }; let initMonitoring = () => { setUpCss(); setUpSticky(); monitor(); monitorDarkTheme(); monitorTimestampToggle(); monitorSettings(); setUpEvents(); }; let init = () => { if (window.top.document.readyState === 'complete') { initMonitoring(); } else { window.top.addEventListener('load', initMonitoring); } }; init(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址