您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhanced markdown formatting for pollinations.ai with better readability, and smoother viewing
当前为
// ==UserScript== // @name Pollinations.ai Enhancer // @namespace https://greasyfork.org/en/users/1462897-fisventurous // @version 1.9.3 // @description Enhanced markdown formatting for pollinations.ai with better readability, and smoother viewing // @author fisven // @match *://*.pollinations.ai/* // @connect * // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; const THEME_KEY = 'pollinations_enhancer_theme'; const FONT_SIZE_KEY = 'pollinations_enhancer_fontsize'; let observer = null; // wait for page load or run immediately if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } function init() { console.log("Pollinations Enhancer v1.9.2 Initialising..."); addStyles(); applyTheme(); applyFontSize(); const pageType = detectPageType(); setupLinkPreviews(); if (pageType.isText) enhanceTextPage(); else if (pageType.isImage) enhanceImagePage(); else if (pageType.isAudio) enhanceAudioPage(); else createCommonButtons(extractUrlParameters(), 'unknown'); // fallback for unknown if (pageType.isText) { updateThemeToggleButton(document.body.classList.contains('theme-dark')); } startObserver(pageType); } function detectPageType() { const url = window.location.href.toLowerCase(); const urlParams = new URLSearchParams(window.location.search); let isImage = false; let isAudio = false; let isText = false; // check URL first if (url.includes('image.pollinations.ai') || url.includes('/image/')) { isImage = true; } else if (url.includes('audio') || url.match(/\.(mp3|wav|ogg|m4a)(\?|$)/i)) { isAudio = true; } else if (url.includes('text.pollinations.ai')) { isText = true; } // check URL params if type unknown if (!isImage && !isAudio && !isText) { const model = urlParams.get('model') || ''; const hasVoice = urlParams.has('voice'); if (model.includes('audio') || hasVoice) { isAudio = true; } else if (model.includes('image')) { isImage = true; } } // check page content as a last resort if (!isImage && !isAudio && !isText) { if (document.querySelector('img:not([width="16"][height="16"])')) { isImage = true; // found a significant image } else if (document.querySelector('audio, video, [type*="audio"]')) { isAudio = true; // found audio/video elements } else { isText = true; // assume text otherwise } } return { isText, isImage, isAudio }; } function startObserver(pageType) { if (observer) observer.disconnect(); observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length) { // try re-enhancing if media loads dynamically if (pageType.isImage && !document.getElementById('save-image-btn')) { const newImages = Array.from(mutation.addedNodes) .filter(node => node.tagName === 'IMG' || (node.querySelectorAll && node.querySelectorAll('img').length)); if (newImages.length) setTimeout(enhanceImagePage, 100); } if (pageType.isAudio && !document.getElementById('save-audio-btn')) { const newAudio = Array.from(mutation.addedNodes) .filter(node => node.tagName === 'AUDIO' || node.tagName === 'VIDEO' || (node.querySelectorAll && ( node.querySelectorAll('audio').length || node.querySelectorAll('video').length || node.querySelectorAll('source[type*="audio"]').length )) ); if (newAudio.length) setTimeout(enhanceAudioPage, 100); } } } }); observer.observe(document.body, { childList: true, subtree: true }); } function applyTheme() { const savedTheme = GM_getValue(THEME_KEY, 'theme-dark'); // default dark document.body.classList.remove('theme-dark', 'theme-light'); document.body.classList.add(savedTheme); } function toggleTheme() { const isDark = document.body.classList.contains('theme-dark'); const newTheme = isDark ? 'theme-light' : 'theme-dark'; document.body.classList.remove('theme-dark', 'theme-light'); document.body.classList.add(newTheme); GM_setValue(THEME_KEY, newTheme); // save preference updateThemeToggleButton(newTheme === 'theme-dark'); } function updateThemeToggleButton(isDark) { const btn = document.getElementById('theme-toggle-btn'); if (btn) { // sun icon for dark theme, moon for light btn.innerHTML = isDark ? '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"></path></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>'; } } function setupLinkPreviews() { const preview = document.createElement('div'); preview.className = 'preview'; document.body.appendChild(preview); let showTimeout = null, hideTimeout = null, currentLink = null; document.body.addEventListener('mouseover', (e) => { const link = e.target.closest('a'); if (link?.href) { // check for link and href clearTimeout(hideTimeout); if (currentLink !== link) { currentLink = link; clearTimeout(showTimeout); showTimeout = setTimeout(() => showPreview(link, preview), 200); // slight delay } } }); document.body.addEventListener('mouseout', (e) => { if (e.target.closest('a')) { clearTimeout(showTimeout); hideTimeout = setTimeout(() => { preview.style.opacity = '0'; setTimeout(() => { preview.style.display = 'none'; currentLink = null; }, 150); // wait for fade out }, 200); // delay before hiding } }); // keep preview open if mouse moves onto it preview.addEventListener('mouseenter', () => clearTimeout(hideTimeout)); preview.addEventListener('mouseleave', () => { hideTimeout = setTimeout(() => { preview.style.opacity = '0'; setTimeout(() => { preview.style.display = 'none'; currentLink = null; }, 150); }, 200); }); } function showPreview(link, preview) { try { const url = link.href; const rect = link.getBoundingClientRect(); let sourceName = 'Source'; let faviconUrl = ''; try { const urlObj = new URL(url); sourceName = urlObj.hostname.replace(/^www\./, ''); faviconUrl = `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=64`; } catch (err) { /* ignore invalid urls */ } let contentHTML = link.textContent?.trim() || url; if (link.closest('pre, code')) { contentHTML = `<code class="inline-code">${contentHTML}</code>`; // preserve code styling } const faviconHTML = faviconUrl ? `<img src="${faviconUrl}" alt="" onerror="this.style.display='none'; this.parentElement.textContent='${sourceName.charAt(0).toUpperCase()}';" />` : sourceName.charAt(0).toUpperCase(); preview.innerHTML = ` <div class="preview-header"> <div class="preview-icon">${faviconHTML}</div> <div>${sourceName}</div> </div> <div class="preview-content">${contentHTML}</div> <div class="preview-url">${url}</div> `; preview.style.opacity = 0; preview.style.display = 'block'; // position calculation needs to happen after display:block requestAnimationFrame(() => { const previewRect = preview.getBoundingClientRect(); const winWidth = window.innerWidth; const winHeight = window.innerHeight; let top = rect.bottom + window.scrollY + 5; let left = rect.left + window.scrollX; // adjust position to stay within viewport if (left + previewRect.width > winWidth - 10) left = winWidth - previewRect.width - 10; if (left < 10) left = 10; if (top + previewRect.height > winHeight + window.scrollY - 10) top = rect.top + window.scrollY - previewRect.height - 5; if (top < window.scrollY + 10) top = window.scrollY + 10; preview.style.top = `${top}px`; preview.style.left = `${left}px`; preview.classList.add('active'); preview.style.opacity = 1; }); } catch (e) { console.error("Pollinations Enhancer: Error showing preview", e); preview.classList.remove('active'); preview.style.display = 'none'; } } function enhanceTextPage() { document.body.classList.add('text-enhanced'); const params = extractUrlParameters(); let contentContainer = null; let originalContent = ""; try { // find the main content area contentContainer = document.querySelector( 'main:not(:empty), article:not(:empty), .content:not(:empty), #content:not(:empty), .main-content:not(:empty), .post-content:not(:empty)' ); // handle pages that might just be a single <pre> tag if (!contentContainer && document.body.children.length === 1 && document.body.firstElementChild?.tagName === 'PRE') { contentContainer = document.body.firstElementChild; } // if no container found, wrap bare text nodes (risky but sometimes needed) if (!contentContainer && (document.body.innerText || document.body.textContent || '').trim().length > 50) { console.log("Pollinations Enhancer: Wrapping bare text content."); contentContainer = document.createElement('div'); contentContainer.className = 'content-container-generated'; // mark as generated // move body nodes into the container, careful not to move our script/buttons while (document.body.firstChild && (!document.body.firstChild.matches || !document.body.firstChild.matches('#pollinations-enhancer-buttons, .preview, script, style'))) { contentContainer.appendChild(document.body.firstChild); } document.body.appendChild(contentContainer); } if (contentContainer) { contentContainer.classList.add('content-container'); if (contentContainer.tagName !== 'PRE') { // get original text *before* markdown processing originalContent = contentContainer.innerText || contentContainer.textContent || ''; processMarkdown(contentContainer); } else { originalContent = contentContainer.textContent || ''; // content is already plain text } } else { console.warn("Pollinations Enhancer: Could not find a suitable content container."); } } catch (error) { console.error("Pollinations Enhancer: Error enhancing text page:", error); } createCommonButtons(params, 'text', contentContainer, originalContent); } function processMarkdown(container) { if (!container) return; // basic cleanup first container.innerHTML = container.innerHTML .replace(/<span class="[^"]*">/g, '') // remove potentially interfering spans .replace(/<\/span>/g, ''); const codeBlocks = []; let codeBlockCount = 0; // isolate code blocks first to prevent markdown processing inside them container.innerHTML = container.innerHTML.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => { const placeholder = `<!--CODEBLOCK_${codeBlockCount}-->`; codeBlocks.push({ placeholder, language: lang || 'text', code: code.replace(/`/g, '`') // escape backticks inside code }); codeBlockCount++; return placeholder; }); const originalHtmlWithPlaceholders = container.innerHTML; const textContentForProcessing = container.textContent || ''; // process blockquotes on the text content let processedHtml = processBlockquotes(textContentForProcessing); // restore code block placeholders if blockquotes were processed if (processedHtml !== textContentForProcessing) { processedHtml = restoreCodeBlocks(processedHtml, originalHtmlWithPlaceholders); } else { processedHtml = originalHtmlWithPlaceholders; // use original if no blockquotes found } // now apply other markdown rules to the potentially modified html processedHtml = processedHtml .replace(/^(#{1,6})\s+(.+)$/gm, (match, hashes, content) => `<h${hashes.length}>${content}</h${hashes.length}>`) // Headers .replace(/^---+$/gm, '<hr class="markdown-hr">') // Horizontal rule .replace(/\*\*(.*?)\*\*|__(.*?)__/g, '<strong>$1$2</strong>') // Bold .replace(/\*(.*?)\*|_(.*?)_/g, '<em>$1$2</em>') // Italic .replace(/~~(.*?)~~/g, '<del>$1</del>') // Strikethrough .replace(/\[([^\]]+?)\]\((https?:\/\/[^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>') // Links .replace(/`([^`]+?)`/g, '<code class="inline-code">$1</code>'); // Inline code // process lists after other formatting processedHtml = processLists(processedHtml); // put the processed html back into the container container.innerHTML = processedHtml; // restore the actual code block content codeBlocks.forEach(block => { const codeHtml = ` <pre class="code-block-container"> <div class="code-header"> <span class="code-language">${block.language}</span> <button class="code-copy-btn" title="Copy code">Copy</button> </div> <code class="language-${block.language}">${block.code}</code> </pre>`; container.innerHTML = container.innerHTML.replace(block.placeholder, codeHtml); }); // add listeners to copy buttons inside code blocks container.querySelectorAll('.code-copy-btn').forEach(btn => { btn.addEventListener('click', () => { const codeElement = btn.closest('.code-block-container')?.querySelector('code'); if (codeElement) { navigator.clipboard.writeText(codeElement.textContent || '').then(() => { const originalText = btn.textContent; btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = originalText; }, 1500); }).catch(err => console.error('Failed to copy code:', err)); } }); }); } function processBlockquotes(text) { const lines = text.split('\n'); let result = []; let inMultiBlockquote = false; let blockquoteContent = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); if (trimmedLine.startsWith('>>>')) { // start multi-line blockquote if (inMultiBlockquote) { // close previous one if any result.push(`<blockquote class="blockquote-multi">${blockquoteContent.join(' ')}</blockquote>`); } inMultiBlockquote = true; const initial = trimmedLine.substring(3).trim(); blockquoteContent = initial ? [initial] : []; } else if (inMultiBlockquote) { // continue or end multi-line blockquote if (trimmedLine === '') { // end on empty line result.push(`<blockquote class="blockquote-multi">${blockquoteContent.join(' ')}</blockquote>`); result.push(''); // keep the empty line separator inMultiBlockquote = false; blockquoteContent = []; } else { blockquoteContent.push(trimmedLine); } } else if (trimmedLine.startsWith('> ')) { // single line blockquote result.push(`<blockquote class="blockquote-single">${trimmedLine.substring(2)}</blockquote>`); } else { // regular line result.push(line); } } // close any open multi-line quote at the end of the text if (inMultiBlockquote) { result.push(`<blockquote class="blockquote-multi">${blockquoteContent.join(' ')}</blockquote>`); } return result.join('\n'); } function restoreCodeBlocks(processedHtml, originalHtmlWithPlaceholders) { // find all placeholders in the original html const placeholders = originalHtmlWithPlaceholders.match(/<!--CODEBLOCK_\d+-->/g) || []; if (placeholders.length === 0) return processedHtml; let currentHtml = processedHtml; // replace placeholder markers in the processed text with the original placeholders // this is tricky because blockquote processing might have altered line structure // simple strategy: replace markers sequentially let placeholderIndex = 0; currentHtml = currentHtml.replace(/<!--CODEBLOCK_\d+-->/g, () => { if (placeholderIndex < placeholders.length) { return placeholders[placeholderIndex++]; } return ''; // should not happen if counts match }); // if counts didn't match, try injecting remaining placeholders (less ideal) while(placeholderIndex < placeholders.length) { console.warn("Pollinations Enhancer: Mismatched code block count during restoration."); currentHtml += placeholders[placeholderIndex++]; // append leftovers } return currentHtml; } function processLists(text) { const lines = text.split('\n'); let result = []; let lastWasNumbered = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const numberedMatch = line.match(/^(\s*)(\d+)\.\s+(.*)/); // capture indent const dashMatch = line.match(/^(\s*)[-*]\s+(.*)/); // capture indent if (numberedMatch) { const indent = numberedMatch[1]; const num = numberedMatch[2]; const content = numberedMatch[3]; // basic indent handling - could be more sophisticated const indentLevel = indent.length / 2; // rough estimate result.push(`<div class="list-item numbered-item" style="padding-left: ${indentLevel * 1.5}em;">${num}. ${content}</div>`); lastWasNumbered = true; } else if (dashMatch) { const indent = dashMatch[1]; const content = dashMatch[2]; const indentLevel = indent.length / 2; const bulletClass = lastWasNumbered ? "nested-bullet-item" : "bullet-item"; result.push(`<div class="list-item ${bulletClass}" style="padding-left: ${indentLevel * 1.5}em;">${content}</div>`); // lastWasNumbered remains true if nested under numbered } else { result.push(line); // pass through non-list lines lastWasNumbered = line.trim() !== ''; // reset if line is not empty } } return result.join('\n'); } function enhanceImagePage() { removeExistingButtons(); // clear old buttons first let mainImage = null; try { // prioritise images directly in body const directImages = Array.from(document.querySelectorAll('body > img')); if (directImages.length > 0) { let largestArea = 0; directImages.forEach(img => { if (img.complete && img.naturalWidth > 30 && img.naturalHeight > 30) { const area = img.naturalWidth * img.naturalHeight; if (area > largestArea) { largestArea = area; mainImage = img; } } else if (!img.complete) { // if image not loaded yet, re-run enhancement on load img.addEventListener('load', () => setTimeout(enhanceImagePage, 100), { once: true }); } }); } // fallback to searching all significant images if no direct body image found if (!mainImage) { const allImages = document.querySelectorAll('img'); let largestArea = 0; allImages.forEach(img => { if (img.width < 30 || img.height < 30) return; // ignore small icons const area = img.naturalWidth * img.naturalHeight || img.width * img.height; if (area > largestArea) { largestArea = area; mainImage = img; } }); } } catch (error) { console.error("Pollinations Enhancer: Error finding image element:", error); } const params = extractUrlParameters(); if (mainImage) { // add width/height from image if available params.width = mainImage.naturalWidth || mainImage.width || params.width || ''; params.height = mainImage.naturalHeight || mainImage.height || params.height || ''; createCommonButtons(params, 'image', mainImage); } else { // if no image found yet, maybe it loads later? retry once. console.log("Pollinations Enhancer: Main image not found, scheduling retry."); setTimeout(enhanceImagePage, 600); // longer delay for retry } } function enhanceAudioPage() { removeExistingButtons(); const params = extractUrlParameters(); const audioSrc = findAudioSource() || window.location.href; // use page url as fallback createCommonButtons(params, 'audio', null, '', audioSrc); } function removeExistingButtons() { document.getElementById('pollinations-enhancer-buttons')?.remove(); document.getElementById('metadata-box')?.remove(); } function findAudioSource() { // check common audio/video tags and sources const mediaElements = document.querySelectorAll('audio, video'); for (const el of mediaElements) { if (el.src && el.src.length > 10) return el.src; for (const source of el.querySelectorAll('source')) { if (source.src && source.src.length > 10) return source.src; } } // check links that look like audio files const audioExtRegex = /\.(mp3|wav|ogg|m4a|flac|aac)(\?|$)/i; for (const link of document.querySelectorAll('a[href]')) { try { const url = new URL(link.getAttribute('href'), window.location.href); if (audioExtRegex.test(url.pathname) || url.pathname.includes('/audio/')) { return url.href; } } catch (e) { /* ignore invalid href */ } } // check elements with audio-related attributes for (const el of document.querySelectorAll('[type*="audio"], [src*=".mp3"], [src*="/audio/"]')) { const src = el.getAttribute('src') || el.getAttribute('data-src'); if (src && src.length > 10) return new URL(src, window.location.href).href; } // check meta tags const metaOgAudio = document.querySelector('meta[property="og:audio"], meta[property="og:audio:url"]'); if (metaOgAudio?.content) return metaOgAudio.content; // last resort: scan html for direct audio urls (less reliable) const htmlContent = document.documentElement.outerHTML; const audioUrlMatches = htmlContent.match(/https?:\/\/[^"'\s]+\.(mp3|wav|ogg|m4a)(\?[^"'\s]*)?/gi); if (audioUrlMatches?.length) return audioUrlMatches[0]; return null; // no source found } // creates the floating button panel function createCommonButtons( params, type, targetElement = null, originalContent = '', resourceUrl = '' ) { try { removeExistingButtons(); // ensure clean state const buttonContainer = document.createElement('div'); buttonContainer.id = 'pollinations-enhancer-buttons'; buttonContainer.style.cssText = ` position: fixed; top: 10px; right: 20px; z-index: 9999; display: flex; flex-direction: column; gap: 8px; align-items: flex-end; `; // stack buttons vertically // --- Text Page Specific Buttons --- if (type === 'text') { const themeToggleBtn = createButton('theme-toggle-btn', 'Toggle theme (Light/Dark)', '', toggleTheme); buttonContainer.appendChild(themeToggleBtn); updateThemeToggleButton(document.body.classList.contains('theme-dark')); if (targetElement) { const copyContentBtn = createButton( 'copy-content-btn', 'Copy text content', '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>', () => { const textToCopy = originalContent || (targetElement.innerText || targetElement.textContent || ''); navigator.clipboard.writeText(textToCopy).then( () => flashButton(copyContentBtn, true), () => flashButton(copyContentBtn, false) ); } ); buttonContainer.appendChild(copyContentBtn); } // --- Font Size Controls --- const fontSizeContainer = document.createElement('div'); fontSizeContainer.className = 'font-size-controls'; fontSizeContainer.style.cssText = 'display: flex; gap: 5px; justify-content: flex-end;'; const decreaseFontBtn = createButton( 'decrease-font-btn', 'Decrease text size', '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="8" y1="12" x2="16" y2="12"></line></svg>', decreaseFontSize ); const increaseFontBtn = createButton( 'increase-font-btn', 'Increase text size', '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>', increaseFontSize ); fontSizeContainer.appendChild(decreaseFontBtn); fontSizeContainer.appendChild(increaseFontBtn); buttonContainer.appendChild(fontSizeContainer); } // --- Image Download Button --- if (type === 'image' && targetElement?.src) { const timestamp = getFormattedTimestamp(); const seedPart = params.seed ? `_${params.seed}` : '_noseed'; let extension = '.jpg'; // default try { const urlPath = new URL(targetElement.src).pathname; const match = urlPath.match(/\.(jpg|jpeg|png|webp|gif)$/i); if (match) { extension = match[0].toLowerCase(); if (extension === '.jpeg') extension = '.jpg'; // normalise } } catch (e) { console.warn("Enhancer: Couldn't parse img URL for extension", e); } const filename = `pollinations.ai${seedPart}_${timestamp}${extension}`; const saveImageBtn = createButton( 'save-image-btn', `Save image (${filename})`, '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>', () => downloadResource(targetElement.src, filename, saveImageBtn, 'image') ); buttonContainer.appendChild(saveImageBtn); } // --- Audio Download Button --- if (type === 'audio' && resourceUrl) { const timestamp = getFormattedTimestamp(); const seedPart = params.seed ? `_${params.seed}` : '_noseed'; let extension = '.mp3'; // default try { const urlPath = new URL(resourceUrl).pathname; const match = urlPath.match(/\.(mp3|wav|ogg|m4a|flac|aac)$/i); if (match) extension = match[0].toLowerCase(); } catch (e) { console.warn("Enhancer: Couldn't parse audio URL for extension", e); } const filename = `pollinations.ai${seedPart}_${timestamp}${extension}`; const saveAudioBtn = createButton( 'save-audio-btn', `Save audio (${filename})`, '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>', () => downloadResource(resourceUrl, filename, saveAudioBtn, 'audio') ); buttonContainer.appendChild(saveAudioBtn); } // --- Metadata Button & Box --- // check if any relevant parameter exists const hasMetadata = Object.entries(params).some(([key, value]) => value && ['model', 'prompt', 'seed', 'voice', 'width', 'system', 'private', 'guidance_scale', 'negative_prompt', 'strength', 'steps', 'language'].includes(key) ); if (hasMetadata) { const metadataBtn = createButton( 'metadata-btn', 'View metadata', '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>', () => { // toggle visibility and collapsed state const box = document.getElementById('metadata-box'); if (box) { const becomesVisible = !box.classList.contains('visible'); box.classList.toggle('visible'); if (becomesVisible) { box.classList.remove('collapsed'); // always expand when showing } else { box.classList.add('collapsed'); // ensure marked collapsed when hiding } updateMetadataToggleIcon(box); } } ); buttonContainer.appendChild(metadataBtn); createMetadataBox(params, type); // create the hidden box } // add the whole container to the page if it has any buttons if (buttonContainer.hasChildNodes()) { document.body.appendChild(buttonContainer); } } catch (error) { console.error("Pollinations Enhancer: Error creating common buttons:", error); } } function increaseFontSize() { const currentScale = parseFloat(GM_getValue(FONT_SIZE_KEY, '1')) || 1; const newScale = Math.min(currentScale + 0.1, 2.0).toFixed(1); // cap at 2.0 GM_setValue(FONT_SIZE_KEY, newScale); applyFontSize(); } function decreaseFontSize() { const currentScale = parseFloat(GM_getValue(FONT_SIZE_KEY, '1')) || 1; const newScale = Math.max(currentScale - 0.1, 0.7).toFixed(1); // cap at 0.7 GM_setValue(FONT_SIZE_KEY, newScale); applyFontSize(); } function applyFontSize() { const scale = GM_getValue(FONT_SIZE_KEY, '1') || '1'; document.getElementById('font-size-style')?.remove(); // remove old style const styleEl = document.createElement('style'); styleEl.id = 'font-size-style'; // apply scale to font-size only, avoid scaling the whole container styleEl.textContent = ` body.text-enhanced .content-container, body.text-enhanced .content-container-generated { font-size: calc(17px * ${scale}) !important; } `; document.head.appendChild(styleEl); } function createButton(id, title, innerHTML, onClick) { const btn = document.createElement('div'); // using div styled as button btn.id = id; btn.className = 'p-btn'; btn.title = title; btn.innerHTML = innerHTML; btn.addEventListener('click', onClick); btn.style.position = 'relative'; // needed for potential future absolute elements inside btn.style.margin = '0'; // remove default margins return btn; } function flashButton(button, success) { if (!button) return; const originalColor = button.style.backgroundColor; // capture default button.style.backgroundColor = success ? 'var(--success-color)' : 'var(--error-color)'; button.style.transform = 'scale(1.1)'; setTimeout(() => { if (button) { // check if button still exists button.style.backgroundColor = ''; // revert to css default button.style.transform = 'scale(1)'; } }, 1000); } // handles downloading, attempts exif stripping for images function downloadResource(url, filename, buttonToFlash, resourceType = 'unknown') { GM_xmlhttpRequest({ method: "GET", url: url, responseType: 'blob', onload: function(response) { if (response.status === 200 && response.response) { const originalBlob = response.response; // attempt exif strip via canvas only for image types if (resourceType === 'image' && originalBlob.type.startsWith('image/')) { const img = document.createElement('img'); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const objectURL = URL.createObjectURL(originalBlob); // temp url for img src img.onload = () => { try { canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; ctx.drawImage(img, 0, 0); let mimeType = originalBlob.type; // ensure canvas export supports the type, default to jpeg if (!['image/jpeg', 'image/png', 'image/webp'].includes(mimeType)) { console.warn(`Enhancer: Unsupported MIME type for canvas: ${mimeType}. Using image/jpeg.`); mimeType = 'image/jpeg'; filename = filename.replace(/\.[^.]+$/, '.jpg'); // fix extension if changed } canvas.toBlob((processedBlob) => { if (processedBlob) { triggerDownload(processedBlob, filename, buttonToFlash, true); } else { console.error('Enhancer: Canvas toBlob failed. Downloading original.'); triggerDownload(originalBlob, filename, buttonToFlash, false); // fallback } URL.revokeObjectURL(objectURL); // cleanup img src url }, mimeType, 0.92); // quality for jpeg/webp } catch (e) { console.error("Enhancer: Error during canvas processing:", e); URL.revokeObjectURL(objectURL); triggerDownload(originalBlob, filename, buttonToFlash, false); // fallback } }; img.onerror = () => { console.error('Enhancer: Failed to load image for EXIF stripping. Downloading original.'); URL.revokeObjectURL(objectURL); triggerDownload(originalBlob, filename, buttonToFlash, false); // fallback }; img.src = objectURL; // load the blob into the img element } else { // download non-images directly triggerDownload(originalBlob, filename, buttonToFlash, true); } } else { console.error("Enhancer: Download failed:", response.statusText, response.status); if (buttonToFlash) flashButton(buttonToFlash, false); tryDirectDownload(url, filename, buttonToFlash); // fallback attempt } }, onerror: function(response) { console.error("Enhancer: Download network error:", response.error); if (buttonToFlash) flashButton(buttonToFlash, false); tryDirectDownload(url, filename, buttonToFlash); // fallback attempt } }); } // helper to create the download link and click it function triggerDownload(blob, filename, buttonToFlash, successStatus) { try { const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; document.body.appendChild(link); // required for firefox link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); // cleanup blob url if (buttonToFlash) flashButton(buttonToFlash, successStatus); } catch (e) { console.error("Enhancer: Error creating download link:", e); if (buttonToFlash) flashButton(buttonToFlash, false); } } // simple fallback using direct link (may not respect filename, no exif strip) function tryDirectDownload(url, filename, buttonToFlash) { try { const link = document.createElement('a'); link.href = url; link.download = filename; // works in chrome, less so elsewhere link.target = '_blank'; link.rel = 'noopener noreferrer'; document.body.appendChild(link); link.click(); document.body.removeChild(link); // cannot reliably flash success here } catch (err) { console.error("Enhancer: Direct download attempt failed:", err); if (buttonToFlash) flashButton(buttonToFlash, false); } } function createMetadataBox(params, type) { document.getElementById('metadata-box')?.remove(); // clear existing const metadataBox = document.createElement('div'); metadataBox.id = 'metadata-box'; metadataBox.className = 'meta-box'; // initially hidden by css (opacity 0) let contentHTML = ''; // common params if (params.model) contentHTML += createMetaRow('Model', params.model); if (params.prompt) contentHTML += createMetaRow('Prompt', params.prompt, true); // allow copy if (params.seed) contentHTML += createMetaRow('Seed', params.seed); if (params.private) contentHTML += createMetaRow('Private', params.private); // image specific if (type === 'image') { if (params.width && params.height) contentHTML += createMetaRow('Size', `${params.width} × ${params.height}`); if (params.guidance_scale) contentHTML += createMetaRow('Guidance', params.guidance_scale); // shorter label if (params.negative_prompt) contentHTML += createMetaRow('Negative', params.negative_prompt, true); // shorter label, allow copy if (params.strength) contentHTML += createMetaRow('Strength', params.strength); if (params.steps) contentHTML += createMetaRow('Steps', params.steps); } // audio specific if (type === 'audio') { if (params.voice) contentHTML += createMetaRow('Voice', params.voice); if (params.format) contentHTML += createMetaRow('Format', params.format); } // text specific if (type === 'text') { if (params.system) contentHTML += createMetaRow('System', params.system, true); // allow copy if (params.language) contentHTML += createMetaRow('Language', params.language); if (params.modalities) contentHTML += createMetaRow('Modalities', params.modalities); } if (!contentHTML.trim()) return; // don't create box if no content metadataBox.innerHTML = ` <div class="meta-header" title="Click to toggle details"> <span>${type.charAt(0).toUpperCase() + type.slice(1)} Parameters</span> <span class="toggle-icon">▼</span> </div> <div class="meta-content">${contentHTML}</div> `; document.body.appendChild(metadataBox); // click header to toggle visibility/collapse metadataBox.querySelector('.meta-header').addEventListener('click', () => { const box = metadataBox; // already have ref const becomesVisible = !box.classList.contains('visible'); box.classList.toggle('visible'); if (becomesVisible) { box.classList.remove('collapsed'); } else { box.classList.add('collapsed'); } updateMetadataToggleIcon(box); }); // add copy functionality to buttons in rows metadataBox.querySelectorAll('.copy-btn[data-copy-target]').forEach(copyBtn => { const targetLabel = copyBtn.getAttribute('data-copy-target'); const textToCopy = params[targetLabel.toLowerCase().replace(' ', '_')]; // handle multi-word labels like 'guidance scale' if (textToCopy) { copyBtn.addEventListener('click', (e) => { e.stopPropagation(); // prevent header click navigator.clipboard.writeText(textToCopy).then(() => { copyBtn.textContent = '✓'; // success indicator setTimeout(() => { copyBtn.textContent = '📋'; }, 1200); // revert icon }).catch(err => console.error(`Enhancer: Failed to copy ${targetLabel}:`, err)); }); } else { copyBtn.style.display = 'none'; // hide button if no text found } }); updateMetadataToggleIcon(metadataBox); // set initial icon state } function updateMetadataToggleIcon(box) { const icon = box?.querySelector('.toggle-icon'); if (icon) { // show down arrow if collapsed or hidden, up arrow if expanded/visible icon.textContent = box.classList.contains('collapsed') || !box.classList.contains('visible') ? '▼' : '▲'; } } function createMetaRow(label, value, addCopyButton = false) { if (!value) return ''; // basic html escaping for value display const cleanValue = String(value).replace(/</g, "<").replace(/>/g, ">"); const copyBtnHTML = addCopyButton ? `<span class="copy-btn" title="Copy ${label}" data-copy-target="${label}">📋</span>` : ''; return ` <div class="meta-row"> <div class="meta-label">${label}:</div> <div class="meta-value">${cleanValue}${copyBtnHTML}</div> </div> `; } // decode prompts, handle spaces encoded as '+' or '%20' function sanitizePrompt(text) { if (!text) return ''; try { let decoded = text; // attempt decoding multiple times for nested encoding for (let i = 0; i < 3; i++) { if (decoded.includes('%')) decoded = decodeURIComponent(decoded); else break; // stop if no more percent signs } // replace '+' with space and remove potential replacement characters return decoded.replace(/\+/g, ' ').replace(/\uFFFD/gu, '').trim(); } catch (e) { console.warn("Enhancer: Failed to fully decode prompt:", text, e); // fallback: basic space replacement return text.replace(/\+/g, ' ').replace(/%20/g, ' ').trim(); } } // helper for consistent timestamp format function getFormattedTimestamp() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); return `${year}${month}${day}${hours}${minutes}${seconds}`; // YYYYMMDDHHMMSS } function extractUrlParameters() { const urlParams = new URLSearchParams(window.location.search); const path = window.location.pathname; let rawPrompt = ''; // prefer 'prompt' param if present if (urlParams.has('prompt')) { rawPrompt = urlParams.get('prompt'); } else { // fallback: try to get prompt from the last part of the path const pathSegments = decodeURIComponent(path).split('/').filter(Boolean); if (pathSegments.length > 0) { const lastSegment = pathSegments[pathSegments.length - 1]; // basic check: assume it's a prompt if it's reasonably long and doesn't look like a file/id if (lastSegment.length > 5 && !lastSegment.match(/^[\w-]{8,}$/) && !lastSegment.includes('.')) { rawPrompt = lastSegment; } } } // return object with all relevant params, sanitising prompts return { prompt: sanitizePrompt(rawPrompt), model: urlParams.get('model') || '', seed: urlParams.get('seed') || '', voice: urlParams.get('voice') || '', width: urlParams.get('width') || '', height: urlParams.get('height') || '', system: sanitizePrompt(urlParams.get('system') || ''), private: urlParams.get('private') || '', // boolean-like format: urlParams.get('format') || '', // audio format modalities: urlParams.get('modalities') || '', guidance_scale: urlParams.get('guidance_scale') || '', negative_prompt: sanitizePrompt(urlParams.get('negative_prompt') || ''), strength: urlParams.get('strength') || '', steps: urlParams.get('steps') || '', language: urlParams.get('language') || '', }; } function addStyles() { GM_addStyle(` :root { --text-color: #f0f0f0; --bg-color: #1a1a1a; --accent: #3498db; --accent-hover: #2980b9; --light-bg: #f2f2f2; --light-text: #333333; --light-content-bg: #ffffff; --dark-content-bg: #2d2d2d; --border-color-dark: #444; --border-color-light: #ddd; --button-bg: #333; /* Unused currently */ --button-hover-bg: #444; /* Unused currently */ --success-color: #27ae60; --error-color: #e74c3c; --code-bg-dark: #1e1e1e; --code-bg-light: #f8f8f8; --code-header-bg-dark: linear-gradient(90deg, rgba(30,30,30,1) 0%, rgba(45,45,45,1) 100%); --code-header-bg-light: linear-gradient(90deg, rgba(245,245,245,1) 0%, rgba(235,235,235,1) 100%); --blockquote-border: #3498db; /* Same as accent */ --blockquote-bg-dark: rgba(255,255,255,0.05); --blockquote-bg-light: rgba(0,0,0,0.03); } body { transition: background-color 0.3s, color 0.3s; } body.theme-light { background-color: var(--light-bg) !important; color: var(--light-text) !important; } body.theme-dark { background-color: var(--bg-color) !important; color: var(--text-color) !important; } /* Buttons */ .p-btn { background-color: var(--accent); color: white; border: none; border-radius: 50%; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; cursor: pointer; /* margin: 5px; Removed, using gap in container */ transition: all 0.2s ease-in-out; box-shadow: 0 2px 5px rgba(0,0,0,0.2); flex-shrink: 0; /* Prevent shrinking in flex container */ } .p-btn:hover { background-color: var(--accent-hover); transform: scale(1.05); } .p-btn svg { pointer-events: none; /* Prevent svg from capturing clicks */ } .font-size-controls { display: flex; gap: 5px; justify-content: flex-end; /* Align to right within its space */ } .font-size-controls .p-btn { width: 30px; /* Slightly smaller font buttons */ height: 30px; } /* Metadata Box */ .meta-box { position: fixed; bottom: 20px; right: 20px; background-color: rgba(45, 45, 45, 0.9); color: var(--text-color); border-radius: 8px; padding: 12px; max-width: 320px; width: auto; font-size: 13px; line-height: 1.5; z-index: 9998; transition: opacity 0.3s, transform 0.3s, height 0.3s ease-out, padding 0.3s ease-out; box-shadow: 0 4px 15px rgba(0,0,0,0.3); opacity: 0; transform: translateY(10px); pointer-events: none; overflow: hidden; backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); } body.theme-light .meta-box { background-color: rgba(255, 255, 255, 0.9); color: var(--light-text); } .meta-box.visible { opacity: 1; transform: translateY(0); pointer-events: auto; } .meta-box.collapsed { /* Animate height collapse */ height: 24px; /* Approx height of header */ padding-top: 5px; padding-bottom: 5px; min-height: 24px; overflow: hidden; } .meta-box.collapsed .meta-content { display: none; } .meta-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; cursor: pointer; user-select: none; font-weight: bold; } .meta-box.collapsed .meta-header { margin-bottom: 0; /* Remove margin when collapsed */ } .meta-header .toggle-icon { font-size: 16px; padding: 0 5px; transition: transform 0.2s; /* Rotate icon */ } .meta-content { margin-top: 5px; } .meta-row { display: flex; margin-bottom: 5px; align-items: baseline; /* Align label and value nicely */ } .meta-label { font-weight: 600; width: 75px; /* Slightly wider label */ color: #aaa; flex-shrink: 0; padding-right: 5px; } body.theme-light .meta-label { color: #555; } .meta-value { flex: 1; word-break: break-word; /* Wrap long values */ display: inline; /* Helps with copy button alignment */ } .copy-btn { display: inline-block; /* Make it inline with text */ cursor: pointer; margin-left: 8px; font-size: 14px; opacity: 0.6; transition: opacity 0.2s; vertical-align: middle; /* Align with text */ user-select: none; } .copy-btn:hover { opacity: 1; } /* Link Preview */ .preview { position: absolute; display: none; max-width: 350px; background-color: #383838; color: #eee; border-radius: 6px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.4); padding: 10px 12px; font-size: 13px; line-height: 1.4; z-index: 10001; pointer-events: none; transition: opacity 0.15s ease-in-out; opacity: 0; border: 1px solid rgba(255, 255, 255, 0.1); } body.theme-light .preview { background-color: white; color: #333; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); border: 1px solid #eee; } .preview.active { opacity: 1; pointer-events: auto; /* Allow interaction when visible */ display: block; } .preview-header { display: flex; align-items: center; margin-bottom: 6px; font-weight: 600; color: #f5f5f5; /* Slightly off-white */ } body.theme-light .preview-header { color: #444; } .preview-icon { width: 16px; height: 16px; margin-right: 7px; border-radius: 3px; background-color: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; color: white; overflow: hidden; flex-shrink: 0; text-align: center; } .preview-icon img { width: 100%; height: 100%; object-fit: cover; } .preview-content { max-height: 100px; /* Limit height */ overflow: hidden; text-overflow: ellipsis; /* Consider white-space: normal; if needed */ } .preview-url { font-size: 11px; color: #aaa; margin-top: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } body.theme-light .preview-url { color: #777; } /* Text Enhancement Styles */ body.text-enhanced { font-family: Arial, sans-serif !important; /* Base font size set by applyFontSize */ line-height: 1.55 !important; /* Slightly more spacing */ } .content-container, .content-container-generated { max-width: 850px !important; margin: 25px auto !important; /* More top/bottom margin */ padding: 25px 35px !important; /* More padding */ border-radius: 8px !important; /* Font size applied via JS */ } body.theme-dark .content-container, body.theme-dark .content-container-generated { background-color: var(--dark-content-bg) !important; box-shadow: 0 3px 10px rgba(0,0,0,0.2); } body.theme-light .content-container, body.theme-light .content-container-generated { background-color: var(--light-content-bg) !important; box-shadow: 0 2px 10px rgba(0,0,0,0.08) !important; } /* Headings */ .content-container h1, .content-container h2, .content-container h3, .content-container h4, .content-container h5, .content-container h6 { margin-top: 1.2em !important; margin-bottom: 0.5em !important; font-weight: 600; line-height: 1.2; } body.theme-dark .content-container h1, body.theme-dark .content-container h2, body.theme-dark .content-container h3 { color: #eee !important; } body.theme-light .content-container h1, body.theme-light .content-container h2, body.theme-light .content-container h3 { color: #222 !important; } .content-container h1 { font-size: 2.1em !important; border-bottom: 1px solid var(--border-color-dark); padding-bottom: 0.3em; } .content-container h2 { font-size: 1.7em !important; border-bottom: 1px solid var(--border-color-dark); padding-bottom: 0.3em; } .content-container h3 { font-size: 1.4em !important; } .content-container h4 { font-size: 1.2em !important; } .content-container h5 { font-size: 1.1em !important; } .content-container h6 { font-size: 1.0em !important; color: #888; } /* Dim h6 */ body.theme-light .content-container h1, body.theme-light .content-container h2 { border-bottom-color: var(--border-color-light); } /* Links */ .content-container a { color: var(--accent) !important; text-decoration: none !important; transition: color 0.2s; } .content-container a:hover { color: var(--accent-hover) !important; text-decoration: underline !important; } /* Blockquotes */ .content-container blockquote, .content-container .blockquote-single, .content-container .blockquote-multi { border-left: 4px solid var(--blockquote-border) !important; padding: 10px 18px !important; /* More padding */ margin: 1em 0 !important; border-radius: 0 4px 4px 0; /* Rounded right corners */ font-style: italic; /* Often blockquotes are italicized */ } body.theme-dark .content-container blockquote, body.theme-dark .content-container .blockquote-single, body.theme-dark .content-container .blockquote-multi { color: #ccc !important; background-color: var(--blockquote-bg-dark); } body.theme-light .content-container blockquote, body.theme-light .content-container .blockquote-single, body.theme-light .content-container .blockquote-multi { color: #555 !important; background-color: var(--blockquote-bg-light); } /* Lists - Handled by divs generated in JS */ .content-container .list-item { margin-bottom: 0.3em; line-height: 1.4; } .content-container .numbered-item { font-weight: normal; /* Less bold than header */ } .content-container .bullet-item::before { content: "•"; /* Standard bullet */ margin-right: 8px; color: var(--accent); /* Themed bullet */ } .content-container .nested-bullet-item::before { content: "◦"; /* Open circle for nested */ margin-right: 8px; color: var(--accent); } /* Ensure JS-generated padding is respected */ .content-container .bullet-item, .content-container .nested-bullet-item { display: list-item; /* Use list-item display for pseudo-elements */ list-style-position: inside; /* Keep bullet inside padding */ list-style-type: none; /* Hide default browser bullet */ } /* Horizontal Rule */ .content-container hr.markdown-hr { border: 0; height: 1px; background: var(--border-color-dark); margin: 1.5em 0; /* More spacing around hr */ } body.theme-light .content-container hr.markdown-hr { background: var(--border-color-light); } /* Bold / Italic / Strikethrough */ .content-container strong, .content-container b { font-weight: 600 !important; } .content-container em, .content-container i { font-style: italic !important; } .content-container del { text-decoration: line-through; color: #888; } /* Dim strikethrough */ /* Code Block Styling */ .code-block-container { position: relative; background-color: var(--code-bg-dark) !important; border: 1px solid var(--border-color-dark); border-radius: 6px !important; margin: 1.2em 0 !important; /* Consistent margin */ padding: 0 !important; /* Remove padding, handled by children */ overflow: hidden !important; } body.theme-light .code-block-container { background-color: var(--code-bg-light) !important; border-color: var(--border-color-light); } .code-header { display: flex; justify-content: space-between; align-items: center; padding: 6px 12px; /* Slightly tighter header */ background: var(--code-header-bg-dark); border-bottom: 1px solid var(--border-color-dark); font-family: 'Consolas', 'Monaco', monospace !important; font-size: 12px; color: #ccc; } body.theme-light .code-header { background: var(--code-header-bg-light); border-bottom: 1px solid var(--border-color-light); color: #555; } .code-language { color: #aaa; text-transform: lowercase; font-weight: bold; } body.theme-light .code-language { color: #666; } .code-copy-btn { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #fff; font-size: 11px; padding: 3px 8px; /* Smaller padding */ border-radius: 4px; cursor: pointer; transition: all 0.2s; font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; user-select: none; } body.theme-light .code-copy-btn { background: rgba(0,0,0,0.05); border: 1px solid rgba(0,0,0,0.15); color: #333; } .code-copy-btn:hover { background-color: rgba(255,255,255,0.2); transform: translateY(-1px); box-shadow: 0 1px 3px rgba(0,0,0,0.2); } body.theme-light .code-copy-btn:hover { background-color: rgba(0,0,0,0.1); } /* Actual code area */ .code-block-container code { display: block; padding: 15px !important; /* Padding inside code area */ overflow-x: auto !important; /* Allow horizontal scroll */ font-family: 'Consolas', 'Monaco', monospace !important; font-size: 0.92em; /* Slightly adjust size */ line-height: 1.5; color: #e9e9e9; background: transparent !important; /* Ensure no extra bg */ border-radius: 0 !important; /* No radius on code tag itself */ } body.theme-light .code-block-container code { color: #2d2d2d !important; } /* Inline code */ .content-container code.inline-code { font-family: 'Consolas', 'Monaco', monospace !important; background-color: rgba(128, 128, 128, 0.15) !important; padding: 2px 5px !important; border-radius: 4px !important; font-size: 0.9em; color: inherit; /* Inherit surrounding text color */ vertical-align: baseline; /* Align nicely with text */ } body.theme-dark .content-container code.inline-code { background-color: rgba(255, 255, 255, 0.1) !important; } body.theme-light .content-container code.inline-code { background-color: rgba(0, 0, 0, 0.08) !important; } `); } })();