iqijun_obsidian-image-share/main.ts
2025-04-13 23:23:09 +08:00

908 lines
No EOL
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { App, Editor, Modal, Plugin, Menu, MarkdownRenderer, Component } from 'obsidian';
import './styles.css';
import html2canvas from 'html2canvas';
// 图片生成器类
class ImageGenerator {
private canvas: HTMLCanvasElement;
private text: string;
private currentTemplate: ShareTemplate;
private currentStyle = 'default'; // Removed type annotation
private app: App;
constructor(app: App, text: string) {
this.app = app;
this.text = text;
this.currentTemplate = SHARE_TEMPLATES[0];
this.canvas = document.createElement('canvas');
}
public async updateCanvas() {
try {
const tempDiv = document.createElement('div');
tempDiv.className = 'markdown-preview-view markdown-rendered temp-markdown-container temp-container';
tempDiv.classList.add(this.currentTemplate.id === 'dark' ? 'dark-theme' : 'light-theme');
// Add the markdown style classes
tempDiv.classList.add('markdown-style-base');
tempDiv.classList.add(`markdown-style-${this.currentStyle}`);
// Use fixed-width classes where possible
tempDiv.classList.add(`template-width-${this.currentTemplate.width}`);
// For non-standard widths, use a dynamic style element
if (!document.querySelector(`.template-width-${this.currentTemplate.width}`)) {
let styleElement = document.getElementById('template-dynamic-styles');
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'template-dynamic-styles';
document.head.appendChild(styleElement);
}
// Add a style rule for this specific width
styleElement.textContent += `
.template-width-${this.currentTemplate.width} {
width: ${this.currentTemplate.width}px;
}
`;
}
tempDiv.classList.add('temp-div');
// 添加日期
const dateDiv = tempDiv.createDiv({ cls: 'metadata-container temp-date-metadata' });
dateDiv.classList.add(this.currentTemplate.id === 'dark' ? 'dark-theme' : 'light-theme');
dateDiv.textContent = new Date().toLocaleDateString();
// 添加内容容器
const contentDiv = tempDiv.createDiv({ cls: 'markdown-preview-sizer' });
// Create a component that we can unload later to prevent memory leaks
const component = new Component();
// 渲染 Markdown
await MarkdownRenderer.render(
this.app,
this.text,
contentDiv,
'',
component
);
document.body.appendChild(tempDiv);
// 获取实际内容高度,包括内边距
const actualHeight = tempDiv.scrollHeight;
// 确保最小高度
const minHeight = 200;
const finalHeight = Math.max(actualHeight, minHeight);
// 增加像素密度和图像质量
const renderedCanvas = await html2canvas(tempDiv, {
width: this.currentTemplate.width,
height: finalHeight,
backgroundColor: this.currentTemplate.id === 'dark' ? '#2d3436' : '#e8ecf9',
scale: window.devicePixelRatio * 2,
windowWidth: this.currentTemplate.width,
windowHeight: finalHeight,
logging: false,
useCORS: true,
imageTimeout: 0,
allowTaint: true,
onclone: (clonedDoc) => {
const clonedDiv = clonedDoc.querySelector('.markdown-preview-view') as HTMLElement;
if (clonedDiv) {
// Set width and height via CSS classes
clonedDiv.classList.add('cloned-preview-container');
clonedDiv.classList.add(`template-width-${this.currentTemplate.width - 40}`);
// Create a custom height class if needed
if (!clonedDoc.querySelector(`.content-height-${finalHeight}`)) {
const styleElement = clonedDoc.createElement('style');
styleElement.textContent = `
.content-height-${finalHeight} {
height: ${finalHeight}px;
}
`;
clonedDoc.head.appendChild(styleElement);
}
clonedDiv.classList.add(`content-height-${finalHeight}`);
clonedDiv.classList.add('dynamic-height');
clonedDiv.classList.add('enhanced-text-rendering');
// Make sure the style classes are properly applied
if (!clonedDiv.classList.contains('markdown-style-base')) {
clonedDiv.classList.add('markdown-style-base');
}
const styleClass = `markdown-style-${this.currentStyle}`;
if (!clonedDiv.classList.contains(styleClass)) {
clonedDiv.classList.add(styleClass);
}
}
}
});
// 更新画布
this.canvas = renderedCanvas;
// 清理临时元素
document.body.removeChild(tempDiv);
// Unload the component to prevent memory leaks
component.unload();
} catch (error) {
console.error('Error rendering markdown:', error);
}
}
// Add a method to set the style
public async setStyle(style: string) {
this.currentStyle = style;
await this.updateCanvas();
}
// Add a method to get the current style
public getCurrentStyle(): string {
return this.currentStyle;
}
public getCanvas(): HTMLCanvasElement {
return this.canvas;
}
public getDataURL(): string {
// 增加输出质量使用PNG格式以保持最佳质量
return this.canvas.toDataURL('image/png', 1.0);
}
public async downloadAsImage() {
// 获取当前活动文件的名称
const activeFile = this.app.workspace.getActiveFile();
const fileName = activeFile ? activeFile.basename : 'share-image';
const dataURL = this.getDataURL();
const a = document.createElement('a');
a.href = dataURL;
a.download = `${fileName}.png`;
a.click();
}
public async copyToClipboard(): Promise<boolean> {
try {
// 创建一个临时的canvas元素以获取Blob对象
const canvas = this.canvas;
// 将canvas转换为blob对象
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob((b) => {
if (b) {
resolve(b);
} else {
reject(new Error('Failed to create blob from canvas'));
}
}, 'image/png', 1.0);
});
// 创建ClipboardItem对象
const data = [new ClipboardItem({ 'image/png': blob })];
// 写入剪贴板
await navigator.clipboard.write(data);
return true;
} catch (error) {
console.error('复制到剪贴板失败:', error);
return false;
}
}
public async setTemplate(templateId: string) {
const template = SHARE_TEMPLATES.find(t => t.id === templateId);
if (template) {
this.currentTemplate = template;
await this.updateCanvas();
}
}
// 添加获取当前模板的方法
public getCurrentTemplate(): ShareTemplate {
return this.currentTemplate;
}
}
class TextPreviewModal extends Modal {
private text: string;
private imageGenerator: ImageGenerator;
private buttonCleanups: (() => void)[] = [];
constructor(app: App, text: string) {
super(app);
this.text = text;
this.imageGenerator = new ImageGenerator(app, text);
}
// Helper method to add event listeners that will be cleaned up on close
private addCleanableListener(element: HTMLElement, event: string, handler: (e: Event) => void): void {
element.addEventListener(event, handler);
this.buttonCleanups.push(() => element.removeEventListener(event, handler));
}
async onOpen() {
const {contentEl} = this;
contentEl.empty();
contentEl.addClass('image-share-modal');
// 获取屏幕大小
// We no longer need these variables
// const windowWidth = window.innerWidth;
// const windowHeight = window.innerHeight;
// 创建一个调整模态框大小的函数
function adjustModalSize(width: number, height: number) {
const modalElement = contentEl.closest('.modal') as HTMLElement;
if (!modalElement) return;
// 计算最终宽高,考虑屏幕尺寸限制
const maxWidth = window.innerWidth * 0.9;
const maxHeight = window.innerHeight * 0.9;
const finalWidth = Math.min(width + 300, maxWidth); // 300px 是左侧面板宽度
const finalHeight = Math.min(height + 80, maxHeight); // 添加额外的高度用于顶部和底部
// Add classes for styling
modalElement.classList.add('modal-size');
modalElement.classList.add('modal-layout');
// Set CSS variables through a dynamically created style element
let styleElement = document.getElementById('modal-dynamic-styles');
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'modal-dynamic-styles';
document.head.appendChild(styleElement);
}
// Update style rules for this specific modal
const modalId = `modal-${Date.now()}`;
modalElement.id = modalId;
styleElement.textContent = `
#${modalId}.modal-size {
width: ${finalWidth}px;
height: ${finalHeight}px;
}
`;
// 调整内容区域
contentEl.classList.add('modal-content-layout');
}
// 计算合适的初始模态框大小
const currentTemplate = this.imageGenerator.getCurrentTemplate();
// 初始设置 - 将在渲染完成后更新
adjustModalSize(currentTemplate.width, 600);
const resizableContainer = contentEl.createDiv({ cls: 'resizable-container' });
// 创建左侧控制面板
const controlsPanel = resizableContainer.createDiv({ cls: 'controls-panel' });
// 创建控制面板标题
controlsPanel.createEl('h2', { text: '图片设置' });
// 1. 创建主题选择区域
const themeSection = controlsPanel.createDiv({ cls: 'control-section' });
themeSection.createEl('div', { text: '选择主题', cls: 'section-title' });
// 创建模板选择器
const templateSelector = themeSection.createDiv({ cls: 'template-selector' });
// 创建空间填充区域
controlsPanel.createDiv({ cls: 'flex-spacer' });
// 3. 创建下载按钮区域 - 总是在底部
const downloadSection = controlsPanel.createDiv({ cls: 'control-section download-section' });
downloadSection.createEl('div', { text: '导出图片', cls: 'section-title' });
// 修改容器类名为垂直布局
const buttonsContainer = downloadSection.createDiv({ cls: 'buttons-container vertical' });
// 创建下载按钮 - 放在前面
const downloadButton = buttonsContainer.createEl('button', {
cls: 'elegant-button download-button',
attr: { 'aria-label': '下载分享图片' }
});
const downloadIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
downloadIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
downloadIcon.setAttribute('width', '18');
downloadIcon.setAttribute('height', '18');
downloadIcon.setAttribute('viewBox', '0 0 24 24');
downloadIcon.setAttribute('fill', 'none');
downloadIcon.setAttribute('stroke', 'currentColor');
downloadIcon.setAttribute('stroke-width', '2');
downloadIcon.setAttribute('stroke-linecap', 'round');
downloadIcon.setAttribute('stroke-linejoin', 'round');
downloadIcon.classList.add('download-icon');
const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path1.setAttribute('d', 'M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4');
const line6 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line6.setAttribute('x1', '7');
line6.setAttribute('y1', '10');
line6.setAttribute('x2', '12');
line6.setAttribute('y2', '15');
const line7 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line7.setAttribute('x1', '17');
line7.setAttribute('y1', '10');
line7.setAttribute('x2', '12');
line7.setAttribute('y2', '15');
const line8 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line8.setAttribute('x1', '12');
line8.setAttribute('y1', '15');
line8.setAttribute('x2', '12');
line8.setAttribute('y2', '3');
downloadIcon.appendChild(path1);
downloadIcon.appendChild(line6);
downloadIcon.appendChild(line7);
downloadIcon.appendChild(line8);
downloadButton.appendChild(downloadIcon);
const downloadText = document.createElement('span');
downloadText.textContent = '下载图片';
downloadButton.appendChild(downloadText);
// 创建复制按钮 - 放在下载按钮后面
const copyButton = buttonsContainer.createEl('button', {
cls: 'elegant-button copy-button',
attr: { 'aria-label': '复制图片到剪贴板' }
});
const copyIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
copyIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
copyIcon.setAttribute('width', '18');
copyIcon.setAttribute('height', '18');
copyIcon.setAttribute('viewBox', '0 0 24 24');
copyIcon.setAttribute('fill', 'none');
copyIcon.setAttribute('stroke', 'currentColor');
copyIcon.setAttribute('stroke-width', '2');
copyIcon.setAttribute('stroke-linecap', 'round');
copyIcon.setAttribute('stroke-linejoin', 'round');
copyIcon.classList.add('copy-icon');
const rect1 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect1.setAttribute('x', '9');
rect1.setAttribute('y', '9');
rect1.setAttribute('width', '13');
rect1.setAttribute('height', '13');
rect1.setAttribute('rx', '2');
rect1.setAttribute('ry', '2');
const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path2.setAttribute('d', 'M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1');
copyIcon.appendChild(rect1);
copyIcon.appendChild(path2);
copyButton.appendChild(copyIcon);
const copyText = document.createElement('span');
copyText.textContent = '复制到剪贴板';
copyButton.appendChild(copyText);
// 创建右侧预览面板
const previewPanel = resizableContainer.createDiv({ cls: 'preview-panel' });
// 创建预览面板标题
previewPanel.createEl('h2', { text: '图片预览' });
// 创建画布容器
const canvasContainer = previewPanel.createDiv({ cls: 'canvas-container' });
// 创建模板选择器
SHARE_TEMPLATES.forEach(template => {
const templateButton = templateSelector.createEl('button', {
cls: 'template-button',
text: template.name
});
// 添加图标到模板按钮
if (template.id === 'light') {
const lightIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
lightIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
lightIcon.setAttribute('width', '16');
lightIcon.setAttribute('height', '16');
lightIcon.setAttribute('viewBox', '0 0 24 24');
lightIcon.setAttribute('fill', 'none');
lightIcon.setAttribute('stroke', 'currentColor');
lightIcon.setAttribute('stroke-width', '2');
lightIcon.setAttribute('stroke-linecap', 'round');
lightIcon.setAttribute('stroke-linejoin', 'round');
lightIcon.classList.add('light-template-icon');
const circle2 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle2.setAttribute('cx', '12');
circle2.setAttribute('cy', '12');
circle2.setAttribute('r', '5');
const line9 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line9.setAttribute('x1', '12');
line9.setAttribute('y1', '1');
line9.setAttribute('x2', '12');
line9.setAttribute('y2', '3');
const line10 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line10.setAttribute('x1', '12');
line10.setAttribute('y1', '21');
line10.setAttribute('x2', '12');
line10.setAttribute('y2', '23');
const line11 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line11.setAttribute('x1', '4.22');
line11.setAttribute('y1', '4.22');
line11.setAttribute('x2', '5.64');
line11.setAttribute('y2', '5.64');
const line12 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line12.setAttribute('x1', '18.36');
line12.setAttribute('y1', '18.36');
line12.setAttribute('x2', '19.78');
line12.setAttribute('y2', '19.78');
const line13 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line13.setAttribute('x1', '1');
line13.setAttribute('y1', '12');
line13.setAttribute('x2', '3');
line13.setAttribute('y2', '12');
const line14 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line14.setAttribute('x1', '21');
line14.setAttribute('y1', '12');
line14.setAttribute('x2', '23');
line14.setAttribute('y2', '12');
const line15 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line15.setAttribute('x1', '4.22');
line15.setAttribute('y1', '19.78');
line15.setAttribute('x2', '5.64');
line15.setAttribute('y2', '18.36');
const line16 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line16.setAttribute('x1', '18.36');
line16.setAttribute('y1', '5.64');
line16.setAttribute('x2', '19.78');
line16.setAttribute('y2', '4.22');
lightIcon.appendChild(circle2);
lightIcon.appendChild(line9);
lightIcon.appendChild(line10);
lightIcon.appendChild(line11);
lightIcon.appendChild(line12);
lightIcon.appendChild(line13);
lightIcon.appendChild(line14);
lightIcon.appendChild(line15);
lightIcon.appendChild(line16);
templateButton.appendChild(lightIcon);
} else {
const darkIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
darkIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
darkIcon.setAttribute('width', '16');
darkIcon.setAttribute('height', '16');
darkIcon.setAttribute('viewBox', '0 0 24 24');
darkIcon.setAttribute('fill', 'none');
darkIcon.setAttribute('stroke', 'currentColor');
darkIcon.setAttribute('stroke-width', '2');
darkIcon.setAttribute('stroke-linecap', 'round');
darkIcon.setAttribute('stroke-linejoin', 'round');
darkIcon.classList.add('dark-template-icon');
const path3 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path3.setAttribute('d', 'M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z');
darkIcon.appendChild(path3);
templateButton.appendChild(darkIcon);
}
const templateText = document.createElement('span');
templateText.textContent = template.name;
templateButton.appendChild(templateText);
// Use our helper method to add a cleanable event listener
const templateClickHandler = async () => {
templateSelector.findAll('.template-button').forEach(btn =>
btn.removeClass('active')
);
templateButton.addClass('active');
// 添加点击反馈动画
templateButton.animate([
{ transform: 'scale(0.95)' },
{ transform: 'scale(1)' }
], {
duration: 150,
easing: 'ease-out'
});
await this.imageGenerator.setTemplate(template.id);
canvasContainer.empty();
const newCanvas = this.imageGenerator.getCanvas();
canvasContainer.appendChild(newCanvas);
// 当模板变更时更新弹出框尺寸
adjustModalSize(newCanvas.width, newCanvas.height);
};
this.addCleanableListener(templateButton, 'click', templateClickHandler);
if (template.id === 'default') {
templateButton.addClass('active');
}
});
// 创建样式选择区域
const styleSection = controlsPanel.createDiv({ cls: 'control-section' });
styleSection.createEl('div', { text: '选择样式', cls: 'section-title' });
// 创建样式选择器
const styleSelector = styleSection.createDiv({ cls: 'style-selector' });
// 添加样式选择按钮
MARKDOWN_STYLES.forEach(style => {
const styleButton = styleSelector.createEl('button', {
cls: 'style-button',
attr: { 'data-style': style.id }
});
// 创建样式标题
const styleTitle = document.createElement('span');
styleTitle.textContent = style.name;
styleButton.appendChild(styleTitle);
// 添加描述
const styleDesc = document.createElement('small');
styleDesc.textContent = style.description;
styleDesc.classList.add('style-description');
styleButton.appendChild(styleDesc);
// 设置当前活动样式
if (style.id === this.imageGenerator.getCurrentStyle()) {
styleButton.classList.add('active');
}
// 添加点击事件,使用我们的辅助方法添加可清理的事件监听器
const styleClickHandler = async () => {
// 移除所有活动状态
styleSelector.findAll('.style-button').forEach(btn =>
btn.removeClass('active')
);
// 设置当前按钮为活动状态
styleButton.addClass('active');
// 添加动画效果
styleButton.animate([
{ transform: 'scale(0.95)' },
{ transform: 'scale(1)' }
], {
duration: 150,
easing: 'ease-out'
});
// 设置新样式并重新渲染
await this.imageGenerator.setStyle(style.id);
// 更新画布
canvasContainer.empty();
const newCanvas = this.imageGenerator.getCanvas();
canvasContainer.appendChild(newCanvas);
// 更新模态框大小
adjustModalSize(newCanvas.width, newCanvas.height);
};
this.addCleanableListener(styleButton, 'click', styleClickHandler);
});
// 等待初始画布渲染完成
await this.imageGenerator.updateCanvas();
const canvas = this.imageGenerator.getCanvas();
canvasContainer.appendChild(canvas);
// 更新模态框大小以适应画布
adjustModalSize(canvas.width, canvas.height);
// 添加复制按钮点击事件
const copyClickHandler = async () => {
copyButton.classList.add('clicked');
const success = await this.imageGenerator.copyToClipboard();
const createToast = (success: boolean, message: string) => {
const toast = document.createElement('div');
toast.className = 'download-toast';
// Create icon
if (success) {
const successIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
successIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
successIcon.setAttribute('width', '24');
successIcon.setAttribute('height', '24');
successIcon.setAttribute('viewBox', '0 0 24 24');
successIcon.setAttribute('fill', 'none');
successIcon.setAttribute('stroke', 'currentColor');
successIcon.setAttribute('stroke-width', '2');
successIcon.setAttribute('stroke-linecap', 'round');
successIcon.setAttribute('stroke-linejoin', 'round');
const circle3 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle3.setAttribute('cx', '12');
circle3.setAttribute('cy', '12');
circle3.setAttribute('r', '10');
const path4 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path4.setAttribute('d', 'M8 12l2 2 4-4');
successIcon.appendChild(circle3);
successIcon.appendChild(path4);
toast.appendChild(successIcon);
} else {
const errorIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
errorIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
errorIcon.setAttribute('width', '24');
errorIcon.setAttribute('height', '24');
errorIcon.setAttribute('viewBox', '0 0 24 24');
errorIcon.setAttribute('fill', 'none');
errorIcon.setAttribute('stroke', 'currentColor');
errorIcon.setAttribute('stroke-width', '2');
errorIcon.setAttribute('stroke-linecap', 'round');
errorIcon.setAttribute('stroke-linejoin', 'round');
const circle4 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle4.setAttribute('cx', '12');
circle4.setAttribute('cy', '12');
circle4.setAttribute('r', '10');
const line17 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line17.setAttribute('x1', '15');
line17.setAttribute('y1', '9');
line17.setAttribute('x2', '9');
line17.setAttribute('y2', '15');
const line18 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line18.setAttribute('x1', '9');
line18.setAttribute('y1', '9');
line18.setAttribute('x2', '15');
line18.setAttribute('y2', '15');
errorIcon.appendChild(circle4);
errorIcon.appendChild(line17);
errorIcon.appendChild(line18);
toast.appendChild(errorIcon);
}
const messageSpan = document.createElement('span');
messageSpan.textContent = message;
toast.appendChild(messageSpan);
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('hide');
setTimeout(() => {
document.body.removeChild(toast);
}, 300);
}, 3000);
};
createToast(success, success ? '已复制到剪贴板' : '复制失败');
};
this.addCleanableListener(copyButton, 'click', copyClickHandler);
// 添加下载按钮点击事件
const downloadClickHandler = () => {
downloadButton.classList.add('clicked');
// 生成下载
const link = document.createElement('a');
link.download = `Obsidian-${new Date().toISOString().replace(/:/g, '-')}.png`;
link.href = this.imageGenerator.getDataURL();
link.click();
const createToast = (success: boolean, message: string) => {
const toast = document.createElement('div');
toast.className = 'download-toast';
// Create icon
if (success) {
const successIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
successIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
successIcon.setAttribute('width', '24');
successIcon.setAttribute('height', '24');
successIcon.setAttribute('viewBox', '0 0 24 24');
successIcon.setAttribute('fill', 'none');
successIcon.setAttribute('stroke', 'currentColor');
successIcon.setAttribute('stroke-width', '2');
successIcon.setAttribute('stroke-linecap', 'round');
successIcon.setAttribute('stroke-linejoin', 'round');
const circle5 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle5.setAttribute('cx', '12');
circle5.setAttribute('cy', '12');
circle5.setAttribute('r', '10');
const path5 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path5.setAttribute('d', 'M8 12l2 2 4-4');
successIcon.appendChild(circle5);
successIcon.appendChild(path5);
toast.appendChild(successIcon);
} else {
const errorIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
errorIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
errorIcon.setAttribute('width', '24');
errorIcon.setAttribute('height', '24');
errorIcon.setAttribute('viewBox', '0 0 24 24');
errorIcon.setAttribute('fill', 'none');
errorIcon.setAttribute('stroke', 'currentColor');
errorIcon.setAttribute('stroke-width', '2');
errorIcon.setAttribute('stroke-linecap', 'round');
errorIcon.setAttribute('stroke-linejoin', 'round');
const circle6 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle6.setAttribute('cx', '12');
circle6.setAttribute('cy', '12');
circle6.setAttribute('r', '10');
const line19 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line19.setAttribute('x1', '15');
line19.setAttribute('y1', '9');
line19.setAttribute('x2', '9');
line19.setAttribute('y2', '15');
const line20 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line20.setAttribute('x1', '9');
line20.setAttribute('y1', '9');
line20.setAttribute('x2', '15');
line20.setAttribute('y2', '15');
errorIcon.appendChild(circle6);
errorIcon.appendChild(line19);
errorIcon.appendChild(line20);
toast.appendChild(errorIcon);
}
const messageSpan = document.createElement('span');
messageSpan.textContent = message;
toast.appendChild(messageSpan);
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('hide');
setTimeout(() => {
document.body.removeChild(toast);
}, 300);
}, 3000);
};
createToast(true, '已开始下载');
};
this.addCleanableListener(downloadButton, 'click', downloadClickHandler);
this.contentEl.addClass('text-preview-modal');
}
onClose() {
const {contentEl} = this;
// Clean up all event listeners
this.buttonCleanups.forEach(cleanup => cleanup());
this.buttonCleanups = [];
// Empty the content element
contentEl.empty();
// Create a dummy canvas to replace any references to the current canvas
// This helps with garbage collection
const dummyCanvas = document.createElement('canvas');
// Replace the canvas in the imageGenerator to help garbage collection
if (this.imageGenerator) {
// @ts-ignore - We're directly accessing the private canvas property for cleanup
this.imageGenerator.canvas = dummyCanvas;
}
// Allow this instance to be garbage collected
// @ts-ignore - We know this is not strictly correct, but it's for cleanup purposes
this.imageGenerator = undefined;
}
}
// 模板接口定义
interface ShareTemplate {
id: string;
name: string;
width: number;
render: (ctx: CanvasRenderingContext2D, text: string) => Promise<void>;
}
// 预定义模板
const SHARE_TEMPLATES: ShareTemplate[] = [
{
id: 'default',
name: '默认模板',
width: 800,
render: async () => {}
},
{
id: 'dark',
name: '深色模板',
width: 800,
render: async () => {}
}
];
// Define the available markdown styles
const MARKDOWN_STYLES = [
{
id: 'default',
name: '默认样式',
description: '默认的高对比度样式,有彩色强调'
},
{
id: 'modern',
name: '现代样式',
description: '现代设计风格,蓝色和粉色强调'
},
{
id: 'minimal',
name: '极简样式',
description: '简约而优雅的黑白设计'
}
];
export default class ImageSharePlugin extends Plugin {
async onload() {
// 注册编辑器右键菜单
this.registerEvent(
this.app.workspace.on('editor-menu', (menu: Menu, editor: Editor) => {
const selectedText = editor.getSelection();
if (selectedText) {
menu.addItem((item) => {
item
.setTitle('Share as Image')
.setIcon('image')
.onClick(async () => {
new TextPreviewModal(this.app, selectedText).open();
});
});
}
})
);
// 添加命令到命令面板
this.addCommand({
id: 'share-as-image',
name: 'Share selected text as image',
editorCallback: (editor: Editor) => {
const selectedText = editor.getSelection();
if (selectedText) {
new TextPreviewModal(this.app, selectedText).open();
}
}
});
}
onunload() {
// 插件卸载时的清理代码
}
}