View difference between Paste ID: mu1LVzTK and 3TVJGgbQ
SHOW: | | - or go back to the newest paste.
1-
// ==UserScript==
1+
javascript:(function(){
2-
// @name         Invidious BBCode Floating Button
2+
  const MAX_IMAGES_PER_POST = 10;
3-
// @namespace    http://tampermonkey.net/
3+
  function replaceDomain(e){return e.replace(/^(https?:\/\/)([^\/]+)/,"$1bsky.app")}
4-
// @version      0.7
4+
  function cleanText(e){
5-
// @description  Adds a floating BB button to trigger BBCode comment and video metadata copying on Invidious instances
5+
    return e ? e.replace(/<a href="([^"]+)"[^>]*>([^<]+)<\/a>/g, (match, url, text) => (url.startsWith('/') ? `[U][URL=https://bsky.app${url}]${text}[/URL][/U]` : `[U][URL=${url}]${text}[/URL][/U]`)).replace(/<br\s*\/?>/g, "\n").replace(/<[^>]+>/g, "").trim() : ""
6-
// @author       You
6+
  }
7-
// @match        *://yewtu.be/*
7+
  function extractMedia(element, excludeSelectors = []) { 
8-
// @match        *://inv.nadeko.net/*
8+
    let images = Array.from(element.querySelectorAll('img[src*="cdn.bsky.app"]'));
9-
// @match        *://invidious.nerdvpn.de/*
9+
    
10-
// @match        *://iv.ggtyler.dev/*
10+
    
11-
// @match        *://invidious.jing.rocks/*
11+
    if (excludeSelectors.length > 0) {
12-
// @match        *://invidious.perennialte.ch/*
12+
        excludeSelectors.forEach(selector => {
13-
// @match        *://invidious.reallyaweso.me/*
13+
            element.querySelectorAll(selector).forEach(excludedContainer => {
14-
// @match        *://invidious.privacyredirect.com/*
14+
                images = images.filter(img => !excludedContainer.contains(img));
15-
// @match        *://invidious.einfachzocken.eu/*
15+
16-
// @match        *://inv.tux.pizza/*
16+
17-
// @match        *://iv.nboeck.de/*
17+
18-
// @match        *://iv.nowhere.moe/*
18+
    
19-
// @match        *://invidious.adminforge.de/*
19+
    images = images.map(e => e.src).filter(e => !e.includes("/avatar_thumbnail/"));
20-
// @match        *://invidious.yourdevice.ch/*
20+
    
21-
// @match        *://invidious.privacydev.net/*
21+
    let videoSourcesQuery = "video source[src], video[src]"; 
22-
// @grant        none
22+
    
23-
// ==/UserScript==
23+
    if (excludeSelectors.length > 0) {
24
        excludeSelectors.forEach(selector => {
25-
(function() {
25+
             
26-
    'use strict';
26+
             
27
             
28-
    // Create the floating button
28+
29-
    const bbButton = document.createElement('button');
29+
30-
    bbButton.textContent = 'BB';
30+
    const videoSources = new Set([...Array.from(element.querySelectorAll(videoSourcesQuery)).map(e => e.src)]);
31-
    bbButton.title = 'Activate BBCode Comment & Video Tool';
31+
    
32-
    bbButton.style.cssText = `
32+
    return {images, videos: Array.from(videoSources)}
33-
        position: fixed;
33+
  }
34-
        bottom: 10px;
34+
  function showToast(e){
35-
        left: 10px;
35+
    const t = document.createElement("div"); t.style.cssText = "position:fixed;bottom:20px;left:20px;padding:10px 20px;background:#333;color:#fff;border-radius:4px;z-index:10000;font-family:Arial,sans-serif;font-size:14px;opacity=1;transition:opacity 1s;box-shadow:0 2px 5px rgba(0,0,0,0.3)"; t.innerText = e; document.body.appendChild(t); setTimeout(() => {t.style.opacity = "0"; setTimeout(() => document.body.removeChild(t), 1e3)}, 2e3)
36-
        z-index: 9999;
36+
  }
37-
        background-color: #4CAF50;
37+
  function createPostCountPrompt(e){
38-
        color: white;
38+
    const t = document.createElement("div"); t.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:9999;display:flex;justify-content:center;align-items:center"; const r = document.createElement("div"); r.style.cssText = "background:white;padding:20px;border-radius:8px;box-shadow:0 0 10px rgba(0,0,0,0.3);text-align:center"; const n = document.createElement("input"); n.type = "text"; n.value = "12"; n.style.cssText = "font-size:30px;width:100px;text-align:center;margin-bottom:20px"; const o = document.createElement("div"); o.style.cssText = "display:grid;grid-template-columns:repeat(3,1fr);gap:10px"; "1234567890".split("").forEach(e => {const t = document.createElement("button"); t.textContent = e; t.style.cssText = "font-size:20px;padding:10px;cursor:pointer"; t.onclick = () => n.value += e; o.appendChild(t)}); const p = document.createElement("div");p.style.cssText="display:flex;gap:10px;margin-bottom:20px";[1,2,10,25,35,50].forEach(num=>{const b=document.createElement("button");b.textContent=num;b.style.cssText="font-size:16px;padding:8px;cursor:pointer";b.onclick=()=>n.value=num;p.appendChild(b)}); const s = document.createElement("button"); s.textContent = "Clear"; s.style.cssText = "font-size:18px;margin-top:10px;padding:10px 20px;cursor:pointer"; s.onclick = () => n.value = ""; const a = document.createElement("button"); a.textContent = "Set Number of Posts"; a.style.cssText = "font-size:18px;margin-top:20px;padding:10px 20px;cursor:pointer"; a.onclick = () => {const o = parseInt(n.value, 10); isNaN(o) || o <= 0 ? showToast("Please enter a valid number.") : (document.body.removeChild(t), e(o))}; r.append(n,p,o, s, a); t.appendChild(r); document.body.appendChild(t)
39-
        border: none;
39+
  }
40-
        border-radius: 50%;
40+
  async function getEmbedUrl(threadData){ 
41-
        width: 40px;
41+
    let url = null; const post = threadData?.thread?.post;
42-
        height: 40px;
42+
    if (post?.embed?.$type === "app.bsky.embed.video#view" && post.embed.playlist) {url = post.embed.playlist;}
43-
        font-size: 16px;
43+
    else if (post?.record?.embed?.$type === "app.bsky.embed.video" && post.author?.did && post.record.embed.video?.ref?.$link) {
44-
        cursor: pointer;
44+
      url = constructVideoUrl(post.author.did, post.record.embed.video);
45-
        box-shadow: 0 2px 5px rgba(0,0,0,0.3);
45+
46-
    `;
46+
    return url
47-
    document.body.appendChild(bbButton);
47+
  }
48
  function constructVideoUrl(did, videoEmbed){
49-
    // BBCode tool logic
49+
    if (videoEmbed.mimeType === "video/webm") return `https://bsky.social/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(videoEmbed.ref.$link)}`;
50-
    function activateBBCodeTool() {
50+
    return `https://video.bsky.app/watch/${encodeURIComponent(did)}/${encodeURIComponent(videoEmbed.ref.$link)}/playlist.m3u8`
51-
        console.log('[Invidious BBCode Tool] Activated - Click copy buttons on comments or video metadata');
51+
  }
52
  function cleanHandle(handle) {
53-
        function htmlToBBCode(html) {
53+
    if (!handle) return "Unknown";
54-
            return html
54+
    let cleaned = handle.replace(/[\u202a-\u202f\u00AD\u200B-\u200D\uFEFF\s]/g, "");
55-
                .replace(/<a href="([^"]+)"[^>]*>([^<]+)<\/a>/g, (m, url, txt) => `[icode]${txt}[/icode]`)
55+
    cleaned = cleaned.trim();
56-
                .replace(/<b>([^<]+)<\/b>/g, '[b]$1[/b]')
56+
    cleaned = cleaned.replace(/^@+/, "");
57-
                .replace(/<i>([^<]+)<\/i>/g, '[i]$1[/i]')
57+
    return cleaned.trim();
58-
                .replace(/<s>([^<]+)<\/s>/g, '[s]$1[/s]')
58+
  }
59-
                .replace(/<\/?p[^>]*>/g, '\n')
59+
60-
                .trim();
60+
  async function extractPost(postElement, isActivePost = false){
61
    if (!(postElement instanceof Element)) return console.error("Invalid post element:", postElement), {text: "", author: "", url: "", threadData: null, quotedUrl: null};
62
    
63-
        function extractTextWithLinks(html) {
63+
    
64-
            const parser = new DOMParser();
64+
    let thisPostsAuthorHandleFromTestId = postElement.getAttribute("data-testid")?.match(/postThreadItem-by-(.+)/)?.[1];
65-
            const doc = parser.parseFromString(html, 'text/html');
65+
    let thisPostsUrl = null;
66-
            let text = doc.body.innerText.trim();
66+
    let thisPostsId = null;
67-
            const links = Array.from(doc.body.querySelectorAll('a'));
67+
    const quoteEmbedSelector = '[role="link"][aria-label*="Post by"]'; 
68
69-
            let reconstructedText = text;
69+
    if (thisPostsAuthorHandleFromTestId) {
70-
            links.forEach(link => {
70+
      thisPostsAuthorHandleFromTestId = cleanHandle(thisPostsAuthorHandleFromTestId);
71-
                const linkText = link.textContent.trim();
71+
      
72-
                // Check if the link text is a YouTube URL
72+
      
73-
                const isYouTubeUrl = linkText.match(/^(https?:\/\/)?(www\.)?youtube\.com/);
73+
      const timestampLinks = Array.from(postElement.querySelectorAll('[data-testid="postTimestamp"] a[href*="/post/"], a[href*="/post/"]'));
74-
                let urlToUse = link.href;
74+
      let foundLinkForThisPost = null;
75-
                if (isYouTubeUrl) {
75+
      for (const link of timestampLinks) {
76-
                    // Ensure https:// protocol for YouTube URLs
76+
          if (!link.closest(quoteEmbedSelector)) { 
77-
                    urlToUse = linkText.match(/^https?:\/\//) ? linkText : `https://${linkText}`;
77+
              foundLinkForThisPost = link;
78-
                }
78+
              break;
79-
                reconstructedText = reconstructedText.replace(linkText, `[icode]${urlToUse}[/icode]`);
79+
          }
80
      }
81-
            return reconstructedText.trim();
81+
      if (!foundLinkForThisPost && timestampLinks.length > 0 && !postElement.querySelector(quoteEmbedSelector)) {
82
          
83
          foundLinkForThisPost = timestampLinks[0];
84-
        function getParentComments(commentElement, maxDepth = 10) {
84+
      }
85-
            const parents = [];
85+
86-
            const seenIds = new Set();
86+
87-
            let currentComment = commentElement.closest('.pure-g');
87+
      if (foundLinkForThisPost) {
88
          thisPostsId = foundLinkForThisPost.href.split('/').pop().split('?')[0];
89-
            while (currentComment && parents.length < maxDepth) {
89+
          thisPostsUrl = `https://bsky.app/profile/${thisPostsAuthorHandleFromTestId}/post/${thisPostsId}`;
90-
                const parentContainer = currentComment.parentElement.closest('#replies');
90+
          console.log(`extractPost: DOM URL for self (postElement): ${thisPostsUrl}`);
91-
                if (!parentContainer) break;
91+
      } else {
92
          console.warn(`extractPost: Could not find reliable DOM URL for self (postElement). TestID handle: ${thisPostsAuthorHandleFromTestId}`);
93-
                const parentComment = parentContainer.parentElement.closest('.pure-g');
93+
      }
94-
                if (!parentComment) break;
94+
95
96-
                const commentId = parentComment.querySelector('a[title="YouTube comment permalink"]')?.href || '';
96+
    
97-
                if (seenIds.has(commentId)) break;
97+
    let threadDataForThisPost = null;
98-
                seenIds.add(commentId);
98+
    if (thisPostsAuthorHandleFromTestId && (thisPostsId || isActivePost)) {
99
      
100-
                const username = parentComment.querySelector('a[href^="/channel/"]')?.textContent.trim() || 'Unknown';
100+
      const postIdForApi = thisPostsId || (isActivePost ? window.location.href.split('/').pop().split('?')[0] : null);
101-
                const datetime = parentComment.querySelector('span[title]')?.getAttribute('title') || 'Unknown';
101+
      if (postIdForApi) {
102-
                const contentHtml = parentComment.querySelector('p[style="white-space:pre-wrap"]');
102+
        const apiUri = `at://${thisPostsAuthorHandleFromTestId}/app.bsky.feed.post/${postIdForApi}`;
103-
                const permalink = parentComment.querySelector('a[title="YouTube comment permalink"]')?.href || window.location.href;
103+
        const apiUrl = `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${encodeURIComponent(apiUri)}&depth=0`;
104-
                const likes = parentComment.querySelector('.icon.ion-ios-thumbs-up')?.nextSibling.textContent.trim() || '0';
104+
        try {
105-
                const contentText = contentHtml ? extractTextWithLinks(contentHtml.innerHTML) : 'No content';
105+
          const res = await fetch(apiUrl);
106-
                const contentBBCode = htmlToBBCode(contentText);
106+
          threadDataForThisPost = res.ok ? await res.json() : null;
107
          if (threadDataForThisPost?.thread?.post?.uri) {
108-
                parents.unshift({
108+
            
109-
                    permalink,
109+
            const apiUriParts = threadDataForThisPost.thread.post.uri.split('/');
110-
                    spoilerContent: `\n${username} commented on ${datetime} | Likes: ${likes}\n\n${contentBBCode}\n`
110+
            thisPostsId = apiUriParts.pop();
111
            const apiAuthorHandle = cleanHandle(threadDataForThisPost.thread.post.author.handle); 
112-
                currentComment = parentComment;
112+
            thisPostsAuthorHandleFromTestId = apiAuthorHandle; 
113
            thisPostsUrl = `https://bsky.app/profile/${apiAuthorHandle}/post/${thisPostsId}`;
114-
            return parents;
114+
            console.log(`extractPost: API OVERRIDE/CONFIRMED URL for self: ${thisPostsUrl}`);
115
          } else if (threadDataForThisPost) {
116
            console.log("extractPost: API data fetched but no post.uri for self:", thisPostsUrl, threadDataForThisPost);
117-
        function createDepthDropdown(maxDepth, callback, button) {
117+
          } else {
118-
            const existingDropdown = document.querySelector('.depth-dropdown');
118+
             console.warn("extractPost: No API data or error for self:", thisPostsUrl || `at://${thisPostsAuthorHandleFromTestId}/app.bsky.feed.post/${postIdForApi}`);
119-
            if (existingDropdown) existingDropdown.remove();
119+
          }
120
        } catch (err) {
121-
            const dropdown = document.createElement('select');
121+
          console.warn("extractPost: API fetch failed for self:", thisPostsUrl, err);
122-
            dropdown.className = 'depth-dropdown';
122+
123-
            dropdown.style.position = 'absolute';
123+
      }
124-
            dropdown.style.marginLeft = '5px';
124+
125-
            dropdown.style.padding = '2px';
125+
     if (!thisPostsUrl && isActivePost && thisPostsAuthorHandleFromTestId) { 
126-
            dropdown.style.fontSize = '12px';
126+
        thisPostsUrl = `https://bsky.app/profile/${thisPostsAuthorHandleFromTestId}/post/${window.location.href.split('/').pop().split('?')[0]}`;
127
        console.log("extractPost: Fallback URL for active post (no API/DOM ID):", thisPostsUrl);
128-
            for (let i = 0; i <= maxDepth; i++) {
128+
129-
                const option = document.createElement('option');
129+
130-
                option.value = i;
130+
131-
                option.textContent = i === 0 ? 'Only this' : `${i} parent${i > 1 ? 's' : ''}`;
131+
    
132-
                dropdown.appendChild(option);
132+
    const textContentElem = postElement.querySelector('div[data-word-wrap="1"]:not([data-testid="quoteEmbed"]) div[data-word-wrap="1"], [data-testid="postText"]:not([data-testid="quoteEmbed"]) [data-testid="postText"]'); 
133
    
134
    const mainTextContentElement = postElement.querySelector(':scope > div > [data-testid="postText"], :scope > div > div > [data-testid="postText"], :scope > div > div > div[data-word-wrap="1"]'); 
135-
            dropdown.addEventListener('change', () => {
135+
    
136-
                callback(parseInt(dropdown.value));
136+
    let postOutputText = "";
137-
                dropdown.remove();
137+
    if (mainTextContentElement) {
138
        
139
        const clonedTextContainer = mainTextContentElement.cloneNode(true);
140-
            dropdown.addEventListener('blur', () => dropdown.remove());
140+
        clonedTextContainer.querySelector(quoteEmbedSelector)?.remove(); 
141-
            button.insertAdjacentElement('afterend', dropdown);
141+
        postOutputText = cleanText(clonedTextContainer.innerHTML || clonedTextContainer.textContent);
142-
            dropdown.focus();
142+
    } else { 
143
        const tempDiv = document.createElement('div');
144
        tempDiv.innerHTML = postElement.innerHTML;
145-
        function createCopyButtons(element) {
145+
        tempDiv.querySelector(quoteEmbedSelector)?.remove(); 
146-
            const existingButtons = element.querySelectorAll('.copy-btn, .nested-btn, .video-copy-btn');
146+
        const mainTextOnlyElem = tempDiv.querySelector('div[data-word-wrap="1"], [data-testid="postText"]');
147-
            existingButtons.forEach(btn => btn.remove());
147+
        postOutputText = mainTextOnlyElem ? cleanText(mainTextOnlyElem.innerHTML || mainTextOnlyElem.textContent) : "";
148
    }
149-
            // Comment Copy Button
149+
150-
            const copyButton = document.createElement('button');
150+
151-
            copyButton.textContent = 'πŸ“‹';
151+
    
152-
            copyButton.title = 'Copy comment to BBCode';
152+
    const mediaForThisPost = extractMedia(postElement, [quoteEmbedSelector]);
153-
            copyButton.className = 'copy-btn';
153+
    mediaForThisPost.images.slice(0, MAX_IMAGES_PER_POST).forEach(imgUrl => postOutputText += `\n[img]${imgUrl}[/img]`);
154-
            copyButton.style.cssText = 'cursor:pointer;background:green;color:white;margin-left:5px;padding:2px 6px;border:none;border-radius:4px;';
154+
    if (threadDataForThisPost) { 
155
      const videoUrl = await getEmbedUrl(threadDataForThisPost);
156-
            // Nested Comment Button
156+
      if (videoUrl) postOutputText += `\n[U][URL]${videoUrl}[/URL][/U]`;
157-
            const nestedButton = document.createElement('button');
157+
158-
            nestedButton.textContent = 'πŸ“‹';
158+
159-
            nestedButton.title = 'Copy comment with nested parents';
159+
160-
            nestedButton.className = 'nested-btn';
160+
    
161-
            nestedButton.style.cssText = 'cursor:pointer;background:blue;color:white;margin-left:5px;padding:2px 6px;border:none;border-radius:4px;';
161+
    let urlOfQuotedPostByThis = null; 
162
    const quoteEmbedElement = postElement.querySelector(quoteEmbedSelector);
163-
            // Video Metadata Copy Button
163+
164-
            const videoCopyButton = document.createElement('button');
164+
    
165-
            videoCopyButton.textContent = 'πŸ“‹';
165+
    const apiQuoteRecordInfo = threadDataForThisPost?.thread?.post?.record?.embed?.record;
166-
            videoCopyButton.title = 'Copy video metadata to BBCode';
166+
    if (apiQuoteRecordInfo?.uri && (apiQuoteRecordInfo?.$type === "app.bsky.feed.defs#postView" || apiQuoteRecordInfo?.$type === "app.bsky.embed.record#viewRecord" || apiQuoteRecordInfo?.$type === "app.bsky.embed.recordWithMedia#viewRecord")) {
167-
            videoCopyButton.className = 'video-copy-btn';
167+
        let actualQuotedRecord = apiQuoteRecordInfo.value ? apiQuoteRecordInfo : apiQuoteRecordInfo.record || apiQuoteRecordInfo;
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;';
168+
        if (actualQuotedRecord?.uri) {
169
            const qAuthorHandleRaw = actualQuotedRecord.author?.handle || actualQuotedRecord.uri.split('/')[2];
170-
            // Handle Comment Elements
170+
            const qAuthorHandleClean = cleanHandle(qAuthorHandleRaw);
171-
            if (element.classList.contains('pure-g') && element.querySelector('a[href^="/channel/"]')) {
171+
            const qPostId = actualQuotedRecord.uri.split('/')[4];
172-
                const permalink = element.querySelector('a[title="YouTube comment permalink"]')?.href || window.location.href;
172+
            const qTextRaw = actualQuotedRecord.value?.text || "";
173-
                const username = element.querySelector('a[href^="/channel/"]')?.textContent.trim() || 'Unknown';
173+
            const qTextClean = cleanText(qTextRaw);
174-
                const datetime = element.querySelector('span[title]')?.getAttribute('title') || 'Unknown';
174+
            
175-
                const contentHtml = element.querySelector('p[style="white-space:pre-wrap"]');
175+
            urlOfQuotedPostByThis = `https://bsky.app/profile/${qAuthorHandleClean}/post/${qPostId}`;
176-
                const likes = element.querySelector('.icon.ion-ios-thumbs-up')?.nextSibling.textContent.trim() || '0';
176+
            postOutputText += `\n\n[QUOTED POST]\nπŸ‡ΊπŸ‡Έ ${qAuthorHandleClean}\n${qTextClean}`;
177
            console.log(`extractPost: API Quoted Post BY post ${thisPostsId}: URL='${urlOfQuotedPostByThis}'`);
178-
                if (!contentHtml) {
178+
179-
                    console.log('[Invidious BBCode Tool] No content found for comment');
179+
            const quotedEmbeds = actualQuotedRecord.embeds || actualQuotedRecord.value?.embeds;
180-
                    return;
180+
            if (quotedEmbeds?.length > 0) {
181-
                }
181+
                quotedEmbeds.forEach(emb => {
182
                    if (emb.$type === "app.bsky.embed.images#view" && emb.images) {
183-
                const contentText = extractTextWithLinks(contentHtml.innerHTML);
183+
                        emb.images.slice(0, MAX_IMAGES_PER_POST).forEach(img => postOutputText += `\n[img]${img.thumb}[/img]`); 
184-
                const contentBBCode = htmlToBBCode(contentText);
184+
                    } else if (emb.$type === "app.bsky.embed.video#view" && emb.playlist) {
185-
                const spoilerContent = `\n${username} commented on ${datetime} | Likes: ${likes}\n\n${contentBBCode}\n`;
185+
                         postOutputText += `\n[U][URL]${emb.playlist}[/URL][/U]`;
186-
                const fullContent = `[icode]${permalink}[/icode]\n${spoilerContent}`;
186+
                    } else if (emb.$type === "app.bsky.embed.images" && emb.images && actualQuotedRecord.author?.did) {
187
                         emb.images.slice(0, MAX_IMAGES_PER_POST).forEach(img => postOutputText += `\n[img]${img.image.ref?.$link ? `https://cdn.bsky.app/img/feed_fullsize/plain/${actualQuotedRecord.author.did}/${img.image.ref.$link}@${img.image.mimeType.split('/')[1]}` : 'fallback.jpg'}[/img]`);
188-
                copyButton.addEventListener('click', (e) => {
188+
                    }
189-
                    e.preventDefault();
189+
190-
                    e.stopPropagation();
190+
191-
                    navigator.clipboard.writeText(fullContent)
191+
192-
                        .then(() => showToast('Comment copied!'))
192+
193-
                        .catch(err => console.error('[Invidious BBCode Tool] Copy failed:', err));
193+
    
194
    else if (quoteEmbedElement) {
195
        const qTextContentElem = quoteEmbedElement.querySelector('div[data-word-wrap="1"]'); 
196-
                nestedButton.addEventListener('click', (e) => {
196+
        const qTextRaw = qTextContentElem?.innerHTML || "";
197-
                    e.preventDefault();
197+
        if (qTextRaw) {
198-
                    e.stopPropagation();
198+
            const qTextClean = cleanText(qTextRaw);
199-
                    const parents = getParentComments(element);
199+
            let qAuthorHandleRaw = quoteEmbedElement.querySelector(".css-146c3p1.r-dnmrzs.r-1udh08x.r-1udbk01.r-3s2u2q.r-1iln25a")?.textContent?.trim() || "Unknown";
200-
                    const maxDepth = parents.length;
200+
            const qAuthorHandleClean = cleanHandle(qAuthorHandleRaw);
201
            
202-
                    createDepthDropdown(maxDepth, (depth) => {
202+
            let qPostId = null;
203-
                        const items = [
203+
            
204-
                            ...parents.slice(0, depth),
204+
            const qPostLink = quoteEmbedElement.querySelector('a[href*="/post/"]'); 
205-
                            { permalink, spoilerContent }
205+
            if (qPostLink) {
206-
                        ];
206+
                qPostId = qPostLink.href.split('/').pop().split('?')[0];
207
            }
208-
                        const payload = items
208+
            
209-
                            .map((item, index) => {
209+
            postOutputText += `\n\n[QUOTED POST]\nπŸ‡ΊπŸ‡Έ ${qAuthorHandleClean}\n${qTextClean}`;
210-
                                const indent = 'β”‚   '.repeat(index);
210+
            if (qAuthorHandleClean !== "Unknown" && qPostId) {
211-
                                const commentLines = `[icode]${item.permalink}[/icode]\n${item.spoilerContent}`.split('\n');
211+
                 urlOfQuotedPostByThis = `https://bsky.app/profile/${qAuthorHandleClean}/post/${qPostId}`;
212-
                                return commentLines.map(line => `${indent}${line}`).join('\n');
212+
                 console.log(`extractPost: DOM Quoted Post BY post ${thisPostsId}: URL='${urlOfQuotedPostByThis}' (ID='${qPostId}')`);
213-
                            })
213+
214-
                            .join('\n\n');
214+
215
            
216-
                        navigator.clipboard.writeText(payload)
216+
            const mediaForTheQuotedPost = extractMedia(quoteEmbedElement); 
217-
                            .then(() => showToast(`Comment with ${depth} parent${depth === 1 ? '' : 's'} copied!`))
217+
            mediaForTheQuotedPost.images.slice(0, MAX_IMAGES_PER_POST).forEach(imgUrl => postOutputText += `\n[img]${imgUrl}[/img]`);
218-
                            .catch(err => console.error('[Invidious BBCode Tool] Nested copy failed:', err));
218+
            
219-
                    }, nestedButton);
219+
            
220
            let quotedVideoUrlDOM = null;
221
            if (quoteEmbedElement.querySelector('video[poster]')) { /* ... video logic ... */ }
222-
                const target = element.querySelector('.pure-u-20-24, .pure-u-md-22-24') || element;
222+
            if (quotedVideoUrlDOM) postOutputText += `\n[U][URL]${quotedVideoUrlDOM}[/URL][/U]`;
223-
                target.appendChild(copyButton);
223+
224-
                target.appendChild(nestedButton);
224+
225
226
    
227-
            // Handle Video Metadata Element
227+
    const normalizeUrl = (url) => (url.startsWith('/') ? `https://bsky.app${url}` : url).replace(/\/+$/, "").toLowerCase();
228-
            if (element.classList.contains('h-box') && element.classList.contains('highlight') && element.querySelector('h1')) {
228+
    
229-
                console.log('[Invidious BBCode Tool] Video metadata element detected:', element);
229+
    const externalLinks = new Set(
230
        Array.from(postElement.querySelectorAll("a[href]"))
231-
                const title = element.querySelector('h1')?.textContent.trim() || 'Untitled';
231+
        .filter(link => !link.closest(quoteEmbedSelector)) 
232-
                const youtubeUrl = document.querySelector('#link-yt-watch')?.href || window.location.href;
232+
        .map(link => normalizeUrl(link.href))
233-
                const channelName = document.querySelector('#channel-name')?.textContent.trim() || 'Unknown';
233+
        .filter(href => href.startsWith("http") && !href.includes("/post/") && !href.includes("/profile/") && !postOutputText.includes(href))
234-
                const channelUrl = document.querySelector('a[href^="/channel/"]')?.href || '';
234+
    );
235-
                const views = document.querySelector('#views')?.textContent.trim().replace(/\sViews/, '') || '0';
235+
    externalLinks.forEach(linkUrl => postOutputText += `\n[U][URL]${linkUrl}[/URL][/U]`);
236-
                const likes = document.querySelector('#likes')?.textContent.trim() || '0';
236+
    
237-
                const publishedDate = document.querySelector('#published-date b')?.textContent.trim() || 'Unknown';
237+
    
238-
                const descriptionHtml = document.querySelector('#descriptionWrapper')?.innerHTML || 'No description';
238+
    const finalAuthorString = thisPostsAuthorHandleFromTestId && thisPostsAuthorHandleFromTestId !== "Unknown"
239-
                const descriptionText = extractTextWithLinks(descriptionHtml);
239+
        ? `πŸ‡ΊπŸ‡Έ ${thisPostsAuthorHandleFromTestId}`
240-
                const descriptionBBCode = htmlToBBCode(descriptionText);
240+
        : `πŸ‡ΊπŸ‡Έ ${cleanHandle(postElement.querySelector(".css-146c3p1.r-dnmrzs.r-1udh08x.r-1udbk01.r-3s2u2q.r-1iln25a")?.textContent?.trim() || "Unknown")}`;
241
242-
                const videoContent = `[b]${title}[/b]\n` +
242+
    return {
243-
                                    `[icode]${youtubeUrl}[/icode]\n\n` +
243+
        text: postOutputText,
244-
                                    `[i]Channel:[/i] [icode]${channelUrl}[/icode] ${channelName}\n` +
244+
        author: finalAuthorString,
245-
                                    `[i]Stats:[/i] Views: ${views} | Likes: ${likes}\n` +
245+
        url: thisPostsUrl, 
246-
                                    `[i]Published:[/i] ${publishedDate}\n\n` +
246+
        threadData: threadDataForThisPost,
247-
                                    `\n${descriptionBBCode}\n`;
247+
        quotedUrl: urlOfQuotedPostByThis 
248
    };
249-
                videoCopyButton.addEventListener('click', (e) => {
249+
  }
250-
                    e.preventDefault();
250+
251-
                    e.stopPropagation();
251+
252-
                    navigator.clipboard.writeText(videoContent)
252+
  async function extractPosts(e_count){
253-
                        .then(() => showToast('Video metadata copied!'))
253+
    const allPosts = Array.from(document.querySelectorAll('[data-testid^="postThreadItem-by-"], [data-testid="postThreadItem"]'));
254-
                        .catch(err => console.error('[Invidious BBCode Tool] Video copy failed:', err));
254+
    if (!allPosts.length) return void showToast("No posts found on this page.");
255
256
    const quoteEmbedSelector = '[role="link"][aria-label*="Post by"]';
257-
                const target = document.querySelector('#subscribe');
257+
    let activePostElement = allPosts.find(p => {
258-
                if (target) {
258+
        const link = p.querySelector('a[href*="/post/' + window.location.href.split('/').pop().split('?')[0] + '"]');
259-
                    console.log('[Invidious BBCode Tool] Attaching video copy button under #subscribe:', target);
259+
        
260-
                    target.insertAdjacentElement('afterend', videoCopyButton);
260+
        return link && !link.closest(quoteEmbedSelector);
261-
                } else {
261+
    }) || allPosts.find(p => !p.closest(quoteEmbedSelector)); 
262-
                    console.log('[Invidious BBCode Tool] #subscribe not found, falling back to element:', element);
262+
    if (!activePostElement && allPosts.length > 0) activePostElement = allPosts[0];
263-
                    element.appendChild(videoCopyButton); // Fallback
263+
264-
                }
264+
265
    const activeIndex = allPosts.indexOf(activePostElement);
266
267
    
268-
        function showToast(message) {
268+
    const activeData = await extractPost(activePostElement, true);
269-
            const toast = document.createElement('div');
269+
    const activeUrl = activeData.url; 
270-
            toast.textContent = message;
270+
    const activeText = activeData.text;
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);';
271+
    const activeAuthor = activeData.author;
272-
            document.body.appendChild(toast);
272+
    const activeThreadData = activeData.threadData;
273-
            setTimeout(() => toast.remove(), 2000);
273+
    const activeQuotedUrl = activeData.quotedUrl; 
274
275
    console.log("extractPosts: Active Post URL for main output:", activeUrl);
276-
        // Use MutationObserver to handle dynamic DOM changes
276+
    if (!activeUrl) {
277-
        const observer = new MutationObserver((mutations) => {
277+
        showToast("Critical error: Could not determine URL for the active post. Aborting.");
278-
            document.querySelectorAll('.pure-g').forEach(element => {
278+
        return;
279-
                if (element.querySelector('a[href^="/channel/"]') && !element.querySelector('.copy-btn')) {
279+
280-
                    console.log('[Invidious BBCode Tool] Processing comment element:', element);
280+
281-
                    createCopyButtons(element);
281+
282-
                }
282+
    let mainAuthorCleanedHandle = "";
283
    const activeAuthorMatch = activeAuthor.match(/πŸ‡ΊπŸ‡Έ (.*)/);
284
    if (activeAuthorMatch && activeAuthorMatch[1] !== "Unknown") {
285-
            document.querySelectorAll('.h-box.highlight').forEach(element => {
285+
        mainAuthorCleanedHandle = activeAuthorMatch[1];
286-
                if (element.querySelector('h1') && !element.querySelector('.video-copy-btn')) {
286+
    } else {
287-
                    console.log('[Invidious BBCode Tool] Processing video element:', element);
287+
        mainAuthorCleanedHandle = cleanHandle(document.querySelector('[data-testid="profileHandle"]')?.textContent?.trim() || "");
288-
                    createCopyButtons(element);
288+
289-
                }
289+
    if (!mainAuthorCleanedHandle || mainAuthorCleanedHandle === "Unknown") return void showToast("Could not determine main username.");
290
    console.log("extractPosts: Main Author Cleaned Handle:", mainAuthorCleanedHandle);
291
292
293-
        observer.observe(document.body, { childList: true, subtree: true });
293+
    let postsToFormat = [];
294
    let parentUrl = null, parentText = null, parentAuthor = null;
295-
        // Initial check in case elements are already loaded
295+
    let topLevelQuotedPostUrlForSpoiler = activeQuotedUrl; 
296-
        document.querySelectorAll('.pure-g').forEach(element => {
296+
297-
            if (element.querySelector('a[href^="/channel/"]')) {
297+
    
298-
                console.log('[Invidious BBCode Tool] Initial processing comment element:', element);
298+
    const apiParentInfo = activeThreadData?.thread?.post?.reply?.parent;
299-
                createCopyButtons(element);
299+
    if (apiParentInfo?.uri) {
300
        
301
        
302
        
303-
        document.querySelectorAll('.h-box.highlight').forEach(element => {
303+
        
304-
            if (element.querySelector('h1')) {
304+
        
305-
                console.log('[Invidious BBCode Tool] Initial processing video element:', element);
305+
        
306-
                createCopyButtons(element);
306+
        
307
        
308
        
309
        
310
        
311-
    // Toggle the BBCode tool on button click
311+
        
312-
    let isActive = false;
312+
        
313-
    bbButton.addEventListener('click', () => {
313+
    } else if (activeIndex > 0) { /* DOM parent */ }
314-
        if (!isActive) {
314+
315-
            activateBBCodeTool();
315+
    
316-
            bbButton.style.backgroundColor = '#f44336'; // Red when active
316+
    postsToFormat.push({author: activeAuthor, text: activeText});
317-
            bbButton.title = 'Deactivate BBCode Comment & Video Tool';
317+
318-
            isActive = true;
318+
319-
        } else {
319+
    
320-
            // Remove all copy buttons and reset
320+
    const repliesElements = allPosts.slice(activeIndex + 1, activeIndex + e_count - postsToFormat.length + 1);
321-
            document.querySelectorAll('.copy-btn, .nested-btn, .video-copy-btn, .depth-dropdown').forEach(el => el.remove());
321+
    for (const replyElement of repliesElements) {
322-
            bbButton.style.backgroundColor = '#4CAF50'; // Green when inactive
322+
        if (postsToFormat.length >= e_count) break;
323-
            bbButton.title = 'Activate BBCode Comment & Video Tool';
323+
        const replyData = await extractPost(replyElement, false); 
324-
            isActive = false;
324+
        if (replyData.text || (replyData.text && replyData.text.includes("[img]"))) {
325
            postsToFormat.push({author: replyData.author, text: replyData.text});
326-
    });
326+
            
327
            if (replyData.url && replyData.author && replyData.author.includes(mainAuthorCleanedHandle)) {
328
                
329
            }
330
        }
331
    }
332
333
    
334
    
335
    
336
    let mainPostUrls = new Set([activeUrl]); 
337
    if (parentUrl && parentAuthor && parentAuthor.includes(mainAuthorCleanedHandle)) mainPostUrls.add(parentUrl);
338
    
339
340
341
    let outputChunks = [];
342
    let currentChunkPostsData = postsToFormat; 
343
    let totalPostsFormatted = postsToFormat.length;
344
    
345
346
    const hasThreadsInfo = (parentUrl && parentAuthor && parentAuthor.includes(mainAuthorCleanedHandle) && parentUrl !== activeUrl) ||
347
                           ([...mainPostUrls].filter(u => u !== activeUrl && u !== parentUrl).length > 0) || 
348
                           (topLevelQuotedPostUrlForSpoiler && topLevelQuotedPostUrlForSpoiler !== activeUrl && !mainPostUrls.has(topLevelQuotedPostUrlForSpoiler) && (parentUrl ? topLevelQuotedPostUrlForSpoiler !== parentUrl : true) );
349
350
    let chunkString = `${activeUrl}`; 
351
    if (hasThreadsInfo) {
352
        chunkString += `\n[SPOILER="Threads Continued"]`;
353
        if (parentUrl && parentAuthor && parentAuthor.includes(mainAuthorCleanedHandle) && parentUrl !== activeUrl) {
354
            chunkString += `\n${parentUrl}`;
355
        }
356
        [...mainPostUrls].filter(u => u !== activeUrl && u !== parentUrl).forEach(u => chunkString += `\n${u}`);
357
        if (topLevelQuotedPostUrlForSpoiler && topLevelQuotedPostUrlForSpoiler !== activeUrl && !mainPostUrls.has(topLevelQuotedPostUrlForSpoiler) && (parentUrl ? topLevelQuotedPostUrlForSpoiler !== parentUrl : true)) {
358
             chunkString += `\n${topLevelQuotedPostUrlForSpoiler}`;
359
        }
360
        chunkString += `\n[/SPOILER]`;
361
    }
362
    chunkString += `\n[SPOILER="full text & large images"]\n\n${currentChunkPostsData.map((p, idx) => `${idx + 1}/${currentChunkPostsData.length}\n${p.author}\n${p.text}`).join("\n\n")}`;
363
    chunkString += `\n\n[COLOR=rgb(184,49,47)][B][SIZE=5]To post tweets in this format, more info here: [URL]https://www.thecoli.com/threads/tips-and-tricks-for-posting-the-coli-megathread.984734/post-52211196[/URL][/SIZE][/B][/COLOR]\n[/SPOILER]`;
364
    outputChunks.push(chunkString);
365
366
    const finalOutput = outputChunks.join("\n\n[threads continued]\n\n");
367
    const d = document.createElement("textarea"); d.value = finalOutput; document.body.appendChild(d); d.select(); document.execCommand("copy"); document.body.removeChild(d); showToast(`Copied: ${totalPostsFormatted} posts`)
368
  }
369
  createPostCountPrompt(extractPosts)
370
})();