// ==UserScript==
// @name Pollinations.ai Enhancer
// @namespace https://greasyfork.org/en/users/1462897-fisventurous
// @version 1.9.4
// @description Enhanced markdown formatting for pollinations.ai with better readability, and smoother viewing
// @author fisventurous
// @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;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
function init() {
const hostname = window.location.hostname;
if (!hostname.startsWith('text.pollinations.ai') && !hostname.startsWith('image.pollinations.ai')) {
console.log("Pollinations Enhancer: Skipping execution on unsupported domain:", hostname);
if (document.readyState === "loading") {
document.removeEventListener("DOMContentLoaded", init);
}
return;
}
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");
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;
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;
}
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;
}
}
if (!isImage && !isAudio && !isText) {
if (document.querySelector('img:not([width="16"][height="16"])')) {
isImage = true;
} else if (
document.querySelector('audio, video, [type*="audio"]')
) {
isAudio = true;
} else {
isText = true;
}
}
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
) {
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");
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);
updateThemeToggleButton(newTheme === "theme-dark");
}
function updateThemeToggleButton(isDark) {
const btn = document.getElementById("theme-toggle-btn");
if (btn) {
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 && link.href) {
clearTimeout(hideTimeout);
if (currentLink !== link) {
currentLink = link;
clearTimeout(showTimeout);
showTimeout = setTimeout(
() => showPreview(link, preview),
200,
);
}
}
});
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);
}, 200);
}
});
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",
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) {}
let contentHTML = link.textContent?.trim() || url;
if (link.closest("pre, code"))
contentHTML = `<code class="inline-code">${contentHTML}</code>`;
preview.innerHTML = `<div class="preview-header"><div class="preview-icon">${faviconUrl ? `<img src="${faviconUrl}" alt="" onerror="this.style.display='none'; this.parentElement.textContent='${sourceName.charAt(0).toUpperCase()}';" />` : sourceName.charAt(0).toUpperCase()}</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";
requestAnimationFrame(() => {
const previewRect = preview.getBoundingClientRect();
const winWidth = window.innerWidth,
winHeight = window.innerHeight;
let top = rect.bottom + window.scrollY + 5,
left = rect.left + window.scrollX;
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");
}
}
function enhanceTextPage() {
document.body.classList.add("text-enhanced");
const params = extractUrlParameters();
let contentContainer = null;
let originalContent = "";
try {
contentContainer = document.querySelector(
"main:not(:empty), article:not(:empty), .content:not(:empty), #content:not(:empty), .main-content:not(:empty), .post-content:not(:empty)",
);
if (
!contentContainer &&
document.body.children.length === 1 &&
document.body.firstElementChild?.tagName === "PRE"
) {
contentContainer = document.body.firstElementChild;
}
if (
!contentContainer &&
(document.body.innerText || document.body.textContent || "")
.trim().length > 50
) {
contentContainer = document.createElement("div");
contentContainer.className = "content-container-generated";
while (
document.body.firstChild &&
(!document.body.firstChild.matches ||
!document.body.firstChild.matches(
"#pollinations-enhancer-buttons, .preview, script, style, #metadata-box",
))
) {
contentContainer.appendChild(document.body.firstChild);
}
document.body.appendChild(contentContainer);
}
if (contentContainer) {
contentContainer.classList.add("content-container");
originalContent =
contentContainer.innerText ||
contentContainer.textContent ||
"";
if (params.json === "true") {
const jsonText = originalContent;
contentContainer.innerHTML = "";
const pre = document.createElement("pre");
pre.className = "code-block-container";
const header = document.createElement("div");
header.className = "code-header";
const langSpan = document.createElement("span");
langSpan.className = "code-language";
langSpan.textContent = "json";
const copyBtn = document.createElement("button");
copyBtn.className = "code-copy-btn";
copyBtn.title = "Copy code";
copyBtn.textContent = "Copy";
copyBtn.addEventListener("click", () => {
navigator.clipboard
.writeText(jsonText)
.then(() => {
const originalText = copyBtn.textContent;
copyBtn.textContent = "Copied!";
setTimeout(() => {
copyBtn.textContent = originalText;
}, 1500);
})
.catch((err) => {
console.error("Failed to copy JSON:", err);
});
});
header.appendChild(langSpan);
header.appendChild(copyBtn);
const code = document.createElement("code");
code.className = "language-json";
code.textContent = jsonText;
pre.appendChild(header);
pre.appendChild(code);
contentContainer.appendChild(pre);
} else {
if (contentContainer.tagName !== "PRE") {
processMarkdown(contentContainer);
} else {
const preContent = contentContainer.textContent || "";
contentContainer.innerHTML = "";
const preWrapper = document.createElement("pre");
preWrapper.className = "code-block-container";
const header = document.createElement("div");
header.className = "code-header";
const langSpan = document.createElement("span");
langSpan.className = "code-language";
langSpan.textContent = "text";
const copyBtn = document.createElement("button");
copyBtn.className = "code-copy-btn";
copyBtn.title = "Copy code";
copyBtn.textContent = "Copy";
copyBtn.addEventListener("click", () => {
navigator.clipboard
.writeText(preContent)
.then(() => {
const originalText = copyBtn.textContent;
copyBtn.textContent = "Copied!";
setTimeout(() => {
copyBtn.textContent = originalText;
}, 1500);
})
.catch((err) => {
console.error(
"Failed to copy preformatted text:",
err,
);
});
});
header.appendChild(langSpan);
header.appendChild(copyBtn);
const code = document.createElement("code");
code.className = "language-text";
code.textContent = preContent;
preWrapper.appendChild(header);
preWrapper.appendChild(code);
contentContainer.appendChild(preWrapper);
}
}
} else {
console.warn(
"Pollinations Enhancer: Could not find 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;
container.innerHTML = container.innerHTML
.replace(/<span class="[^"]*">/g, "")
.replace(/<\/span>/g, "");
const codeBlocks = [];
let codeBlockCount = 0;
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, "`"),
});
codeBlockCount++;
return placeholder;
},
);
const originalHtml = container.innerHTML;
const content = container.textContent;
const blockquoteHtml = processBlockquotes(content);
if (blockquoteHtml !== content) {
const processedWithCodeBlocksPreserved = restoreCodeBlocks(
blockquoteHtml,
originalHtml,
);
container.innerHTML = processedWithCodeBlocksPreserved;
}
container.innerHTML = container.innerHTML.replace(
/^(#{1,6})\s+(.+)$/gm,
function (match, hashes, content) {
const level = hashes.length;
return `<h${level}>${content}</h${level}>`;
},
);
container.innerHTML = container.innerHTML.replace(
/^---+$/gm,
'<hr class="markdown-hr">',
);
container.innerHTML = processLists(container.innerHTML);
container.innerHTML = container.innerHTML
.replace(/\*\*(.*?)\*\*|__(.*?)__/g, "<strong>$1$2</strong>")
.replace(/\*(.*?)\*|_(.*?)_/g, "<em>$1$2</em>")
.replace(/~~(.*?)~~/g, "<del>$1</del>")
.replace(
/\[([^\]]+?)\]\((https?:\/\/[^)]+)\)/g,
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
)
.replace(/`([^`]+?)`/g, '<code class="inline-code">$1</code>');
for (let i = 0; i < codeBlocks.length; i++) {
const block = codeBlocks[i];
const html = `<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,
html,
);
}
const copyButtons = container.querySelectorAll(".code-copy-btn");
copyButtons.forEach((btn) => {
btn.addEventListener("click", () => {
const codeBlock = btn.parentElement.nextElementSibling;
const textToCopy = codeBlock.textContent;
navigator.clipboard
.writeText(textToCopy)
.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(">>>")) {
if (inMultiBlockquote) {
result.push(
`<blockquote class="blockquote-multi">${blockquoteContent.join(" ")}</blockquote>`,
);
}
inMultiBlockquote = true;
const initial = trimmedLine.substring(3).trim();
blockquoteContent = initial ? [initial] : [];
continue;
}
if (inMultiBlockquote) {
if (trimmedLine === "" || trimmedLine.startsWith(">")) {
result.push(
`<blockquote class="blockquote-multi">${blockquoteContent.join(" ")}</blockquote>`,
);
inMultiBlockquote = false;
blockquoteContent = [];
if (trimmedLine === "") {
result.push("");
continue;
}
} else {
blockquoteContent.push(trimmedLine);
continue;
}
}
if (trimmedLine === ">") {
result.push("");
continue;
}
if (trimmedLine.startsWith("> ")) {
const content = trimmedLine.substring(2);
if (content) {
result.push(
`<blockquote class="blockquote-single">${content}</blockquote>`,
);
} else {
result.push("");
}
continue;
}
result.push(line);
}
if (inMultiBlockquote) {
result.push(
`<blockquote class="blockquote-multi">${blockquoteContent.join(" ")}</blockquote>`,
);
}
return result.join("\n");
}
function restoreCodeBlocks(blockquoteHtml, originalHtml) {
const placeholders = [];
const regex = /<!--CODEBLOCK_\d+-->/g;
let match;
while ((match = regex.exec(originalHtml)) !== null) {
placeholders.push(match[0]);
}
if (placeholders.length === 0) {
return blockquoteHtml;
}
const blockquoteLines = blockquoteHtml.split("\n");
const originalLines = originalHtml.split("\n");
let placeholderIndex = 0;
for (let i = 0; i < originalLines.length; i++) {
if (
originalLines[i].includes("<!--CODEBLOCK_") &&
placeholderIndex < placeholders.length
) {
const placeholder = placeholders[placeholderIndex];
const insertPosition = Math.min(i, blockquoteLines.length);
blockquoteLines.splice(insertPosition, 0, placeholder);
placeholderIndex++;
}
}
return blockquoteLines.join("\n");
}
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(/^(\d+)\.\s+(.*)/);
const dashMatch = line.match(/^[-*]\s+(.*)/);
if (numberedMatch) {
const num = numberedMatch[1];
const content = numberedMatch[2];
result.push(
`<div class="numbered-item">${num}. ${content}</div>`,
);
lastWasNumbered = true;
} else if (dashMatch && lastWasNumbered) {
const content = dashMatch[1];
result.push(
`<div class="nested-bullet-item">${content}</div>`,
);
} else if (dashMatch) {
const content = dashMatch[1];
result.push(`<div class="bullet-item">${content}</div>`);
} else if (line.trim() === "") {
result.push(line);
lastWasNumbered = false;
} else {
result.push(line);
lastWasNumbered = false;
}
}
return result.join("\n");
}
function enhanceImagePage() {
let mainImage = null;
removeExistingButtons();
try {
const directImages = document.querySelectorAll("body > img");
if (directImages.length > 0) {
let largestArea = 0;
directImages.forEach((img) => {
if (img.complete) {
const area = img.naturalWidth * img.naturalHeight;
if (area > largestArea) {
largestArea = area;
mainImage = img;
}
} else {
img.addEventListener("load", () => {
setTimeout(enhanceImagePage, 100);
});
}
});
}
if (!mainImage) {
const allImages = document.querySelectorAll("img");
let largestArea = 0;
allImages.forEach((img) => {
if (img.width < 30 || img.height < 30) return;
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) {
params.width = mainImage.naturalWidth || mainImage.width || 0;
params.height = mainImage.naturalHeight || mainImage.height || 0;
createCommonButtons(params, "image", mainImage);
} else {
setTimeout(enhanceImagePage, 500);
}
}
function enhanceAudioPage() {
removeExistingButtons();
const params = extractUrlParameters();
let audioSrc = findAudioSource();
if (!audioSrc) {
audioSrc = window.location.href;
}
createCommonButtons(params, "audio", null, "", audioSrc);
}
function removeExistingButtons() {
const existingContainer = document.getElementById(
"pollinations-enhancer-buttons",
);
if (existingContainer) existingContainer.remove();
const existingMetadata = document.getElementById("metadata-box");
if (existingMetadata) existingMetadata.remove();
}
function findAudioSource() {
const audioExtRegex = /\.(mp3|wav|ogg|m4a|flac|aac)(\?|$)/i;
const audioElements = document.querySelectorAll("audio, video");
for (const el of audioElements) {
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;
}
}
}
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) {}
}
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;
}
}
const metaOgAudio = document.querySelector(
'meta[property="og:audio"], meta[property="og:audio:url"]',
);
if (metaOgAudio && metaOgAudio.content) {
return metaOgAudio.content;
}
const htmlContent = document.documentElement.outerHTML;
const audioUrlMatches = htmlContent.match(
/https?:\/\/[^"'\s]+\.(mp3|wav|ogg|m4a)(\?[^"'\s]*)?/gi,
);
if (audioUrlMatches && audioUrlMatches.length) {
return audioUrlMatches[0];
}
return null;
}
function createCommonButtons(
params,
type,
targetElement = null,
originalContent = "",
resourceUrl = "",
) {
try {
removeExistingButtons();
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;";
if (type === "text") {
const themeToggleBtn = createButton(
"theme-toggle-btn",
"Toggle theme (Light/Dark)",
"",
toggleTheme,
);
buttonContainer.appendChild(themeToggleBtn);
}
if (type === "text" && 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>',
() => {
navigator.clipboard
.writeText(
originalContent ||
(targetElement.innerText ||
targetElement.textContent ||
""),
)
.then(
() => flashButton(copyContentBtn, true),
() => flashButton(copyContentBtn, false),
);
},
);
buttonContainer.appendChild(copyContentBtn);
const fontSizeContainer = document.createElement("div");
fontSizeContainer.className = "font-size-controls";
fontSizeContainer.style.cssText =
"position: relative; right: 41px; display: flex; gap: 5px;";
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,
);
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,
);
fontSizeContainer.appendChild(increaseFontBtn);
fontSizeContainer.appendChild(decreaseFontBtn);
buttonContainer.appendChild(fontSizeContainer);
}
if (type === "image" && targetElement?.src) {
const saveImageBtn = createButton(
"save-image-btn",
"Save image (Stripped EXIF)",
'<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,
`pollinations-image-${Date.now()}.jpg`,
saveImageBtn,
"image",
),
);
buttonContainer.appendChild(saveImageBtn);
}
if (type === "audio" && resourceUrl) {
const saveAudioBtn = createButton(
"save-audio-btn",
"Save audio",
'<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,
`pollinations-audio-${Date.now()}.mp3`,
saveAudioBtn,
"audio",
),
);
buttonContainer.appendChild(saveAudioBtn);
}
const hasMetadata =
params.model ||
params.prompt ||
params.seed ||
params.voice ||
params.width ||
params.system ||
params.private ||
params.nologo;
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>',
() => {
const box = document.getElementById("metadata-box");
if (box) {
box.classList.toggle("visible");
if (box.classList.contains("visible"))
box.classList.remove("collapsed");
updateMetadataToggleIcon(box);
}
},
);
buttonContainer.appendChild(metadataBtn);
createMetadataBox(params, type);
}
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);
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);
GM_setValue(FONT_SIZE_KEY, newScale);
applyFontSize();
}
function applyFontSize() {
const scale = GM_getValue(FONT_SIZE_KEY, "1") || "1";
const existingStyle = document.getElementById("font-size-style");
if (existingStyle) existingStyle.remove();
const styleEl = document.createElement("style");
styleEl.id = "font-size-style";
styleEl.textContent = `
.content-container {
font-size: calc(17px * ${scale}) !important;
line-height: 1.5 !important;
transform: scale(1) !important;
}
`;
document.head.appendChild(styleEl);
}
function createButton(id, title, innerHTML, onClick) {
const btn = document.createElement("div");
btn.id = id;
btn.className = "p-btn";
btn.title = title;
btn.innerHTML = innerHTML;
btn.addEventListener("click", onClick);
btn.style.position = "relative";
btn.style.margin = "0";
return btn;
}
function flashButton(button, success) {
if (!button) return;
const originalColor = button.style.backgroundColor;
button.style.backgroundColor = success
? "var(--success-color)"
: "var(--error-color)";
button.style.transform = "scale(1.1)";
setTimeout(() => {
if (button) {
button.style.backgroundColor = originalColor;
button.style.transform = "scale(1)";
}
}, 1000);
}
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;
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);
img.onload = () => {
try {
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0);
let mimeType = originalBlob.type;
if (
![
"image/jpeg",
"image/png",
"image/webp",
].includes(mimeType)
) {
mimeType = "image/jpeg";
filename = filename.replace(
/\.[^.]+$/,
".jpg",
);
}
canvas.toBlob(
(processedBlob) => {
if (processedBlob) {
triggerDownload(
processedBlob,
filename,
buttonToFlash,
true,
);
} else {
triggerDownload(
originalBlob,
filename,
buttonToFlash,
false,
);
}
URL.revokeObjectURL(objectURL);
},
mimeType,
0.92,
);
} catch (e) {
URL.revokeObjectURL(objectURL);
triggerDownload(
originalBlob,
filename,
buttonToFlash,
false,
);
}
};
img.onerror = () => {
URL.revokeObjectURL(objectURL);
triggerDownload(
originalBlob,
filename,
buttonToFlash,
false,
);
};
img.src = objectURL;
} else {
triggerDownload(
originalBlob,
filename,
buttonToFlash,
true,
);
}
} else {
if (buttonToFlash) flashButton(buttonToFlash, false);
tryDirectDownload(url, filename, buttonToFlash);
}
},
onerror: function (response) {
if (buttonToFlash) flashButton(buttonToFlash, false);
tryDirectDownload(url, filename, buttonToFlash);
},
});
}
function triggerDownload(blob, filename, buttonToFlash, successStatus) {
try {
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
if (buttonToFlash) flashButton(buttonToFlash, successStatus);
} catch (e) {
if (buttonToFlash) flashButton(buttonToFlash, false);
}
}
function tryDirectDownload(url, filename, buttonToFlash) {
try {
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.target = "_blank";
link.rel = "noopener noreferrer";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (err) {
if (buttonToFlash) flashButton(buttonToFlash, false);
}
}
function createMetadataBox(params, type) {
const existingBox = document.getElementById("metadata-box");
if (existingBox) existingBox.remove();
const metadataBox = document.createElement("div");
metadataBox.id = "metadata-box";
metadataBox.className = "meta-box";
let contentHTML = "";
if (params.model) contentHTML += createMetaRow("Model", params.model);
if (params.prompt)
contentHTML += createMetaRow("Prompt", params.prompt, true);
if (params.seed) contentHTML += createMetaRow("Seed", params.seed);
if (params.private)
contentHTML += createMetaRow("Private", params.private);
if (params.json) contentHTML += createMetaRow("JSON Mode", params.json);
if (type === "image") {
if (params.width && params.height)
contentHTML += createMetaRow(
"Size",
`${params.width} × ${params.height}`,
);
if (params.guidance_scale)
contentHTML += createMetaRow(
"Guidance Scale",
params.guidance_scale,
);
if (params.negative_prompt)
contentHTML += createMetaRow(
"Negative Prompt",
params.negative_prompt,
true,
);
if (params.strength)
contentHTML += createMetaRow("Strength", params.strength);
if (params.steps)
contentHTML += createMetaRow("Steps", params.steps);
if (params.nologo)
contentHTML += createMetaRow("No Logo", params.nologo);
}
if (type === "audio") {
if (params.voice)
contentHTML += createMetaRow("Voice", params.voice);
if (params.format)
contentHTML += createMetaRow("Format", params.format);
}
if (type === "text") {
if (params.system)
contentHTML += createMetaRow("System", params.system, true);
if (params.language)
contentHTML += createMetaRow("Language", params.language);
if (params.modalities)
contentHTML += createMetaRow("Modalities", params.modalities);
}
if (!contentHTML.trim()) return;
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);
metadataBox.querySelector(".meta-header").addEventListener("click", () => {
if (metadataBox.classList.contains("visible")) {
metadataBox.classList.toggle("collapsed");
} else {
metadataBox.classList.add("visible");
metadataBox.classList.remove("collapsed");
}
updateMetadataToggleIcon(metadataBox);
});
metadataBox
.querySelectorAll(".copy-btn[data-copy-target]")
.forEach((copyBtn) => {
const targetLabel = copyBtn.getAttribute("data-copy-target");
const paramKey = targetLabel.toLowerCase().replace(" ", "_");
const textToCopy = params[paramKey];
if (textToCopy) {
copyBtn.addEventListener("click", (e) => {
e.stopPropagation();
navigator.clipboard
.writeText(textToCopy)
.then(() => {
copyBtn.textContent = "✓";
setTimeout(() => {
copyBtn.textContent = "📋";
}, 1200);
})
.catch((err) =>
console.error(
`Failed to copy ${targetLabel}:`,
err,
),
);
});
} else {
copyBtn.style.display = "none";
}
});
updateMetadataToggleIcon(metadataBox);
}
function updateMetadataToggleIcon(box) {
const icon = box?.querySelector(".toggle-icon");
if (icon)
icon.textContent = box.classList.contains("collapsed") ? "▼" : "▲";
}
function createMetaRow(label, value, addCopyButton = false) {
if (!value) return "";
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>`;
}
function sanitizePrompt(text) {
if (!text) return "";
try {
let decoded = text;
for (let i = 0; i < 3; i++) {
if (decoded.includes("%")) decoded = decodeURIComponent(decoded);
else break;
}
return decoded.replace(/\+/g, " ").replace(/\uFFFD/gu, "").trim();
} catch (e) {
return text.replace(/\+/g, " ").replace(/%20/g, " ").trim();
}
}
function extractUrlParameters() {
const urlParams = new URLSearchParams(window.location.search);
const path = window.location.pathname;
let rawPrompt = "";
if (urlParams.has("prompt")) {
rawPrompt = urlParams.get("prompt");
} else {
const promptFromPath = decodeURIComponent(path)
.split("/")
.filter(Boolean)
.pop();
if (promptFromPath) rawPrompt = promptFromPath;
}
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") || "",
format: urlParams.get("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") || "",
json: urlParams.get("json") || "",
nologo: urlParams.get("nologo") || "",
};
}
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; --button-hover-bg: #444; --success-color: #27ae60; --error-color: #e74c3c; }
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; }
body { transition: background-color 0.3s, color 0.3s; }
.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; transition: all 0.2s; box-shadow: 0 2px 5px rgba(0,0,0,0.2); flex-shrink: 0; }
.p-btn:hover { background-color: var(--accent-hover); transform: scale(1.05); }
.p-btn svg { pointer-events: none; }
.font-size-controls { display: flex; gap: 5px; justify-content: center; }
.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; 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); }
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 { height: 24px; padding-top: 5px; padding-bottom: 5px; min-height: 24px; }
.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; }
.meta-header .toggle-icon { font-size: 16px; padding: 0 5px; }
.meta-content { margin-top: 5px; }
.meta-row { display: flex; margin-bottom: 5px; }
.meta-label { font-weight: 600; width: 70px; color: #aaa; flex-shrink: 0; }
body.theme-light .meta-label { color: #555; }
.meta-value { flex: 1; word-break: break-word; }
.copy-btn { display: inline-block; cursor: pointer; margin-left: 8px; font-size: 14px; opacity: 0.7; transition: opacity 0.2s; }
.copy-btn:hover { opacity: 1; }
.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; 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; display: block; }
.preview-header { display: flex; align-items: center; margin-bottom: 6px; font-weight: 600; color: #f5f5f5; }
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; color: white; overflow: hidden; flex-shrink: 0; }
.preview-icon img { width: 100%; height: 100%; object-fit: cover; }
.preview-content { line-height: 1.4; max-height: 100px; overflow: hidden; text-overflow: ellipsis; }
.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; }
body.text-enhanced { font-family: Arial, sans-serif !important; font-size: 17px !important; line-height: 1.5 !important; }
.content-container { max-width: 850px !important; margin: 20px auto !important; padding: 20px 30px !important; border-radius: 8px !important; }
body.theme-dark .content-container { background-color: var(--dark-content-bg) !important; box-shadow: 0 3px 10px rgba(0,0,0,0.2); }
body.theme-light .content-container { background-color: var(--light-content-bg) !important; box-shadow: 0 2px 10px rgba(0,0,0,0.08) !important; }
.content-container h1, .content-container h2, .content-container h3, .content-container h4, .content-container h5, .content-container h6 { margin-top: 0.8em !important; margin-bottom: 0.3em !important; font-weight: 600; line-height: 1.1; }
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 h3 + .code-block-container { margin-top: 0.25em !important; }
.content-container h3:has(+ .code-block-container) { margin-bottom: 0.25em !important; }
.content-container h1 { font-size: 2.0em !important; border-bottom: 1px solid var(--border-color-dark); padding-bottom: 0.2em; }
.content-container h2 { font-size: 1.6em !important; border-bottom: 1px solid var(--border-color-dark); padding-bottom: 0.2em;}
.content-container h3 { font-size: 1.3em !important; }
body.theme-light .content-container h1, body.theme-light .content-container h2 { border-bottom-color: var(--border-color-light); }
.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; }
.content-container blockquote,
.content-container .blockquote-single,
.content-container .blockquote-multi {
border-left: 4px solid var(--accent) !important;
padding: 8px 15px !important;
margin: 0.8em 0 !important;
background-color: rgba(128,128,128,0.08);
border-radius: 0 4px 4px 0;
}
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: rgba(255,255,255,0.05);
}
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: rgba(0,0,0,0.03);
}
.content-container ul, .content-container ol { list-style: disc; margin: 0; padding-left: 20px; }
.content-container ul li, .content-container ol li { margin: 0; padding: 0; line-height: 0.05; }
.content-container ul ul, .content-container ol ol, .content-container ul ol, .content-container ol ul { margin: 0.1em 0; }
.code-block-container ul { list-style: none; margin: 0; padding: 0; }
.code-block-container ul li { margin: 0; }
.content-container code:not(pre 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; }
.content-container pre { background-color: #262626 !important; border: 1px solid var(--border-color-dark); border-radius: 6px !important; padding: 15px !important; overflow-x: auto !important; margin: 1em 0 !important; font-family: 'Consolas', 'Monaco', monospace !important; font-size: 0.9em; line-height: 1.45; color: #eee; }
body.theme-light .content-container pre { background-color: #f8f8f8 !important; border-color: var(--border-color-light); color: #333 !important; }
.content-container pre code { background-color: transparent !important; padding: 0 !important; border-radius: 0 !important; font-size: inherit; }
.content-container strong, .content-container b { font-weight: 600 !important; }
.content-container em, .content-container i { font-style: italic !important; }
.code-block-container {
position: relative;
background-color: #1e1e1e !important;
border: 1px solid var(--border-color-dark);
border-radius: 6px !important;
margin: 0.50em 0 !important;
padding: 0 !important;
overflow: hidden !important;
}
body.theme-light .code-block-container {
background-color: #ffffff !important;
border-color: var(--border-color-light);
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: linear-gradient(90deg, rgba(30,30,30,1) 0%, rgba(45,45,45,1) 100%);
border-bottom: 1px solid #444;
font-family: 'Consolas', 'Monaco', monospace !important;
font-size: 12px;
}
body.theme-light .code-header {
background: linear-gradient(90deg, rgba(245,245,245,1) 0%, rgba(235,235,235,1) 100%);
border-bottom: 1px solid #ddd;
}
.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.3);
color: #fff;
font-size: 11px;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.5px;
}
body.theme-light .code-copy-btn {
background: rgba(0,0,0,0.05);
border: 1px solid rgba(0,0,0,0.2);
color: #333;
}
.code-copy-btn:hover {
background-color: rgba(255,255,255,0.2);
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
body.theme-light .code-copy-btn:hover {
background-color: rgba(0,0,0,0.1);
color: black;
}
.code-block-container code {
display: block;
padding: 15px !important;
overflow-x: auto !important;
font-family: 'Consolas', 'Monaco', monospace !important;
font-size: 0.9em;
line-height: 1.45;
color: #e9e9e9;
}
body.theme-light .code-block-container code {
color: #2d2d2d !important;
}
.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;
}
.content-container .numbered-item {
font-weight: bold;
margin: 0 0 0.1em 0;
line-height: 1.1;
}
.content-container .nested-bullet-item {
margin: 0 0 0.1em 0;
line-height: 1.1;
padding-left: 2em;
}
.content-container .nested-bullet-item:before {
content: "◦ ";
margin-right: 3px;
}
.content-container .bullet-item {
margin: 0 0 0.1em 0;
line-height: 1.1;
padding-left: 1em;
}
.content-container .bullet-item:before {
content: "• ";
margin-right: 3px;
}
.content-container hr.markdown-hr {
border: 0;
height: 1px;
background: var(--border-color-dark);
margin: 0.6em 0;
}
body.theme-light .content-container hr.markdown-hr {
background: var(--border-color-light);
}
`);
}
})();