SHOW:
|
|
- or go back to the newest paste.
| 1 | - | javascript:(function() {
|
| 1 | + | // ==UserScript== |
| 2 | - | var textareas = document.querySelectorAll('textarea.input');
|
| 2 | + | // @name Invidious BBCode Floating Button |
| 3 | - | textareas.forEach(function(textarea) {
|
| 3 | + | // @namespace http://tampermonkey.net/ |
| 4 | - | var text = textarea.value; |
| 4 | + | // @version 0.7 |
| 5 | // @description Adds a floating BB button to trigger BBCode comment and video metadata copying on Invidious instances | |
| 6 | - | |
| 6 | + | // @author You |
| 7 | - | var pattern1 = /$$\s*URL=(.*?)\s*\$\$(.*?)\s*\/\s*URL\s*$$/gi; |
| 7 | + | // @match *://yewtu.be/* |
| 8 | - | var pattern2 = /$$\s*URL\s*\$\$(.*?)\s*\/\s*URL\s*$$/gi; |
| 8 | + | // @match *://inv.nadeko.net/* |
| 9 | - | replacedText = text.replace(pattern1, '[U][URL=$1]$2[/URL][/U]'); |
| 9 | + | // @match *://invidious.nerdvpn.de/* |
| 10 | - | replacedText = replacedText.replace(pattern2, '[U][URL]$1[/URL][/U]'); |
| 10 | + | // @match *://iv.ggtyler.dev/* |
| 11 | - | replacedText = replacedText.replace(/$$U$$\$\$U\$\$URL=(.*?)\/URL\$\$\/U\$\$\/U$$/g, '[U][URL=$1[/URL][/U]'); |
| 11 | + | // @match *://invidious.jing.rocks/* |
| 12 | // @match *://invidious.perennialte.ch/* | |
| 13 | - | |
| 13 | + | // @match *://invidious.reallyaweso.me/* |
| 14 | - | var existingUrlPattern = /(?<!\[U\])\[URL=(.*?)\](.*?)\[\/URL\](?!\[\/U\])/gi; |
| 14 | + | // @match *://invidious.privacyredirect.com/* |
| 15 | - | replacedText = replacedText.replace(existingUrlPattern, '[U][URL=$1]$2[/URL][/U]'); |
| 15 | + | // @match *://invidious.einfachzocken.eu/* |
| 16 | // @match *://inv.tux.pizza/* | |
| 17 | - | var existingUrlSimplePattern = /(?<!\[U\])\[URL\](.*?)\[\/URL\](?!\[\/U\])/gi; |
| 17 | + | // @match *://iv.nboeck.de/* |
| 18 | - | replacedText = replacedText.replace(existingUrlSimplePattern, '[U][URL]$1[/URL][/U]'); |
| 18 | + | // @match *://iv.nowhere.moe/* |
| 19 | // @match *://invidious.adminforge.de/* | |
| 20 | - | var pattern3 = /(\n)([A-Z"]|\d|$$B$$|$$I$$|$$IMG$$)/g; |
| 20 | + | // @match *://invidious.yourdevice.ch/* |
| 21 | - | var pattern6 = /($$HEADING=2$$|$$LIST$$|$$\*$$|$$\/LIST$$|$$IMG$$)/g; |
| 21 | + | // @match *://invidious.privacydev.net/* |
| 22 | - | replacedText = replacedText.replace(pattern6, '\n$1'); |
| 22 | + | // @grant none |
| 23 | - | replacedText = replacedText.replace(/$$\/HEADING$$\n/g, '[/HEADING]\n'); |
| 23 | + | // ==/UserScript== |
| 24 | - | replacedText = replacedText.replace(/$$\/IMG$$\n/g, '[/IMG]\n'); |
| 24 | + | |
| 25 | - | replacedText = replacedText.replace(/\[\/LIST\]/g, '[/LIST]\n'); |
| 25 | + | (function() {
|
| 26 | 'use strict'; | |
| 27 | - | var pattern4 = /$$IMG alt="([^"]*)"/g; |
| 27 | + | |
| 28 | - | var pattern9 = /\$\$\/IMG$$([A-Za-z0-9])/g; |
| 28 | + | // Create the floating button |
| 29 | - | replacedText = replacedText.replace(pattern4, '\n[IMG alt="$1"] '); |
| 29 | + | const bbButton = document.createElement('button');
|
| 30 | - | replacedText = replacedText.replace(pattern9, '[/IMG] $1'); |
| 30 | + | bbButton.textContent = 'BB'; |
| 31 | bbButton.title = 'Activate BBCode Comment & Video Tool'; | |
| 32 | - | var pattern5 = /\n/g; |
| 32 | + | bbButton.style.cssText = ` |
| 33 | - | var pattern10 = /^\n+|\n{2,}|\n+$/g;
|
| 33 | + | position: fixed; |
| 34 | - | replacedText = replacedText.replace(pattern5, '\n'); |
| 34 | + | bottom: 10px; |
| 35 | - | replacedText = replacedText.replace(pattern10, '\n'); |
| 35 | + | left: 10px; |
| 36 | z-index: 9999; | |
| 37 | - | var pattern7 = /$$\/?FONT(=.*?)?$$/g; |
| 37 | + | background-color: #4CAF50; |
| 38 | - | var pattern8 = /$$\/?COLOR(=.*?)?$$/g; |
| 38 | + | color: white; |
| 39 | - | replacedText = replacedText.replace(pattern7, ''); |
| 39 | + | border: none; |
| 40 | - | replacedText = replacedText.replace(pattern8, ''); |
| 40 | + | border-radius: 50%; |
| 41 | width: 40px; | |
| 42 | - | replacedText = replacedText.replace(/\$\$B\$\$(.*?)\$\$\/B\$\$/g, '[B]$1[/B]'); |
| 42 | + | height: 40px; |
| 43 | - | replacedText = replacedText.replace(/\$\$I\$\$(.*?)\$\$\/I\$\$/g, '[I]$1[/I]'); |
| 43 | + | font-size: 16px; |
| 44 | cursor: pointer; | |
| 45 | - | var mathPattern = /\$\$(.*?)\$\$/g; |
| 45 | + | box-shadow: 0 2px 5px rgba(0,0,0,0.3); |
| 46 | - | replacedText = replacedText.replace(mathPattern, '\$$$1\$$'); |
| 46 | + | `; |
| 47 | document.body.appendChild(bbButton); | |
| 48 | - | var paragraphPattern = /([^\n])\n([A-Z0-9])/g; |
| 48 | + | |
| 49 | - | replacedText = replacedText.replace(paragraphPattern, '$1\n\n$2'); |
| 49 | + | // BBCode tool logic |
| 50 | function activateBBCodeTool() {
| |
| 51 | - | var quotePattern = /$$QUOTE$$(?:.*?\n)*?(.*?)$$\/QUOTE$$/gs; |
| 51 | + | console.log('[Invidious BBCode Tool] Activated - Click copy buttons on comments or video metadata');
|
| 52 | - | replacedText = replacedText.replace(quotePattern, '[QUOTE][SIZE=4]$1[/SIZE][/QUOTE]'); |
| 52 | + | |
| 53 | function htmlToBBCode(html) {
| |
| 54 | - | const emptyListItemsPattern = /\[LIST\](\n\[\*\]\s*)+\[\/LIST\]/gi; |
| 54 | + | return html |
| 55 | - | replacedText = replacedText.replace(emptyListItemsPattern, ''); |
| 55 | + | .replace(/<a href="([^"]+)"[^>]*>([^<]+)<\/a>/g, (m, url, txt) => `[icode]${txt}[/icode]`)
|
| 56 | - | const emptyListBlockPattern = /\[LIST\]\s*\[\/LIST\]/gi; |
| 56 | + | .replace(/<b>([^<]+)<\/b>/g, '[b]$1[/b]') |
| 57 | - | replacedText = replacedText.replace(emptyListBlockPattern, ''); |
| 57 | + | .replace(/<i>([^<]+)<\/i>/g, '[i]$1[/i]') |
| 58 | .replace(/<s>([^<]+)<\/s>/g, '[s]$1[/s]') | |
| 59 | - | replacedText = replacedText.replace(/\[B\]\[\/B\]/g, ''); |
| 59 | + | .replace(/<\/?p[^>]*>/g, '\n') |
| 60 | - | replacedText = replacedText.replace(/\[I\]\[\/I\]/g, ''); |
| 60 | + | .trim(); |
| 61 | } | |
| 62 | - | textarea.value = replacedText; |
| 62 | + | |
| 63 | - | }); |
| 63 | + | function extractTextWithLinks(html) {
|
| 64 | const parser = new DOMParser(); | |
| 65 | const doc = parser.parseFromString(html, 'text/html'); | |
| 66 | let text = doc.body.innerText.trim(); | |
| 67 | const links = Array.from(doc.body.querySelectorAll('a'));
| |
| 68 | ||
| 69 | let reconstructedText = text; | |
| 70 | links.forEach(link => {
| |
| 71 | const linkText = link.textContent.trim(); | |
| 72 | // Check if the link text is a YouTube URL | |
| 73 | const isYouTubeUrl = linkText.match(/^(https?:\/\/)?(www\.)?youtube\.com/); | |
| 74 | let urlToUse = link.href; | |
| 75 | if (isYouTubeUrl) {
| |
| 76 | // Ensure https:// protocol for YouTube URLs | |
| 77 | urlToUse = linkText.match(/^https?:\/\//) ? linkText : `https://${linkText}`;
| |
| 78 | } | |
| 79 | reconstructedText = reconstructedText.replace(linkText, `[icode]${urlToUse}[/icode]`);
| |
| 80 | }); | |
| 81 | return reconstructedText.trim(); | |
| 82 | } | |
| 83 | ||
| 84 | function getParentComments(commentElement, maxDepth = 10) {
| |
| 85 | const parents = []; | |
| 86 | const seenIds = new Set(); | |
| 87 | let currentComment = commentElement.closest('.pure-g');
| |
| 88 | ||
| 89 | while (currentComment && parents.length < maxDepth) {
| |
| 90 | const parentContainer = currentComment.parentElement.closest('#replies');
| |
| 91 | if (!parentContainer) break; | |
| 92 | ||
| 93 | const parentComment = parentContainer.parentElement.closest('.pure-g');
| |
| 94 | if (!parentComment) break; | |
| 95 | ||
| 96 | const commentId = parentComment.querySelector('a[title="YouTube comment permalink"]')?.href || '';
| |
| 97 | if (seenIds.has(commentId)) break; | |
| 98 | seenIds.add(commentId); | |
| 99 | ||
| 100 | const username = parentComment.querySelector('a[href^="/channel/"]')?.textContent.trim() || 'Unknown';
| |
| 101 | const datetime = parentComment.querySelector('span[title]')?.getAttribute('title') || 'Unknown';
| |
| 102 | const contentHtml = parentComment.querySelector('p[style="white-space:pre-wrap"]');
| |
| 103 | const permalink = parentComment.querySelector('a[title="YouTube comment permalink"]')?.href || window.location.href;
| |
| 104 | const likes = parentComment.querySelector('.icon.ion-ios-thumbs-up')?.nextSibling.textContent.trim() || '0';
| |
| 105 | const contentText = contentHtml ? extractTextWithLinks(contentHtml.innerHTML) : 'No content'; | |
| 106 | const contentBBCode = htmlToBBCode(contentText); | |
| 107 | ||
| 108 | parents.unshift({
| |
| 109 | permalink, | |
| 110 | spoilerContent: `\n${username} commented on ${datetime} | Likes: ${likes}\n\n${contentBBCode}\n`
| |
| 111 | }); | |
| 112 | currentComment = parentComment; | |
| 113 | } | |
| 114 | return parents; | |
| 115 | } | |
| 116 | ||
| 117 | function createDepthDropdown(maxDepth, callback, button) {
| |
| 118 | const existingDropdown = document.querySelector('.depth-dropdown');
| |
| 119 | if (existingDropdown) existingDropdown.remove(); | |
| 120 | ||
| 121 | const dropdown = document.createElement('select');
| |
| 122 | dropdown.className = 'depth-dropdown'; | |
| 123 | dropdown.style.position = 'absolute'; | |
| 124 | dropdown.style.marginLeft = '5px'; | |
| 125 | dropdown.style.padding = '2px'; | |
| 126 | dropdown.style.fontSize = '12px'; | |
| 127 | ||
| 128 | for (let i = 0; i <= maxDepth; i++) {
| |
| 129 | const option = document.createElement('option');
| |
| 130 | option.value = i; | |
| 131 | option.textContent = i === 0 ? 'Only this' : `${i} parent${i > 1 ? 's' : ''}`;
| |
| 132 | dropdown.appendChild(option); | |
| 133 | } | |
| 134 | ||
| 135 | dropdown.addEventListener('change', () => {
| |
| 136 | callback(parseInt(dropdown.value)); | |
| 137 | dropdown.remove(); | |
| 138 | }); | |
| 139 | ||
| 140 | dropdown.addEventListener('blur', () => dropdown.remove());
| |
| 141 | button.insertAdjacentElement('afterend', dropdown);
| |
| 142 | dropdown.focus(); | |
| 143 | } | |
| 144 | ||
| 145 | function createCopyButtons(element) {
| |
| 146 | const existingButtons = element.querySelectorAll('.copy-btn, .nested-btn, .video-copy-btn');
| |
| 147 | existingButtons.forEach(btn => btn.remove()); | |
| 148 | ||
| 149 | // Comment Copy Button | |
| 150 | const copyButton = document.createElement('button');
| |
| 151 | copyButton.textContent = '📋'; | |
| 152 | copyButton.title = 'Copy comment to BBCode'; | |
| 153 | copyButton.className = 'copy-btn'; | |
| 154 | copyButton.style.cssText = 'cursor:pointer;background:green;color:white;margin-left:5px;padding:2px 6px;border:none;border-radius:4px;'; | |
| 155 | ||
| 156 | // Nested Comment Button | |
| 157 | const nestedButton = document.createElement('button');
| |
| 158 | nestedButton.textContent = '📋'; | |
| 159 | nestedButton.title = 'Copy comment with nested parents'; | |
| 160 | nestedButton.className = 'nested-btn'; | |
| 161 | nestedButton.style.cssText = 'cursor:pointer;background:blue;color:white;margin-left:5px;padding:2px 6px;border:none;border-radius:4px;'; | |
| 162 | ||
| 163 | // Video Metadata Copy Button | |
| 164 | const videoCopyButton = document.createElement('button');
| |
| 165 | videoCopyButton.textContent = '📋'; | |
| 166 | videoCopyButton.title = 'Copy video metadata to BBCode'; | |
| 167 | videoCopyButton.className = 'video-copy-btn'; | |
| 168 | videoCopyButton.style.cssText = 'cursor:pointer;background:purple;color:white;margin-top:5px;padding:2px 6px;border:none;border-radius:4px;z-index:1000;display:block;width:fit-content;'; | |
| 169 | ||
| 170 | // Handle Comment Elements | |
| 171 | if (element.classList.contains('pure-g') && element.querySelector('a[href^="/channel/"]')) {
| |
| 172 | const permalink = element.querySelector('a[title="YouTube comment permalink"]')?.href || window.location.href;
| |
| 173 | const username = element.querySelector('a[href^="/channel/"]')?.textContent.trim() || 'Unknown';
| |
| 174 | const datetime = element.querySelector('span[title]')?.getAttribute('title') || 'Unknown';
| |
| 175 | const contentHtml = element.querySelector('p[style="white-space:pre-wrap"]');
| |
| 176 | const likes = element.querySelector('.icon.ion-ios-thumbs-up')?.nextSibling.textContent.trim() || '0';
| |
| 177 | ||
| 178 | if (!contentHtml) {
| |
| 179 | console.log('[Invidious BBCode Tool] No content found for comment');
| |
| 180 | return; | |
| 181 | } | |
| 182 | ||
| 183 | const contentText = extractTextWithLinks(contentHtml.innerHTML); | |
| 184 | const contentBBCode = htmlToBBCode(contentText); | |
| 185 | const spoilerContent = `\n${username} commented on ${datetime} | Likes: ${likes}\n\n${contentBBCode}\n`;
| |
| 186 | const fullContent = `[icode]${permalink}[/icode]\n${spoilerContent}`;
| |
| 187 | ||
| 188 | copyButton.addEventListener('click', (e) => {
| |
| 189 | e.preventDefault(); | |
| 190 | e.stopPropagation(); | |
| 191 | navigator.clipboard.writeText(fullContent) | |
| 192 | .then(() => showToast('Comment copied!'))
| |
| 193 | .catch(err => console.error('[Invidious BBCode Tool] Copy failed:', err));
| |
| 194 | }); | |
| 195 | ||
| 196 | nestedButton.addEventListener('click', (e) => {
| |
| 197 | e.preventDefault(); | |
| 198 | e.stopPropagation(); | |
| 199 | const parents = getParentComments(element); | |
| 200 | const maxDepth = parents.length; | |
| 201 | ||
| 202 | createDepthDropdown(maxDepth, (depth) => {
| |
| 203 | const items = [ | |
| 204 | ...parents.slice(0, depth), | |
| 205 | { permalink, spoilerContent }
| |
| 206 | ]; | |
| 207 | ||
| 208 | const payload = items | |
| 209 | .map((item, index) => {
| |
| 210 | const indent = '│ '.repeat(index); | |
| 211 | const commentLines = `[icode]${item.permalink}[/icode]\n${item.spoilerContent}`.split('\n');
| |
| 212 | return commentLines.map(line => `${indent}${line}`).join('\n');
| |
| 213 | }) | |
| 214 | .join('\n\n');
| |
| 215 | ||
| 216 | navigator.clipboard.writeText(payload) | |
| 217 | .then(() => showToast(`Comment with ${depth} parent${depth === 1 ? '' : 's'} copied!`))
| |
| 218 | .catch(err => console.error('[Invidious BBCode Tool] Nested copy failed:', err));
| |
| 219 | }, nestedButton); | |
| 220 | }); | |
| 221 | ||
| 222 | const target = element.querySelector('.pure-u-20-24, .pure-u-md-22-24') || element;
| |
| 223 | target.appendChild(copyButton); | |
| 224 | target.appendChild(nestedButton); | |
| 225 | } | |
| 226 | ||
| 227 | // Handle Video Metadata Element | |
| 228 | if (element.classList.contains('h-box') && element.classList.contains('highlight') && element.querySelector('h1')) {
| |
| 229 | console.log('[Invidious BBCode Tool] Video metadata element detected:', element);
| |
| 230 | ||
| 231 | const title = element.querySelector('h1')?.textContent.trim() || 'Untitled';
| |
| 232 | const youtubeUrl = document.querySelector('#link-yt-watch')?.href || window.location.href;
| |
| 233 | const channelName = document.querySelector('#channel-name')?.textContent.trim() || 'Unknown';
| |
| 234 | const channelUrl = document.querySelector('a[href^="/channel/"]')?.href || '';
| |
| 235 | const views = document.querySelector('#views')?.textContent.trim().replace(/\sViews/, '') || '0';
| |
| 236 | const likes = document.querySelector('#likes')?.textContent.trim() || '0';
| |
| 237 | const publishedDate = document.querySelector('#published-date b')?.textContent.trim() || 'Unknown';
| |
| 238 | const descriptionHtml = document.querySelector('#descriptionWrapper')?.innerHTML || 'No description';
| |
| 239 | const descriptionText = extractTextWithLinks(descriptionHtml); | |
| 240 | const descriptionBBCode = htmlToBBCode(descriptionText); | |
| 241 | ||
| 242 | const videoContent = `[b]${title}[/b]\n` +
| |
| 243 | `[icode]${youtubeUrl}[/icode]\n\n` +
| |
| 244 | `[i]Channel:[/i] [icode]${channelUrl}[/icode] ${channelName}\n` +
| |
| 245 | `[i]Stats:[/i] Views: ${views} | Likes: ${likes}\n` +
| |
| 246 | `[i]Published:[/i] ${publishedDate}\n\n` +
| |
| 247 | `\n${descriptionBBCode}\n`;
| |
| 248 | ||
| 249 | videoCopyButton.addEventListener('click', (e) => {
| |
| 250 | e.preventDefault(); | |
| 251 | e.stopPropagation(); | |
| 252 | navigator.clipboard.writeText(videoContent) | |
| 253 | .then(() => showToast('Video metadata copied!'))
| |
| 254 | .catch(err => console.error('[Invidious BBCode Tool] Video copy failed:', err));
| |
| 255 | }); | |
| 256 | ||
| 257 | const target = document.querySelector('#subscribe');
| |
| 258 | if (target) {
| |
| 259 | console.log('[Invidious BBCode Tool] Attaching video copy button under #subscribe:', target);
| |
| 260 | target.insertAdjacentElement('afterend', videoCopyButton);
| |
| 261 | } else {
| |
| 262 | console.log('[Invidious BBCode Tool] #subscribe not found, falling back to element:', element);
| |
| 263 | element.appendChild(videoCopyButton); // Fallback | |
| 264 | } | |
| 265 | } | |
| 266 | } | |
| 267 | ||
| 268 | function showToast(message) {
| |
| 269 | const toast = document.createElement('div');
| |
| 270 | toast.textContent = message; | |
| 271 | toast.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:10px;border-radius:5px;background:#fff;color:#000;z-index:1000;box-shadow:0 0 10px rgba(0,0,0,0.2);'; | |
| 272 | document.body.appendChild(toast); | |
| 273 | setTimeout(() => toast.remove(), 2000); | |
| 274 | } | |
| 275 | ||
| 276 | // Use MutationObserver to handle dynamic DOM changes | |
| 277 | const observer = new MutationObserver((mutations) => {
| |
| 278 | document.querySelectorAll('.pure-g').forEach(element => {
| |
| 279 | if (element.querySelector('a[href^="/channel/"]') && !element.querySelector('.copy-btn')) {
| |
| 280 | console.log('[Invidious BBCode Tool] Processing comment element:', element);
| |
| 281 | createCopyButtons(element); | |
| 282 | } | |
| 283 | }); | |
| 284 | ||
| 285 | document.querySelectorAll('.h-box.highlight').forEach(element => {
| |
| 286 | if (element.querySelector('h1') && !element.querySelector('.video-copy-btn')) {
| |
| 287 | console.log('[Invidious BBCode Tool] Processing video element:', element);
| |
| 288 | createCopyButtons(element); | |
| 289 | } | |
| 290 | }); | |
| 291 | }); | |
| 292 | ||
| 293 | observer.observe(document.body, { childList: true, subtree: true });
| |
| 294 | ||
| 295 | // Initial check in case elements are already loaded | |
| 296 | document.querySelectorAll('.pure-g').forEach(element => {
| |
| 297 | if (element.querySelector('a[href^="/channel/"]')) {
| |
| 298 | console.log('[Invidious BBCode Tool] Initial processing comment element:', element);
| |
| 299 | createCopyButtons(element); | |
| 300 | } | |
| 301 | }); | |
| 302 | ||
| 303 | document.querySelectorAll('.h-box.highlight').forEach(element => {
| |
| 304 | if (element.querySelector('h1')) {
| |
| 305 | console.log('[Invidious BBCode Tool] Initial processing video element:', element);
| |
| 306 | createCopyButtons(element); | |
| 307 | } | |
| 308 | }); | |
| 309 | } | |
| 310 | ||
| 311 | // Toggle the BBCode tool on button click | |
| 312 | let isActive = false; | |
| 313 | bbButton.addEventListener('click', () => {
| |
| 314 | if (!isActive) {
| |
| 315 | activateBBCodeTool(); | |
| 316 | bbButton.style.backgroundColor = '#f44336'; // Red when active | |
| 317 | bbButton.title = 'Deactivate BBCode Comment & Video Tool'; | |
| 318 | isActive = true; | |
| 319 | } else {
| |
| 320 | // Remove all copy buttons and reset | |
| 321 | document.querySelectorAll('.copy-btn, .nested-btn, .video-copy-btn, .depth-dropdown').forEach(el => el.remove());
| |
| 322 | bbButton.style.backgroundColor = '#4CAF50'; // Green when inactive | |
| 323 | bbButton.title = 'Activate BBCode Comment & Video Tool'; | |
| 324 | isActive = false; | |
| 325 | } | |
| 326 | }); | |
| 327 | })(); |