划词后弹出浮窗,可编辑出处/作者和正文内容,重新生成时可选择更新哪些内容。
// ==UserScript==
// @name 划词分享 - 可编辑浮窗版 (修复版)
// @namespace http://tampermonkey.net/
// @version 3.3
// @description 划词后弹出浮窗,可编辑出处/作者和正文内容,重新生成时可选择更新哪些内容。
// @author Van
// @match *://*/*
// @grant GM_addStyle
// @connect *
// @run-at document-end
// @require https://cdnjs.cloudflare.com/ajax/libs/marked/16.3.0/lib/marked.umd.js
// @grant GM_notification
// @grant unsafeWindow
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ---------------------------------------------------------------------------
// 【重构部分】全局日志控制
// ---------------------------------------------------------------------------
/**
* 全局调试开关。
* 设为 false 时,所有 log 和 logError 将静默,不会在控制台输出任何信息。
* 在脚本发布或确认无误后,建议将此值设为 false。
*/
const DEBUG = true;
/**
* 全局日志方法。
* 替代 console.log,受 DEBUG 开关控制。
* @param {...any} args - 任意数量的参数,将被传递给 console.log
*/
function log(...args) {
if (DEBUG) {
console.log('[TextShare]', ...args);
}
}
/**
* 全局错误日志方法。
* 替代 console.error,受 DEBUG 开关控制。
* @param {...any} args - 任意数量的参数,将被传递给 console.error
*/
function logError(...args) {
if (DEBUG) {
console.error('[TextShare Error]', ...args);
}
}
// ---------------------------------------------------------------------------
// Toast notification styles
GM_addStyle(`
/* Loading spinner styles */
.tm-loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 10001;
display: flex;
justify-content: center;
align-items: center;
}
.tm-loading-spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
animation: tm-spin 1s linear infinite;
}
@keyframes tm-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.tm-toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
display: flex;
flex-direction: column;
gap: 10px;
}
.tm-toast {
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 20px;
border-radius: 5px;
font-family: sans-serif;
font-size: 14px;
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
.tm-toast.show {
opacity: 1;
}
`);
// Color schemes data
const colorSchemes = {
'scheme1': {
name: '方案1',
background: '#FFF9F3',
primaryTextColor: '#3A2C21',
sourceTextColor: '#403B36',
authorTextColor: '#67625E'
},
'scheme2': {
name: '方案2',
background: '#FAFAFA',
primaryTextColor: '#3A2C21',
sourceTextColor: "#403B36",
authorTextColor: '#67625E'
},
'scheme3': {
name: '方案3',
background: '#121212',
primaryTextColor: '#E8D7B0',
sourceTextColor: '#DCD2AE',
authorTextColor: '#ACA694'
},
'scheme4': {
name: '方案4',
background: '#F2F7F0',
primaryTextColor: '#3A2C21',
sourceTextColor: '#403B36',
authorTextColor: '#67625E'
}
};
let shareButton = null;
let selectionText = ''; // ✅ 正文内容,全局保存,不随重新生成改变
let selectionRect = null;
let currentModal = null;
let isPreviewOpen = false; // Track if preview modal is open
let loadingOverlay = null; // Loading overlay element
let currentColorScheme = 'scheme1'; // Default color scheme
let currentFont = '"Segoe UI", "Microsoft YaHei", sans-serif'; // Default font
let isContentEditable = false; // Whether content can be edited
let isMarkdown = false; // Whether content is Markdown format
// 显示淡入淡出通知
function showToast(message, duration = 3000) {
let toastContainer = document.querySelector('.tm-toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'tm-toast-container';
document.body.appendChild(toastContainer);
}
const toast = document.createElement('div');
toast.className = 'tm-toast';
toast.textContent = message;
toastContainer.appendChild(toast);
// Force reflow to apply initial opacity: 0 before transition
void toast.offsetWidth;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
toast.addEventListener('transitionend', () => {
toast.remove();
if (toastContainer.children.length === 0) {
toastContainer.remove();
}
}, { once: true });
}, duration);
}
// Show loading overlay
function showLoading() {
if (loadingOverlay) {
loadingOverlay.remove();
}
loadingOverlay = document.createElement('div');
loadingOverlay.className = 'tm-loading-overlay';
const spinner = document.createElement('div');
spinner.className = 'tm-loading-spinner';
loadingOverlay.appendChild(spinner);
document.body.appendChild(loadingOverlay);
}
// Hide loading overlay
function hideLoading() {
if (loadingOverlay) {
loadingOverlay.remove();
loadingOverlay = null;
}
}
/**
* 获取选区末尾相对于视口 left 和 top 位置的健壮方法
* @returns {{left: number, top: number}|{}} 返回坐标对象,如果无选区则返回 null
*/
function getSelectionEndPosition() {
const selection = window.getSelection();
// 1. 检查是否有选区且选区不为空
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
// isCollapsed 为 true 表示是光标,而不是选中区域
return {};
}
// 2. 获取 Range 对象
const range = selection.getRangeAt(0);
// 3. 获取所有覆盖范围的矩形
const clientRects = range.getClientRects();
if (clientRects.length === 0) {
// 在某些罕见情况下,可能无法获取矩形
return {};
}
// 4. 取最后一个矩形,它代表了选区末尾所在的那一行/块
const lastRect = clientRects[clientRects.length - 1];
// 我们要的是末尾的点,所以用 right 和 bottom
let endX = lastRect.right;
let endY = lastRect.bottom;
// 如果选区是折叠的(光标),`right` 和 `left` 相等,`bottom` 和 `top` 相等
// 我们可能想把“弹窗”放在光标的后面或下方一点,所以可以做微调
// 但根据题意,我们还是用精确的点
// 为了更普遍适用性,处理光标情况
if (selection.isCollapsed) {
endX = lastRect.left;
endY = lastRect.bottom; // 或者 lastRect.top + lastRect.height
}
return {
left: endX,
top: endY
};
}
/**
* 获取浏览器视口的中心点坐标
* @returns {Object} 一个包含 x 和 y 坐标的对象 {x: number, y: number}
*/
function getViewportCenter() {
// 获取视口的宽度和高度
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// 计算中心点坐标
const centerX = viewportWidth / 2;
const centerY = viewportHeight / 2;
// 返回一个包含坐标的对象,便于使用
return { x: centerX, y: centerY };
}
/**
* 健壮地获取用户在可视化区域内选中文本的**最后一个**矩形坐标。
* @returns {DOMRect | null} 返回最后一个选中块的边界信息(通常是选区的末尾)。
*/
function getLastSelectionRect() {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return null;
}
// 将核心逻辑抽离,以便在 shadow DOM 检测中复用
return getLastRectFromSelection(selection);
}
/**
* [核心修改点] 从一个给定的 Selection 对象中获取最后一个有效矩形的坐标。
* @param {Selection} selection - 来自 window 或 shadowRoot 的 selection 对象。
* @returns {DOMRect | null}
*/
function getLastRectFromSelection(selection) {
if (!selection || selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
if (range.collapsed) return null;
if (!range.commonAncestorContainer.isConnected) return null;
// === 关键变更在这里 ===
const rects = range.getClientRects();
if (rects.length === 0) {
return null; // 选区没有产生任何布局(例如在隐藏元素中)
}
// 获取列表中的最后一个矩形
const lastRect = rects[rects.length - 1];
// 最后一个矩形理论上也不应该为0尺寸
if (lastRect.width === 0 || lastRect.height === 0) {
// 在某些极端情况下,最后一行可能只有换行符等,可以逐个向前检查
// 为了简单和健壮性,这里我们直接返回null
return null;
}
// 可视区域校验
const viewport = {
top: 0,
left: 0,
bottom: window.innerHeight,
right: window.innerWidth,
};
const isVisible = !(
lastRect.bottom < viewport.top ||
lastRect.top > viewport.bottom ||
lastRect.right < viewport.left ||
lastRect.left > viewport.right
);
return isVisible ? lastRect : null;
}
/**
* [辅助函数] 用于处理 Shadow DOM 内的 INPUT/TEXTAREA 选区
* 其选区特性与常规DOM不同,通常返回元素自身的rect作为近似值即可。
*/
function getCoordsFromShadowFormControl(selection) {
const startNode = selection.anchorNode;
const targetElement = startNode.nodeType === Node.ELEMENT_NODE ? startNode : startNode.parentElement;
if (targetElement && ['INPUT', 'TEXTAREA'].includes(targetElement.nodeName)) {
// 对于表单元素,选区通常是连续的,getBoundingClientRect() 是唯一的选择
const rect = targetElement.getBoundingClientRect();
// 这里可以返回元素的rect,或者更精确地计算光标位置,但通常元素rect已足够
return rect;
}
return null;
}
/**
* 主事件处理器:穿透 Light DOM 和 Shadow DOM 获取选区最后一个矩形坐标
*/
function handleSelectionChange(path) {
// 1. 首先尝试在主文档中查找
let coords = getLastRectFromSelection(window.getSelection());
if (coords) {
log("从 Light DOM 获取到最后矩形坐标:", coords);
// showTooltipAtEnd(coords); // 在这里处理你的UI
// 例如,将弹窗的top定位到lastRect.bottom, left定位到lastRect.right
return coords;
}
// 2. 如果没找到,则探测 Shadow DOM
//const path = event.composedPath();
for (const el of path) {
if (el.shadowRoot && el.shadowRoot.mode === 'open') {
// 检查是否是表单控件
const shadowSelection = el.shadowRoot.getSelection();
const fcCoords = getCoordsFromShadowFormControl(shadowSelection);
if (fcCoords) {
log(`从 Shadow DOM 表单获取到坐标:`, fcCoords);
// showTooltipAtEnd(fcCoords);
return fcCoords;
}
const coords = getLastRectFromSelection(shadowSelection);
if (coords) {
log(`从 Shadow DOM (Host: <${el.tagName.toLowerCase()}>) 获取到最后矩形坐标:`, coords);
// showTooltipAtEnd(coords);
return coords;
}
}
}
log("未在任何地方找到有效的选区。");
return {};
}
// 创建并插入分享按钮
function createShareButton(path) {
if (shareButton) {
shareButton.remove();
shareButton = null;
}
const selection = window.getSelection();
if (!selection.rangeCount || selection.toString().trim().length === 0) {
return;
}
// Only update selectionText if preview modal is not open
if (!isPreviewOpen) {
selectionText = selection.toString().trim(); // ✅ 保存选中文本
}
shareButton = document.createElement('div');
shareButton.id = 'share-button';
shareButton.textContent = '分享';
shareButton.style.cssText = `
position: absolute;
background: #333;
color: white;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
transition: background 0.2s;
`;
shareButton.addEventListener('mouseenter', () => {
shareButton.style.background = '#555';
});
shareButton.addEventListener('mouseleave', () => {
shareButton.style.background = '#333';
});
shareButton.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
// Use setTimeout to make the function non-blocking
setTimeout(showPreview, 0);
});
let { bottom, height, left, right, top, width, x, y } = handleSelectionChange(path);
const textTop = bottom + window.scrollY + 8;
const textLeft = x + width;
log(`分享按钮X: ${textLeft}, Y: ${textTop}`)
shareButton.style.top = `${textTop}px`;
shareButton.style.left = `${textLeft}px`;
document.body.appendChild(shareButton);
}
// 显示预览浮窗
async function showPreview() {
log('[TextShare] showPreview started at', performance.now());
if (!selectionText) return;
// Show loading overlay
showLoading();
isPreviewOpen = true; // Set flag when preview opens
const now = new Date();
const day = now.getDate();
const month = now.toLocaleString('en-US', { month: 'long' }).toUpperCase();
const year = now.getFullYear();
const weekday = now.toLocaleDateString('zh-CN', { weekday: 'long' });
// 默认出处为页面标题,作者为空
const defaultSource = document.title || '未知来源';
const defaultAuthor = '-';
try {
log('About to call createQuoteImage at', performance.now());
const canvasStart = performance.now();
// Create canvas directly for better performance
const canvas = await createQuoteImage(selectionText, defaultSource, defaultAuthor, colorSchemes[currentColorScheme]);
const canvasEnd = performance.now();
log('createQuoteImage took', canvasEnd - canvasStart, 'ms');
const imageUrl = canvas.toDataURL('image/png');
createModal(imageUrl, defaultSource, defaultAuthor);
} catch (error) {
logError('生成图片失败:', error);
showToast('生成图片失败,请重试。');
} finally {
// Hide loading overlay
hideLoading();
log('showPreview finished at', performance.now());
}
}
// Create quote image using direct canvas operations for better performance
async function createQuoteImage(text, source, author, colorScheme) {
const dayFontSize = 120;
const monthYearFontSize = 32;
const weekdayFontSize = 18;
const textFontSize = 20;
const sourceFontSize = 18;
const authorFontSize = 18;
// Create canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set initial canvas dimensions
const width = 600;
canvas.width = width * 2; // Scale by 2 for better quality
// Set text properties
ctx.textAlign = 'center';
ctx.fillStyle = colorScheme.primaryTextColor;
ctx.scale(2, 2); // Apply scale to context for high DPI
// Get current date
const now = new Date();
const day = now.getDate();
const month = now.toLocaleString('en-US', { month: 'long' }).toUpperCase();
const year = now.getFullYear();
const weekday = now.toLocaleDateString('zh-CN', { weekday: 'long' });
const maxWidth = 520; // Maximum width for text (600px canvas width - 40px padding * 2);
const sourceMaxWidth = 300;
// Check if content is Markdown
if (isMarkdown) {
// Preprocess text to handle line breaks properly
// Convert single line breaks to double line breaks for Markdown
// const processedText = text.replace(/\n(?!\n)/g, '\n\n');
// Convert Markdown to HTML
const htmlContent = marked.parse(text);
// Create a temporary container to measure HTML content
const tempContainer = document.createElement('div');
tempContainer.innerHTML = htmlContent;
tempContainer.style.fontFamily = currentFont;
tempContainer.style.fontSize = `${textFontSize}px`;
tempContainer.style.color = colorScheme.primaryTextColor;
tempContainer.style.backgroundColor = colorScheme.background;
tempContainer.style.padding = '20px';
tempContainer.style.boxSizing = 'border-box';
tempContainer.style.width = `${width}px`;
tempContainer.style.position = 'absolute';
tempContainer.style.left = '-9999px';
tempContainer.style.top = '-9999px';
document.body.appendChild(tempContainer);
// Measure the height of the HTML content
const htmlHeight = tempContainer.offsetHeight;
document.body.removeChild(tempContainer);
// Calculate dimensions
const dimensions = calculateImageDimensions(ctx, text, source, dayFontSize, monthYearFontSize, weekdayFontSize, textFontSize, sourceFontSize, authorFontSize, maxWidth, sourceMaxWidth);
const {
headerHeight, dayY, monthYearY, weekdayY, separatorY,
textHeight, sourceHeight, footerHeight, totalHeight, lineHeight,
sourceLines
} = dimensions;
log('markdown dimensions', dimensions)
// Set canvas height
canvas.height = totalHeight * 2; // Scale by 2 for better quality
// Redraw with proper scaling
ctx.setTransform(2, 0, 0, 2, 0, 0); // Reset transform and reapply scale
// Fill background
ctx.fillStyle = colorScheme.background;
ctx.fillRect(0, 0, width, totalHeight);
// Draw header
ctx.fillStyle = colorScheme.primaryTextColor;
ctx.textAlign = 'center';
// Draw day (large)
ctx.font = `bold ${dayFontSize}px ${currentFont}`;
ctx.fillText(day, width / 2, dayY);
// Draw month and year
ctx.font = `${monthYearFontSize}px ${currentFont}`;
ctx.fillText(`${month} ${year}`, width / 2, monthYearY);
// Draw weekday
ctx.font = `${weekdayFontSize}px ${currentFont}`;
ctx.fillStyle = colorScheme.sourceTextColor;
ctx.fillText(weekday, width / 2, weekdayY);
// Draw separator line
ctx.beginPath();
ctx.moveTo(width / 2 - 40, separatorY);
ctx.lineTo(width / 2 + 40, separatorY);
ctx.strokeStyle = colorScheme.primaryTextColor;
ctx.globalAlpha = 0.3;
ctx.lineWidth = 1;
ctx.stroke();
ctx.globalAlpha = 1.0;
// Calculate text start Y position
const textStartY = separatorY + (totalHeight - headerHeight - footerHeight - textHeight) / 2 + lineHeight + 40;
// Draw HTML content directly on canvas
ctx.font = `${textFontSize}px ${currentFont}`;
ctx.fillStyle = colorScheme.primaryTextColor;
ctx.textAlign = 'left';
// Parse HTML content and draw it on canvas
drawHtmlContent(ctx, htmlContent, 40, textStartY, maxWidth, htmlHeight / 2, colorScheme);
// Draw source and author at the bottom
const sourceY = totalHeight - 60 - 18 - 15 - sourceHeight;
const authorY = totalHeight - 60;
// Draw source
ctx.font = `${sourceFontSize}px ${currentFont}`;
ctx.textAlign = 'center';
ctx.fillStyle = colorScheme.sourceTextColor;
const sourceLeftMargin = width / 2;
addQuoatInSourceLines(sourceLines).forEach((line, index) => {
ctx.fillText(line, sourceLeftMargin, sourceY + index * lineHeight);
});
// ctx.fillText(`《${source}》`, width / 2, sourceY);
// Draw author
ctx.font = `${authorFontSize}px ${currentFont}`;
ctx.fillStyle = colorScheme.authorTextColor;
ctx.fillText(author, width / 2, authorY);
return canvas;
} else {
// Use the original text rendering logic
// Calculate dimensions
const dimensions = calculateImageDimensions(ctx, text, source, dayFontSize, monthYearFontSize, weekdayFontSize, textFontSize, sourceFontSize, authorFontSize, maxWidth, sourceMaxWidth);
const {
headerHeight, dayY, monthYearY, weekdayY, separatorY,
textHeight, sourceHeight, footerHeight, totalHeight, lineHeight,
sourceLines
} = dimensions;
log('text dimensions', dimensions)
// Wrap text
ctx.font = `${textFontSize}px ${currentFont}`;
ctx.textAlign = 'left';
const { lines, textMaxWidth } = wrapText(ctx, text, maxWidth);
// Set canvas height
canvas.height = totalHeight * 2; // Scale by 2 for better quality
// Redraw with proper scaling
ctx.setTransform(2, 0, 0, 2, 0, 0); // Reset transform and reapply scale
// Fill background
ctx.fillStyle = colorScheme.background;
ctx.fillRect(0, 0, width, totalHeight);
// Draw header
ctx.fillStyle = colorScheme.primaryTextColor;
ctx.textAlign = 'center';
// Draw day (large)
ctx.font = `bold ${dayFontSize}px ${currentFont}`;
ctx.fillText(day, width / 2, dayY);
// Draw month and year
ctx.font = `${monthYearFontSize}px ${currentFont}`;
ctx.fillText(`${month} ${year}`, width / 2, monthYearY);
// Draw weekday
ctx.font = `${weekdayFontSize}px ${currentFont}`;
ctx.fillStyle = colorScheme.sourceTextColor;
ctx.fillText(weekday, width / 2, weekdayY);
// Draw separator line
ctx.beginPath();
ctx.moveTo(width / 2 - 40, separatorY);
ctx.lineTo(width / 2 + 40, separatorY);
ctx.strokeStyle = colorScheme.primaryTextColor;
ctx.globalAlpha = 0.3;
ctx.lineWidth = 1;
ctx.stroke();
ctx.globalAlpha = 1.0;
// Calculate text start Y position
const textStartY = separatorY + (totalHeight - headerHeight - footerHeight - textHeight) / 2 + lineHeight + 40;
// Draw text lines
ctx.font = `${textFontSize}px ${currentFont}`;
ctx.fillStyle = colorScheme.primaryTextColor;
ctx.textAlign = 'left';
// Calculate left margin to center the text block while keeping left alignment
const leftMargin = (width - textMaxWidth) / 2;
lines.forEach((line, index) => {
ctx.fillText(line, leftMargin, textStartY + index * lineHeight);
});
ctx.textAlign = 'center';
// Draw source and author at the bottom
const sourceY = totalHeight - 60 - 18 - 15 - sourceHeight;
const authorY = totalHeight - 60;
// Draw source
ctx.font = `${sourceFontSize}px ${currentFont}`;
ctx.textAlign = 'center';
ctx.fillStyle = colorScheme.sourceTextColor;
const sourceLeftMargin = width / 2;
addQuoatInSourceLines(sourceLines).forEach((line, index) => {
ctx.fillText(line, sourceLeftMargin, sourceY + index * lineHeight);
});
// ctx.fillText(`《${source}》`, width / 2, sourceY);
// Draw author
ctx.font = `${authorFontSize}px ${currentFont}`;
ctx.fillStyle = colorScheme.authorTextColor;
ctx.fillText(author, width / 2, authorY);
return canvas;
}
}
// Function to draw HTML content on canvas
function drawHtmlContent(ctx, htmlContent, x, y, maxWidth = 520, maxHeight, colorScheme, lineHeight = 30) {
// Create a temporary container to parse HTML
const tempContainer = document.createElement('div');
tempContainer.innerHTML = htmlContent;
tempContainer.style.position = 'absolute';
tempContainer.style.left = '-9999px';
tempContainer.style.top = '-9999px';
tempContainer.style.width = `${maxWidth}px`;
tempContainer.style.fontFamily = currentFont;
tempContainer.style.fontSize = ctx.font.replace(/^[0-9]*\.?[0-9]*px/, '').trim();
tempContainer.style.color = colorScheme.primaryTextColor;
tempContainer.style.backgroundColor = colorScheme.background;
document.body.appendChild(tempContainer);
// Recursive function to draw elements
function drawElement(element, currentX, currentY, isParentBlockElement = false, isPreviousNodeBlockElement = false) {
// Check if element is valid
if (!element) {
return { x: currentX, y: currentY };
}
// Get computed style safely
let computedStyle;
try {
computedStyle = element.nodeType === Node.ELEMENT_NODE ? window.getComputedStyle(element) : null;
} catch (e) {
// If we can't get computed style, use default values
log('we can\'t get computed style, use default values')
computedStyle = null;
}
const tagName = element.tagName ? element.tagName.toLowerCase() : '';
// Apply styles
ctx.save();
let font = ctx.font;
let fillStyle = colorScheme.primaryTextColor;
if (computedStyle) {
font = computedStyle.font;
fillStyle = computedStyle.color || colorScheme.primaryTextColor;
// Handle different elements
if (tagName === 'strong' || tagName === 'b') {
font = `bold ${computedStyle.fontSize} ${computedStyle.fontFamily}`;
lineHeight = parseInt(computedStyle.fontSize) * 1.5;
} else if (tagName === 'em' || tagName === 'i') {
font = `italic ${computedStyle.fontSize} ${computedStyle.fontFamily}`;
lineHeight = parseInt(computedStyle.fontSize) * 1.5;
} else if (tagName === 'h1') {
font = `bold ${parseInt(computedStyle.fontSize) * 1.5}px ${computedStyle.fontFamily}`;
lineHeight = parseInt(computedStyle.fontSize) * 1.5 * 1.2;
} else if (tagName === 'h2') {
font = `bold ${parseInt(computedStyle.fontSize) * 1.3}px ${computedStyle.fontFamily}`;
lineHeight = parseInt(computedStyle.fontSize) * 1.5 * 1.3;
} else if (tagName === 'h3') {
font = `bold ${parseInt(computedStyle.fontSize) * 1.1}px ${computedStyle.fontFamily}`;
lineHeight = parseInt(computedStyle.fontSize) * 1.5 * 1.4;
}
}
ctx.font = font;
ctx.fillStyle = fillStyle;
ctx.textAlign = 'left';
let isCurrentBlockElement = false;
log('element', element, 'isParentBlockElement', isParentBlockElement)
log('Final line height, font ', lineHeight, font)
// Draw text content
if (element.nodeType === Node.TEXT_NODE) {
const text = element.textContent || '';
log('element is text', text)
if (text.trim()) {
// Split text by newlines to preserve line breaks
const textLines = text.split('\n');
let lineCurrentY = currentY;
log("start currentY", currentY)
// const lineHeight = computedStyle ? parseInt(computedStyle.fontSize) * 1.2 : 20;
let tmpCurrentX = currentX;
log('textLines length', textLines)
textLines.forEach((textLine, index) => {
if (textLine.trim()) { // Draw even empty lines except possibly the first
const { lines, textMaxWidth } = x === currentX ? wrapText(ctx, textLine, maxWidth) : wrapTextWithXOffset(ctx, textLine, maxWidth, currentX, x);
//const { lines, textMaxWidth } = wrapText(ctx, textLine, maxWidth);
lines.forEach((line, iindex) => {
ctx.fillText(line, currentX, lineCurrentY);
if (iindex !== lines.length - 1) {
currentX = x; // 最初始位置
}
lineCurrentY += lineHeight; // Line height
log('iindex, lineCurrentY', iindex, lineCurrentY)
});
log('current textLine, isParentBlockElement, isPreviousNodeBlockElement' , textLine, isParentBlockElement, isPreviousNodeBlockElement)
// 记录最后一行的结束位置
const metrics = ctx.measureText(lines[lines.length - 1]);
tmpCurrentX = currentX + metrics.width
} else {
lineCurrentY += lineHeight; // Still add height for empty lines
}
});
log("lineCurrentY, lineHeight", lineCurrentY, lineHeight)
lineCurrentY -= lineHeight;
currentY = lineCurrentY;
log("end Y", currentY)
currentX = tmpCurrentX;
}
} else if (element.nodeType === Node.ELEMENT_NODE) {
// Handle child elements
let childCurrentX = currentX;
let childCurrentY = currentY;
// const lineHeight = computedStyle ? parseInt(computedStyle.fontSize) * 1.5 : 20;
// Define block-level elements that should have line breaks
const blockElements = ['div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'br', 'blockquote', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'pre', 'hr', 'table', 'form', 'fieldset', 'legend', 'details', 'summary'];
isCurrentBlockElement = blockElements.includes(tagName);
// Process children
let isPreviousNodeBlock = false;
for (let child of element.childNodes) {
const result = drawElement(child, childCurrentX, childCurrentY, isCurrentBlockElement, isPreviousNodeBlock);
childCurrentX = result.x;
childCurrentY = result.y;
isPreviousNodeBlock = result.isBlock;
}
log('element is node, is Block :',isCurrentBlockElement)
// For block elements, add a line break at the beginning
if (isCurrentBlockElement) {
childCurrentY += lineHeight; // Add line height before block element content
childCurrentX = x; // Reset X position for new line
}
currentY = Math.max(currentY, childCurrentY);
currentX = childCurrentX;
}
ctx.restore();
return { x: currentX, y: currentY, isBlock: isCurrentBlockElement };
}
/**
* 返回在给定宽度 maxWidth 之内,能够完整绘制的最长文本前缀。
* 兼容中英文、emoji、其他 Unicode 码点(不会把代理对截成半个)。
*
* @param {CanvasRenderingContext2D} ctx 已设置好 font、style 等属性的 canvas 上下文
* @param {string} text 待裁剪的完整文字(可能含中英文、emoji 等)
* @param {number} maxWidth 目标宽度(像素)
* @returns {string} 能够完整显示的最长前缀;若 width 小于首字符宽度则返回空串
*/
function fitTextPrefix(ctx, text, maxWidth) {
if (maxWidth <= 0) return '';
if (!text) return '';
// 把字符串拆成「Unicode 码点」数组,避免把 surrogate pair(如 emoji)截成半个
const chars = Array.from(text); // 如 ["a","中","😁", ...]
const totalLen = chars.length;
// 整体能放下就直接返回
if (ctx.measureText(text).width <= maxWidth) {
return text;
}
// 二分查找最长可放的字符数
let low = 0; // 已确认可以放下的字符数
let high = totalLen; // 上界(右闭区间)
while (low < high) {
// 取上中位数,防止 low 与 high 相差 1 时死循环
const mid = Math.floor((low + high + 1) / 2);
const candidate = chars.slice(0, mid).join('');
const w = ctx.measureText(candidate).width;
if (w <= maxWidth) {
low = mid; // 这段可以接受,继续往右
} else {
high = mid - 1; // 超出宽度,往左收敛
}
}
// low 为满足宽度限制的最大字符数
return chars.slice(0, low).join('');
}
// Simple text wrapping function
/**
*
* @param {CanvasRenderingContext2D} context 已设置好 font、style 等属性的 canvas 上下文
* @param {string} text 待裁剪的完整文字(可能含中英文、emoji 等)
* @param {number} maxWidth 最大宽度(像素)
* @param {number} x 光标位置(像素)
* @param {number} standardX 标准光标起始(像素)
*/
function wrapTextWithXOffset(context, text, maxWidth, x, standardX) {
if (x === standardX ) {
return wrapText(context, text, maxWidth);
} else {
const maxStr = fitTextPrefix(context, text, maxWidth - x);
log('x, standardX, maxStr', x, standardX, maxStr)
let { lines, textMaxWidth } = {};
if (maxStr.length === text.length) {
return wrapText(ctx, text, maxWidth)
} else {
let { lines, textMaxWidth } = wrapText(ctx, text.substring(maxStr.length), maxWidth);
lines.unshift(maxStr);
return { lines, textMaxWidth }
}
}
}
// Start drawing from the root element
drawElement(tempContainer, x, y, false, false);
// Clean up
document.body.removeChild(tempContainer);
}
// Calculate image dimensions
function calculateImageDimensions(ctx, text, source, dayFontSize, monthYearFontSize, weekdayFontSize, textFontSize, sourceFontSize, authorFontSize, textMaxWidth, sourceMaxWidth) {
// Header dimensions
const headerTopMargin = 60;
const separatorHeight = 20;
const headerSpacing = 20;
const textPadding = 40;
const headerHeight = headerTopMargin + dayFontSize + headerSpacing + monthYearFontSize +
headerSpacing + weekdayFontSize + separatorHeight + textPadding;
const dayY = headerTopMargin + dayFontSize;
const monthYearY = dayY + headerSpacing + monthYearFontSize;
const weekdayY = monthYearY + headerSpacing + weekdayFontSize;
const separatorY = weekdayY + separatorHeight;
// Text dimensions
ctx.font = `${textFontSize}px ${currentFont}`;
ctx.textAlign = 'left';
const { lines } = wrapText(ctx, text, textMaxWidth);
const lineHeight = 30; // Approximate line height
const textHeight = lines.length * lineHeight;
// Source dimensions
ctx.font = `${sourceFontSize}px ${currentFont}`;
ctx.textAlign = 'center';
const { lines: sourceLines } = wrapText(ctx, source, sourceMaxWidth);
const sourceHeight = sourceLines.length * lineHeight;
// Footer dimensions
const footerBottomMargin = 60;
const footerSpacing = 15;
const footerHeight = textPadding + 18 + footerSpacing + authorFontSize + footerBottomMargin + sourceHeight;
// Total height
const totalHeight = headerHeight + textHeight + footerHeight;
return {
headerHeight, dayY, monthYearY, weekdayY, separatorY,
textHeight, sourceHeight, footerHeight, totalHeight, lineHeight,
sourceLines
};
}
// Add 《》 at source lines
function addQuoatInSourceLines(lines) {
if (lines.length === 0) {
return lines;
}
const modifiedLines = [...lines];
modifiedLines[0] = `《${modifiedLines[0]}`;
modifiedLines[modifiedLines.length - 1] += '》';
return modifiedLines;
}
// Helper function to wrap text
function wrapText(context, text, maxWidth) {
// Set the font for text measurement
const lines = [];
const paragraphs = text.split('\n');
let textMaxWidth = 0;
paragraphs.forEach(paragraph => {
// Handle Chinese text differently from English text
if (/[\u4e00-\u9fa5]/.test(paragraph)) {
// For Chinese text, we need to break by characters
let line = '';
for (let i = 0; i < paragraph.length; i++) {
const char = paragraph[i];
const testLine = line + char;
const metrics = context.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && line !== '') {
// Measure the actual line width before pushing
const lineWidth = context.measureText(line).width;
lines.push(line);
textMaxWidth = Math.max(textMaxWidth, lineWidth);
line = char;
} else {
line = testLine;
}
}
if (line !== '') {
// Measure the last line width
const lineWidth = context.measureText(line).width;
lines.push(line);
textMaxWidth = Math.max(textMaxWidth, lineWidth);
}
} else {
// For English text, break by words
let line = '';
const words = paragraph.split(' ');
words.forEach(word => {
const testLine = line + word + ' ';
const metrics = context.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && line !== '') {
// Measure the actual line width before pushing
const lineWidth = context.measureText(line).width;
lines.push(line);
textMaxWidth = Math.max(textMaxWidth, lineWidth);
line = word + ' ';
} else {
line = testLine;
}
});
if (line !== '') {
// Measure the last line width and trim it
const trimmedLine = line.trim();
const lineWidth = context.measureText(trimmedLine).width;
lines.push(trimmedLine);
textMaxWidth = Math.max(textMaxWidth, lineWidth);
}
}
});
log('textMaxWidth : ', textMaxWidth)
// console.log(`Content : ${lines}`)
return { lines, textMaxWidth };
}
// Function to detect available fonts
function getAvailableFonts() {
// Common fonts to check
const commonFonts = [
'"Segoe UI", "Microsoft YaHei", sans-serif',
'PingFangSC, sans-serif',
'JB-Mono-ND-MiS, sans-serif',
'Arial, sans-serif',
'"Times New Roman", Times, serif',
'Georgia, serif',
'Verdana, sans-serif',
'"Courier New", Courier, monospace',
'Tahoma, sans-serif',
'"Trebuchet MS", sans-serif',
'"Arial Black", sans-serif',
'"Comic Sans MS", cursive, sans-serif',
'Impact, sans-serif',
'"Lucida Console", Monaco, monospace',
'"Lucida Sans Unicode", "Lucida Grande", sans-serif',
'Palatino Linotype, "Book Antiqua", Palatino, serif',
'Symbol',
'Tahoma, Geneva, sans-serif',
'"Helvetica Neue", Helvetica, Arial, sans-serif'
];
// Add system fonts if available
const systemFonts = [];
try {
// Try to get system fonts using CSS Font Loading API if available
if (document.fonts && typeof document.fonts.ready === 'object') {
// This is a simplified approach since we can't easily enumerate all system fonts
// in a userscript due to security restrictions
systemFonts.push(...commonFonts);
} else {
// Fallback to common fonts
systemFonts.push(...commonFonts);
}
} catch (e) {
// Fallback to common fonts
systemFonts.push(...commonFonts);
}
// Remove duplicates and return
return [...new Set(systemFonts)];
}
// 创建模态框
function createModal(initialImageUrl, initialSource, initialAuthor) {
if (currentModal) {
currentModal.remove();
}
isPreviewOpen = true; // Set flag when preview opens
const overlay = document.createElement('div');
overlay.id = 'modal-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 9998;
display: flex;
justify-content: center;
align-items: center;
overflow-y: auto;
padding: 20px;
`;
const modal = document.createElement('div');
modal.id = 'modal-container';
modal.style.cssText = `
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
padding: 20px;
max-width: 90vw;
min-width: 50vw;
max-height: 90vh;
overflow: auto;
position: relative;
`;
const closeButton = document.createElement('button');
closeButton.textContent = '×';
closeButton.style.cssText = `
position: fixed;
background-color: transparent;
top: 20px;
right: 20px;
background: #f00;
color: white;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
font-size: 20px;
cursor: pointer;
z-index: 10000;
`;
closeButton.addEventListener('click', closeModals);
const imgContainer = document.createElement('div');
imgContainer.style.cssText = `
text-align: center;
margin: 15px 0;
`;
const img = document.createElement('img');
img.src = initialImageUrl;
img.style.cssText = `
max-width: 100%;
max-height: 80vh;
object-fit: contain;
display: block;
margin: 0 auto;
`;
imgContainer.appendChild(img);
const inputContainer = document.createElement('div');
inputContainer.style.cssText = `
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 10px;
`;
const sourceLabel = document.createElement('label');
sourceLabel.textContent = '出处:';
sourceLabel.style.cssText = `
font-weight: bold;
color: #333;
`;
const sourceInput = document.createElement('input');
sourceInput.type = 'text';
sourceInput.value = initialSource;
sourceInput.placeholder = '请输入出处,例如:《人类简史》';
sourceInput.style.cssText = `
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
width: 100%;
`;
const authorLabel = document.createElement('label');
authorLabel.textContent = '作者:';
authorLabel.style.cssText = `
font-weight: bold;
color: #333;
`;
const authorInput = document.createElement('input');
authorInput.type = 'text';
authorInput.value = initialAuthor;
authorInput.placeholder = '请输入作者,例如:尤瓦尔·赫拉利';
authorInput.style.cssText = `
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
width: 100%;
`;
inputContainer.appendChild(sourceLabel);
inputContainer.appendChild(sourceInput);
inputContainer.appendChild(authorLabel);
inputContainer.appendChild(authorInput);
// Content editable checkbox
const editableContainer = document.createElement('div');
editableContainer.style.cssText = `
margin: 15px 0;
display: flex;
align-items: center;
gap: 8px;
`;
const editableCheckbox = document.createElement('input');
editableCheckbox.type = 'checkbox';
editableCheckbox.id = 'content-editable-checkbox';
editableCheckbox.checked = isContentEditable;
editableCheckbox.style.cssText = `
width: 16px;
height: 16px;
`;
const editableLabel = document.createElement('label');
editableLabel.textContent = '允许编辑文本内容';
editableLabel.setAttribute('for', 'content-editable-checkbox');
editableLabel.style.cssText = `
font-weight: bold;
color: #333;
cursor: pointer;
width: 80%;
`;
editableContainer.appendChild(editableCheckbox);
editableContainer.appendChild(editableLabel);
// Add event listener to toggle content editing
editableCheckbox.addEventListener('change', function () {
isContentEditable = this.checked;
const contentTextarea = document.querySelector('#content-textarea');
if (contentTextarea) {
contentTextarea.disabled = !this.checked;
contentTextarea.style.opacity = this.checked ? '1' : '0.6';
}
});
// Content text area
const contentLabel = document.createElement('label');
contentLabel.textContent = '文本内容:';
contentLabel.style.cssText = `
font-weight: bold;
color: #333;
`;
editableContainer.prepend(contentLabel);
const contentTextarea = document.createElement('textarea');
contentTextarea.id = 'content-textarea';
contentTextarea.value = selectionText;
contentTextarea.placeholder = '请输入要分享的文本内容';
contentTextarea.disabled = !isContentEditable;
contentTextarea.style.cssText = `
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
line-height: 1.5;
min-height: 120px;
resize: vertical;
background-color: #f9fafb;
color: #111827;
outline: none;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
transition: border-color 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
opacity: ${isContentEditable ? '1' : '0.6'};
`;
// Markdown switch
const markdownContainer = document.createElement('div');
markdownContainer.style.cssText = `
margin: 15px 0;
display: flex;
align-items: center;
gap: 8px;
`;
const markdownCheckbox = document.createElement('input');
markdownCheckbox.type = 'checkbox';
markdownCheckbox.id = 'markdown-checkbox';
markdownCheckbox.checked = isMarkdown;
markdownCheckbox.style.cssText = `
width: 16px;
height: 16px;
`;
const markdownLabel = document.createElement('label');
markdownLabel.textContent = 'Markdown格式';
markdownLabel.setAttribute('for', 'markdown-checkbox');
markdownLabel.style.cssText = `
font-weight: bold;
color: #333;
cursor: pointer;
`;
markdownContainer.appendChild(markdownCheckbox);
markdownContainer.appendChild(markdownLabel);
// Add event listener to toggle Markdown formatting
markdownCheckbox.addEventListener('change', function () {
isMarkdown = this.checked;
});
// Color scheme selection
const colorSchemeContainer = document.createElement('div');
colorSchemeContainer.style.cssText = `
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 10px;
`;
const colorSchemeLabel = document.createElement('label');
colorSchemeLabel.textContent = '配色方案:';
colorSchemeLabel.style.cssText = `
font-weight: bold;
color: #333;
`;
// Create circular color scheme selectors
const colorSchemeSelectors = document.createElement('div');
colorSchemeSelectors.style.cssText = `
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
`;
// Create circular preview for each color scheme
Object.keys(colorSchemes).forEach(key => {
const scheme = colorSchemes[key];
const schemeContainer = document.createElement('div');
schemeContainer.style.cssText = `
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
`;
const circle = document.createElement('div');
// 添加一个名为 'circle' 的类
circle.classList.add('circle');
circle.style.cssText = `
width: 50px;
height: 50px;
border-radius: 50%;
// border: 2px solid #ddd;
border: 2px solid rgb(100, 123, 255);
overflow: hidden;
position: relative;
`;
// Background half (top half)
const bgHalf = document.createElement('div');
bgHalf.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 50%;
background: ${scheme.background};
`;
// Text color half (bottom half)
const textHalf = document.createElement('div');
textHalf.style.cssText = `
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 50%;
background: ${scheme.primaryTextColor};
`;
circle.appendChild(bgHalf);
circle.appendChild(textHalf);
// Add selection indicator
if (key === currentColorScheme) {
circle.style.border = '2px solid #007bff';
circle.style.boxShadow = '0 0 0 2px #007bff';
}
// Add click event to select this scheme
circle.addEventListener('click', function () {
currentColorScheme = key;
// Update all circle borders
colorSchemeSelectors.querySelectorAll('.circle').forEach((circle, index) => {
const schemeKey = Object.keys(colorSchemes)[index];
if (schemeKey === currentColorScheme) {
log('Selected scheme:', schemeKey)
circle.style.border = '2px solid #007bff';
circle.style.boxShadow = '0 0 0 2px #007bff';
} else {
// circle.style.border = '2px solid #ddd';
// circle.style.boxShadow = 'none';
circle.style.border = '2px solid rgb(100, 123, 255);';
circle.style.boxShadow = '';
}
});
// Immediately regenerate the image with the new color scheme
const sourceInput = document.querySelector('#modal-container input[type="text"]:first-of-type');
const authorInput = document.querySelector('#modal-container input[type="text"]:nth-of-type(2)');
const contentTextarea = document.querySelector('#content-textarea');
const finalText = contentTextarea ? contentTextarea.value : selectionText;
if (sourceInput && authorInput) {
regenerateImage(img, sourceInput.value, authorInput.value, finalText);
}
});
const label = document.createElement('div');
label.textContent = scheme.name;
label.style.cssText = `
font-size: 12px;
margin-top: 5px;
color: #666;
`;
schemeContainer.appendChild(circle);
schemeContainer.appendChild(label);
colorSchemeSelectors.appendChild(schemeContainer);
});
colorSchemeContainer.appendChild(colorSchemeLabel);
colorSchemeContainer.appendChild(colorSchemeSelectors);
// Font selection
const fontContainer = document.createElement('div');
fontContainer.style.cssText = `
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 10px;
`;
const fontLabel = document.createElement('label');
fontLabel.textContent = '字体:';
fontLabel.style.cssText = `
font-weight: bold;
color: #333;
`;
const fontSelect = document.createElement('select');
fontSelect.id = 'font-selector';
fontSelect.style.cssText = `
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
`;
// Add common fonts to the selector
const commonFonts = getAvailableFonts();
commonFonts.forEach(font => {
const option = document.createElement('option');
option.value = font;
option.textContent = font.split(',')[0].replace(/"/g, '');
if (font === currentFont) {
option.selected = true;
}
fontSelect.appendChild(option);
});
fontSelect.addEventListener('change', function () {
currentFont = this.value;
// Immediately regenerate the image with the new font
const sourceInput = document.querySelector('#modal-container input[type="text"]:first-of-type');
const authorInput = document.querySelector('#modal-container input[type="text"]:nth-of-type(2)');
const contentTextarea = document.querySelector('#content-textarea');
const finalText = contentTextarea ? contentTextarea.value : selectionText;
if (sourceInput && authorInput) {
regenerateImage(img, sourceInput.value, authorInput.value, finalText);
}
});
fontContainer.appendChild(fontLabel);
fontContainer.appendChild(fontSelect);
colorSchemeContainer.appendChild(fontContainer);
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
`;
const regenerateButton = document.createElement('button');
regenerateButton.textContent = '重新生成';
regenerateButton.style.cssText = `
padding: 8px 16px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
regenerateButton.addEventListener('click', function () {
// 获取可能修改后的文本内容
const contentTextarea = document.querySelector('#content-textarea');
const finalText = contentTextarea ? contentTextarea.value : selectionText;
// 重新生成图片时使用可能修改后的文本内容
regenerateImage(img, sourceInput.value, authorInput.value, finalText);
});
const downloadButton = document.createElement('button');
downloadButton.textContent = '下载图片';
downloadButton.style.cssText = `
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
downloadButton.addEventListener('click', function () {
const link = document.createElement('a');
link.href = img.src;
link.download = `quote_${new Date().getDate()}_${new Date().toLocaleString('en-US', { month: 'long' })}_${new Date().getFullYear()}.png`;
link.target = '_blank'; // Open in a new tab/window to trigger download
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showToast('图片已开始下载!');
});
buttonContainer.appendChild(regenerateButton);
// buttonContainer.appendChild(downloadButton);
modal.appendChild(closeButton);
modal.appendChild(imgContainer);
modal.appendChild(inputContainer);
modal.appendChild(editableContainer);
//modal.appendChild(contentLabel);
modal.appendChild(contentTextarea);
modal.appendChild(markdownContainer);
modal.appendChild(colorSchemeContainer);
modal.appendChild(buttonContainer);
overlay.appendChild(modal);
currentModal = overlay;
document.body.appendChild(overlay);
overlay.addEventListener('click', function (e) {
if (e.target === overlay) {
closeModals();
}
});
}
// ✅ 修复版:重新生成图片函数
async function regenerateImage(imgElement, newSource, newAuthor, newText = null) {
log('[TextShare] regenerateImage started at', performance.now());
// Show loading overlay
showLoading();
// 如果没有提供新的文本内容,则使用原始选择的文本
const finalText = newText !== null ? newText : selectionText;
try {
log('About to call createQuoteImage in regenerateImage at', performance.now());
const canvasStart = performance.now();
// Create canvas directly for better performance
const canvas = await createQuoteImage(finalText, newSource, newAuthor, colorSchemes[currentColorScheme]);
const canvasEnd = performance.now();
log('createQuoteImage in regenerateImage took', canvasEnd - canvasStart, 'ms');
const newImageUrl = canvas.toDataURL('image/png');
imgElement.src = newImageUrl;
showToast('图片已更新!');
} catch (error) {
logError('重新生成图片失败:', error);
showToast('重新生成图片失败,请重试。');
} finally {
// Hide loading overlay
hideLoading();
log('[TextShare] regenerateImage finished at', performance.now());
}
}
function closeModals() {
if (currentModal) {
currentModal.remove();
currentModal = null;
}
if (shareButton) {
shareButton.remove();
shareButton = null;
}
isPreviewOpen = false; // Reset flag when modal closes
}
unsafeWindow.document.addEventListener('mouseup', function (e) {
const composedPath = e.composedPath();
setTimeout(() => { createShareButton(composedPath) }, 500);
});
document.addEventListener('mousedown', function (e) {
if (shareButton && !shareButton.contains(e.target)) {
shareButton.remove();
shareButton = null;
}
});
})();