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 | })(); |