/* eslint-disable */ const { Plugin, Notice, TFile, Modal, Setting, MarkdownView, MarkdownRenderer, PluginSettingTab } = require('obsidian'); let electronClipboard = null; try { electronClipboard = require('electron').clipboard; } catch (e) { electronClipboard = null; } /* ========== 用户配置 ========== */ const DEFAULT_USER_CONFIG = { currentPlatform: 'feishu', imageMaxWidth: 1000, imageQuality: 0.92, displayMaxWidth: 800, imageFormat: 'auto', platformImageStrategies: { wechat: 'balanced', feishu: 'quality', zhihu: 'balanced', yuque: 'quality' }, platformThemes: { wechat: 'official', feishu: 'minimal', zhihu: 'knowledge', yuque: 'soft' }, customThemes: { custom: { name: '我的主题', fontFamily: "'PingFang SC', 'Microsoft YaHei', sans-serif", textColor: '#24303f', mutedColor: '#64748b', backgroundColor: '#ffffff', accentColor: '#2563eb', linkColor: '#2563eb', blockquoteBorder: '#60a5fa', blockquoteBackground: '#eff6ff', codeBackground: '#f8fafc', codeColor: '#1e293b', headingColor: '#0f172a', borderColor: '#dbe2ea', borderRadius: '8px', paragraphSpacing: '16px', lineHeight: '1.75', headingDivider: true, tableHeaderFill: true, imageRounded: true, codeBlockBorder: true } }, customThemeEditorKey: 'custom', customThemeSearchQuery: '', customThemeOrder: ['custom'], themePreviewPlatform: 'wechat', themeListShowUsedOnly: false, themeListUsedFirst: true, themeListShowRecommendedOnly: false, themeManagerListCollapsed: false, lastAppliedRecommendedTheme: null, themeManagerSidebarWidth: 280, platformThemeOverrides: { wechat: {}, feishu: {}, zhihu: {}, yuque: {} }, compressGif: false, enableCodeHighlight: true, enableThemeStyles: true, enableFrontmatter: false, enableProgressNotice: true, enablePreview: true, parallelImageProcessing: true, maxParallelImages: 5, themeStylesCache: true, includeFrontmatter: false, clipboardFormats: ['text/html', 'text/plain'], }; let USER_CONFIG = { ...DEFAULT_USER_CONFIG }; /* ========== 图片优化配置 ========== */ const IMAGE_CONFIG = { MAX_WIDTH: 1000, QUALITY: 0.85, DISPLAY_MAX_WIDTH: 800, FORMAT: 'jpeg', COMPRESS_GIF: false }; /* ========== 缓存管理 ========== */ const Cache = { themeStyles: null, themeType: null, clear() { this.themeStyles = null; this.themeType = null; } }; /* ========== 平台预设配置 ========== */ const PLATFORM_PRESETS = { wechat: { name: '微信公众号', imageMaxWidth: 900, imageQuality: 0.9, displayMaxWidth: '100%', imageFormat: 'auto', compressGif: false, enableCodeHighlight: true, enableThemeStyles: false, enableProgressNotice: true, description: '适合微信公众号,图片宽度900px,质量0.85', compatibilityNotes: [ '代码块使用截图效果最佳', '表格可能显示异常', '样式已优化为内联样式' ], styleStrategy: 'inline', styleMode: 'inline-only', preserveThemeLevel: 'medium', supports: { styleTag: false, className: false, table: 'partial', codeBlock: 'partial', math: 'weak' }, transforms: { flattenCallout: true, simplifyTable: true, simplifyNestedBlocks: true }, previewFrame: { background: '#f5f5f5', padding: '20px 14px', contentBackground: '#ffffff', contentShadow: '0 8px 24px rgba(0,0,0,0.06)', contentBorder: '1px solid #ebeef5' }, maxImageWidth: 900, fontSize: '16px', lineHeight: '1.75' }, feishu: { name: '飞书云文档', imageMaxWidth: 1000, imageQuality: 0.92, displayMaxWidth: 800, imageFormat: 'auto', compressGif: false, enableCodeHighlight: true, enableThemeStyles: true, enableProgressNotice: true, description: '适合飞书文档,图片宽度1000px,质量0.85', compatibilityNotes: [ '支持完整的样式和代码高亮', '图片支持拖拽调整大小', '推荐使用此预设' ], styleStrategy: 'css', styleMode: 'inline-preferred', preserveThemeLevel: 'high', supports: { styleTag: true, className: true, table: 'good', codeBlock: 'good', math: 'medium' }, transforms: { flattenCallout: false, simplifyTable: false, simplifyNestedBlocks: false }, previewFrame: { background: '#eef2f7', padding: '24px', contentBackground: '#ffffff', contentShadow: '0 10px 28px rgba(15,23,42,0.08)', contentBorder: '1px solid #d8dee9' }, maxImageWidth: 1000, fontSize: '16px', lineHeight: '1.6' }, zhihu: { name: '知乎', imageMaxWidth: 800, imageQuality: 0.9, displayMaxWidth: 600, imageFormat: 'auto', compressGif: false, enableCodeHighlight: true, enableThemeStyles: true, enableProgressNotice: true, description: '适合知乎回答,图片宽度800px,质量0.8', compatibilityNotes: [ '代码高亮支持良好', '表格样式基本兼容', '图片支持点击查看大图' ], styleStrategy: 'css', styleMode: 'inline-preferred', preserveThemeLevel: 'medium', supports: { styleTag: true, className: true, table: 'partial', codeBlock: 'partial', math: 'weak' }, transforms: { flattenCallout: false, simplifyTable: true, simplifyNestedBlocks: true }, previewFrame: { background: '#f6f6f6', padding: '28px 18px', contentBackground: '#fffdf8', contentShadow: '0 12px 30px rgba(120, 113, 108, 0.08)', contentBorder: '1px solid #eee7da' }, maxImageWidth: 800, fontSize: '16px', lineHeight: '1.6' }, yuque: { name: '语雀', imageMaxWidth: 900, imageQuality: 0.9, displayMaxWidth: 700, imageFormat: 'auto', compressGif: false, enableCodeHighlight: true, enableThemeStyles: true, enableProgressNotice: true, description: '适合语雀文档,图片宽度900px,质量0.8', compatibilityNotes: [ '样式支持完整', '代码高亮效果佳', '推荐使用' ], styleStrategy: 'css', styleMode: 'inline-preferred', preserveThemeLevel: 'high', supports: { styleTag: true, className: true, table: 'good', codeBlock: 'good', math: 'medium' }, transforms: { flattenCallout: false, simplifyTable: false, simplifyNestedBlocks: false }, previewFrame: { background: '#f7f8fb', padding: '24px 18px', contentBackground: '#fcfcfd', contentShadow: '0 10px 26px rgba(99,102,241,0.06)', contentBorder: '1px solid #e6e8f0' }, maxImageWidth: 900, fontSize: '16px', lineHeight: '1.6' } }; const PLATFORM_THEMES = { official: { name: '官方简洁', fontFamily: "'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif", textColor: '#222222', mutedColor: '#57606a', backgroundColor: '#ffffff', accentColor: '#07c160', linkColor: '#576b95', blockquoteBorder: '#07c160', blockquoteBackground: '#f6fffb', codeBackground: '#f6f8fa', codeColor: '#1f2328', headingColor: '#111111', borderColor: '#d0d7de', borderRadius: '6px', paragraphSpacing: '16px', lineHeight: '1.75', headingDivider: true, tableHeaderFill: true, imageRounded: true, codeBlockBorder: true }, minimal: { name: '极简专业', fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif", textColor: '#1f2328', mutedColor: '#59636e', backgroundColor: '#ffffff', accentColor: '#2f80ed', linkColor: '#0969da', blockquoteBorder: '#2f80ed', blockquoteBackground: '#f6f8fa', codeBackground: '#f6f8fa', codeColor: '#24292f', headingColor: '#111827', borderColor: '#d8dee4', borderRadius: '8px', paragraphSpacing: '16px', lineHeight: '1.7', headingDivider: true, tableHeaderFill: true, imageRounded: true, codeBlockBorder: true }, knowledge: { name: '知识专栏', fontFamily: "'Noto Serif SC', 'Source Han Serif SC', Georgia, serif", textColor: '#2d2d2d', mutedColor: '#666666', backgroundColor: '#fffdf8', accentColor: '#d97706', linkColor: '#b45309', blockquoteBorder: '#f59e0b', blockquoteBackground: '#fff7ed', codeBackground: '#f8fafc', codeColor: '#1e293b', headingColor: '#1f2937', borderColor: '#e5dccf', borderRadius: '8px', paragraphSpacing: '18px', lineHeight: '1.85', headingDivider: false, tableHeaderFill: true, imageRounded: false, codeBlockBorder: false }, soft: { name: '柔和文档', fontFamily: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif", textColor: '#24303f', mutedColor: '#64748b', backgroundColor: '#fcfcfd', accentColor: '#7c3aed', linkColor: '#6d28d9', blockquoteBorder: '#8b5cf6', blockquoteBackground: '#f5f3ff', codeBackground: '#f8fafc', codeColor: '#1e293b', headingColor: '#0f172a', borderColor: '#e2e8f0', borderRadius: '10px', paragraphSpacing: '16px', lineHeight: '1.75', headingDivider: true, tableHeaderFill: true, imageRounded: true, codeBlockBorder: true }, 'wechat-business': { name: '公众号商务风', fontFamily: "'PingFang SC', 'Microsoft YaHei', sans-serif", textColor: '#1f1f1f', mutedColor: '#5f6368', backgroundColor: '#ffffff', accentColor: '#0f766e', linkColor: '#0f766e', blockquoteBorder: '#14b8a6', blockquoteBackground: '#f0fdfa', codeBackground: '#f8fafc', codeColor: '#0f172a', headingColor: '#111827', borderColor: '#dbe4ea', borderRadius: '4px', paragraphSpacing: '18px', lineHeight: '1.8', headingDivider: true, tableHeaderFill: false, imageRounded: false, codeBlockBorder: false }, 'wechat-minimal': { name: '公众号极简风', fontFamily: "'PingFang SC', 'Helvetica Neue', sans-serif", textColor: '#262626', mutedColor: '#737373', backgroundColor: '#ffffff', accentColor: '#3b82f6', linkColor: '#2563eb', blockquoteBorder: '#93c5fd', blockquoteBackground: '#eff6ff', codeBackground: '#f5f7fb', codeColor: '#1f2937', headingColor: '#111827', borderColor: '#e5e7eb', borderRadius: '2px', paragraphSpacing: '16px', lineHeight: '1.75', headingDivider: false, tableHeaderFill: false, imageRounded: false, codeBlockBorder: false }, 'zhihu-column': { name: '知乎长文风', fontFamily: "'Noto Serif SC', Georgia, serif", textColor: '#2b2b2b', mutedColor: '#646464', backgroundColor: '#fffefb', accentColor: '#2563eb', linkColor: '#1d4ed8', blockquoteBorder: '#60a5fa', blockquoteBackground: '#f8fbff', codeBackground: '#f8fafc', codeColor: '#111827', headingColor: '#1f2937', borderColor: '#e6e8ec', borderRadius: '6px', paragraphSpacing: '20px', lineHeight: '1.9', headingDivider: false, tableHeaderFill: true, imageRounded: false, codeBlockBorder: false }, 'feishu-team': { name: '飞书团队文档风', fontFamily: "Inter, 'PingFang SC', 'Segoe UI', sans-serif", textColor: '#172033', mutedColor: '#5b6475', backgroundColor: '#ffffff', accentColor: '#1456f0', linkColor: '#1456f0', blockquoteBorder: '#7aa2ff', blockquoteBackground: '#f5f8ff', codeBackground: '#f3f6fb', codeColor: '#172033', headingColor: '#111827', borderColor: '#dbe2ea', borderRadius: '8px', paragraphSpacing: '16px', lineHeight: '1.7', headingDivider: true, tableHeaderFill: true, imageRounded: true, codeBlockBorder: true }, 'yuque-notes': { name: '语雀知识库风', fontFamily: "'Inter', 'PingFang SC', sans-serif", textColor: '#243042', mutedColor: '#64748b', backgroundColor: '#fcfcfe', accentColor: '#7c3aed', linkColor: '#6d28d9', blockquoteBorder: '#a78bfa', blockquoteBackground: '#f5f3ff', codeBackground: '#f8fafc', codeColor: '#0f172a', headingColor: '#111827', borderColor: '#e5e7ef', borderRadius: '10px', paragraphSpacing: '17px', lineHeight: '1.78', headingDivider: true, tableHeaderFill: true, imageRounded: true, codeBlockBorder: true } }; const PLATFORM_THEME_RECOMMENDATIONS = { wechat: ['wechat-business', 'official', 'wechat-minimal'], feishu: ['feishu-team', 'minimal'], zhihu: ['zhihu-column', 'knowledge'], yuque: ['yuque-notes', 'soft'] }; const REMOVE_SELECTORS = [ '.collapse-indicator', '.internal-query-header', '.copy-code-button', '.markdown-embed-link', '.mod-header', '.metadata-container', '.metadata-properties', '.inline-title', '.cm-widgetBuffer', '.markdown-reading-view .print', 'script', 'style' ]; const BASE_STYLE_PROPS = [ 'color', 'background-color', 'font-size', 'font-family', 'font-weight', 'font-style', 'line-height', 'text-align', 'text-decoration', 'letter-spacing', 'margin-top', 'margin-bottom', 'margin-left', 'margin-right', 'padding-top', 'padding-bottom', 'padding-left', 'padding-right', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-radius', 'display', 'width', 'max-width', 'min-width', 'white-space', 'overflow-x', 'list-style-type' ]; const TAG_STYLE_PROPS = { p: ['margin-top', 'margin-bottom', 'line-height', 'text-align'], li: ['margin-top', 'margin-bottom', 'line-height'], blockquote: ['color', 'background-color', 'border-left', 'padding-left', 'margin-top', 'margin-bottom'], pre: ['background-color', 'color', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'border-radius', 'overflow-x', 'white-space'], code: ['background-color', 'color', 'font-family', 'font-size', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'border-radius', 'white-space'], table: ['width', 'border-collapse', 'margin-top', 'margin-bottom'], th: ['border-top', 'border-right', 'border-bottom', 'border-left', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'background-color', 'text-align'], td: ['border-top', 'border-right', 'border-bottom', 'border-left', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'background-color', 'text-align'], img: ['display', 'width', 'max-width', 'height', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', 'border-radius'], h1: ['color', 'font-size', 'font-weight', 'line-height', 'margin-top', 'margin-bottom'], h2: ['color', 'font-size', 'font-weight', 'line-height', 'margin-top', 'margin-bottom'], h3: ['color', 'font-size', 'font-weight', 'line-height', 'margin-top', 'margin-bottom'], h4: ['color', 'font-size', 'font-weight', 'line-height', 'margin-top', 'margin-bottom'], h5: ['color', 'font-size', 'font-weight', 'line-height', 'margin-top', 'margin-bottom'], h6: ['color', 'font-size', 'font-weight', 'line-height', 'margin-top', 'margin-bottom'] }; /* ========== 应用平台预设 ========== */ function applyPlatformPreset(platformKey) { const preset = PLATFORM_PRESETS[platformKey]; if (!preset) { console.warn('[bulk-copy] 未找到平台预设:', platformKey); return; } USER_CONFIG.currentPlatform = platformKey; USER_CONFIG.imageMaxWidth = preset.imageMaxWidth; USER_CONFIG.imageQuality = preset.imageQuality; USER_CONFIG.displayMaxWidth = preset.displayMaxWidth; USER_CONFIG.imageFormat = preset.imageFormat; USER_CONFIG.compressGif = preset.compressGif; USER_CONFIG.enableCodeHighlight = preset.enableCodeHighlight; USER_CONFIG.enableThemeStyles = preset.enableThemeStyles; USER_CONFIG.enableProgressNotice = preset.enableProgressNotice; IMAGE_CONFIG.MAX_WIDTH = preset.maxImageWidth || preset.imageMaxWidth; console.log('[bulk-copy] 已应用平台预设:', preset.name); new Notice(`✅ 已切换到 ${preset.name} 预设`); } function syncImageConfigFromUserConfig() { IMAGE_CONFIG.MAX_WIDTH = USER_CONFIG.imageMaxWidth; IMAGE_CONFIG.QUALITY = USER_CONFIG.imageQuality; IMAGE_CONFIG.DISPLAY_MAX_WIDTH = USER_CONFIG.displayMaxWidth; IMAGE_CONFIG.FORMAT = USER_CONFIG.imageFormat; IMAGE_CONFIG.COMPRESS_GIF = USER_CONFIG.compressGif; } function getCurrentImageStrategy(platformKey = USER_CONFIG.currentPlatform) { return USER_CONFIG.platformImageStrategies?.[platformKey] || 'quality'; } function getImageStrategyConfig(platformKey = USER_CONFIG.currentPlatform) { switch (getCurrentImageStrategy(platformKey)) { case 'size': return { keepPng: false, smallImageThreshold: 150 * 1024, qualityBoost: -0.12, resizeThresholdBoost: -200 }; case 'balanced': return { keepPng: true, smallImageThreshold: 280 * 1024, qualityBoost: -0.04, resizeThresholdBoost: 0 }; case 'quality': default: return { keepPng: true, smallImageThreshold: 500 * 1024, qualityBoost: 0, resizeThresholdBoost: 200 }; } } function getImageStrategyLabel(platformKey = USER_CONFIG.currentPlatform) { switch (getCurrentImageStrategy(platformKey)) { case 'size': return '体积优先'; case 'balanced': return '平衡'; case 'quality': default: return '高清优先'; } } function getCurrentPlatformTheme(platformKey = USER_CONFIG.currentPlatform) { return USER_CONFIG.platformThemes?.[platformKey] || 'minimal'; } function getAllThemeOptions() { return { ...PLATFORM_THEMES, ...(USER_CONFIG.customThemes || {}) }; } function getEditableCustomThemeKey() { return USER_CONFIG.customThemeEditorKey || 'custom'; } function getEditableCustomTheme() { const customThemes = USER_CONFIG.customThemes || DEFAULT_USER_CONFIG.customThemes; return customThemes[getEditableCustomThemeKey()] || customThemes.custom || DEFAULT_USER_CONFIG.customThemes.custom; } function getOrderedCustomThemeKeys() { const customThemes = USER_CONFIG.customThemes || DEFAULT_USER_CONFIG.customThemes; const allKeys = Object.keys(customThemes); const configuredOrder = USER_CONFIG.customThemeOrder || DEFAULT_USER_CONFIG.customThemeOrder || []; const ordered = configuredOrder.filter(key => allKeys.includes(key)); const missing = allKeys.filter(key => !ordered.includes(key)); return [...ordered, ...missing]; } function getPlatformThemeConfig(platformKey = USER_CONFIG.currentPlatform) { const allThemes = getAllThemeOptions(); const baseTheme = allThemes[getCurrentPlatformTheme(platformKey)] || allThemes.minimal || PLATFORM_THEMES.minimal; const overrides = USER_CONFIG.platformThemeOverrides?.[platformKey] || {}; return { ...baseTheme, ...overrides }; } function getPlatformThemeLabel(platformKey = USER_CONFIG.currentPlatform) { return getPlatformThemeConfig(platformKey).name; } function getRecommendedThemeKeys(platformKey) { return PLATFORM_THEME_RECOMMENDATIONS[platformKey] || []; } function isRecommendedTheme(platformKey, themeKey) { return getRecommendedThemeKeys(platformKey).includes(themeKey); } function getRecommendedThemeLabels(platformKey) { return getRecommendedThemeKeys(platformKey) .map(themeKey => getAllThemeOptions()[themeKey]?.name) .filter(Boolean); } function updatePlatformThemeOverride(platformKey, key, value) { USER_CONFIG.platformThemeOverrides = { ...DEFAULT_USER_CONFIG.platformThemeOverrides, ...(USER_CONFIG.platformThemeOverrides || {}), [platformKey]: { ...(USER_CONFIG.platformThemeOverrides?.[platformKey] || {}), [key]: value } }; } function resetPlatformToRecommendedTheme(platformKey) { const recommendedThemeKey = getRecommendedThemeKeys(platformKey)[0] || DEFAULT_USER_CONFIG.platformThemes[platformKey] || 'official'; USER_CONFIG.platformThemes = { ...DEFAULT_USER_CONFIG.platformThemes, ...(USER_CONFIG.platformThemes || {}), [platformKey]: recommendedThemeKey }; USER_CONFIG.platformThemeOverrides = { ...DEFAULT_USER_CONFIG.platformThemeOverrides, ...(USER_CONFIG.platformThemeOverrides || {}), [platformKey]: {} }; } function validateSingleThemeImport(parsed) { if (!parsed || typeof parsed !== 'object') { return 'JSON 顶层必须是对象'; } if (!parsed.theme || typeof parsed.theme !== 'object') { return '缺少 theme 对象'; } if (parsed.key && typeof parsed.key !== 'string') { return 'key 必须是字符串'; } return null; } function validateThemeCollectionImport(parsed) { if (!parsed || typeof parsed !== 'object') { return 'JSON 顶层必须是对象'; } if (!parsed.customThemes || typeof parsed.customThemes !== 'object') { return '缺少 customThemes 对象'; } if (parsed.customThemeOrder && !Array.isArray(parsed.customThemeOrder)) { return 'customThemeOrder 必须是数组'; } if (parsed.customThemeEditorKey && typeof parsed.customThemeEditorKey !== 'string') { return 'customThemeEditorKey 必须是字符串'; } return null; } function getThemeDetailDiffs(currentTheme, compareTheme) { const items = [ ['headingDivider', '标题分隔线'], ['tableHeaderFill', '表头底色'], ['imageRounded', '图片圆角'], ['codeBlockBorder', '代码块边框'] ]; return items .filter(([key]) => !!currentTheme && !!compareTheme && currentTheme[key] !== compareTheme[key]) .map(([key, label]) => `${label}: 当前${currentTheme[key] ? '开启' : '关闭'} / 推荐${compareTheme[key] ? '开启' : '关闭'}`); } function getThemeColorDiffs(currentTheme, compareTheme) { const items = [ ['accentColor', '强调色'], ['backgroundColor', '背景色'], ['codeBackground', '代码背景'], ['headingColor', '标题颜色'] ]; return items .filter(([key]) => !!currentTheme && !!compareTheme && currentTheme[key] !== compareTheme[key]) .map(([key, label]) => `${label}: 当前 ${currentTheme[key] || '-'} / 推荐 ${compareTheme[key] || '-'}`); } function applyCurrentPlatformConfig() { const preset = PLATFORM_PRESETS[USER_CONFIG.currentPlatform]; if (!preset) { syncImageConfigFromUserConfig(); return; } USER_CONFIG.platformImageStrategies = { ...DEFAULT_USER_CONFIG.platformImageStrategies, ...(USER_CONFIG.platformImageStrategies || {}) }; USER_CONFIG = { ...USER_CONFIG, imageMaxWidth: USER_CONFIG.imageMaxWidth ?? preset.imageMaxWidth, imageQuality: USER_CONFIG.imageQuality ?? preset.imageQuality, displayMaxWidth: USER_CONFIG.displayMaxWidth ?? preset.displayMaxWidth, imageFormat: USER_CONFIG.imageFormat || preset.imageFormat, compressGif: typeof USER_CONFIG.compressGif === 'boolean' ? USER_CONFIG.compressGif : preset.compressGif, enableCodeHighlight: typeof USER_CONFIG.enableCodeHighlight === 'boolean' ? USER_CONFIG.enableCodeHighlight : preset.enableCodeHighlight, enableThemeStyles: typeof USER_CONFIG.enableThemeStyles === 'boolean' ? USER_CONFIG.enableThemeStyles : preset.enableThemeStyles, enableProgressNotice: typeof USER_CONFIG.enableProgressNotice === 'boolean' ? USER_CONFIG.enableProgressNotice : preset.enableProgressNotice }; syncImageConfigFromUserConfig(); } /* ========== 安全大图转 base64 ========== */ async function buf2base64(buf) { return new Promise((resolve, reject) => { try { const bytes = new Uint8Array(buf); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } resolve(btoa(binary)); } catch (e) { reject(e); } }); } /* ========== 图片压缩和尺寸限制 ========== */ async function compressImage(buf, ext, maxWidth = IMAGE_CONFIG.MAX_WIDTH, quality = IMAGE_CONFIG.QUALITY) { return new Promise((resolve) => { try { const normalizedExt = (ext || '').toLowerCase(); const mime = getMime(normalizedExt); const blob = new Blob([buf], { type: mime }); const img = new Image(); const url = URL.createObjectURL(blob); const timeout = setTimeout(() => { URL.revokeObjectURL(url); resolve({ data: buf, needsConversion: false }); }, 30000); img.onload = () => { clearTimeout(timeout); URL.revokeObjectURL(url); try { let width = img.width; let height = img.height; if (width > maxWidth) { height = Math.round((height * maxWidth) / width); width = maxWidth; } if (width === img.width) { resolve({ data: buf, needsConversion: false, wasResized: false, mime }); return; } const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (ctx) { ctx.drawImage(img, 0, 0, width, height); const preservePng = normalizedExt === 'png' && IMAGE_CONFIG.FORMAT === 'auto'; const outputMime = preservePng ? 'image/png' : 'image/jpeg'; const outputQuality = outputMime === 'image/png' ? undefined : quality; canvas.toBlob( (compressedBlob) => { if (!compressedBlob) { resolve({ data: buf, needsConversion: false, wasResized: false, mime }); return; } const reader = new FileReader(); reader.onload = () => { resolve({ data: reader.result, needsConversion: true, mime: outputMime, wasResized: true }); }; reader.onerror = () => resolve({ data: buf, needsConversion: false, wasResized: false, mime }); reader.readAsArrayBuffer(compressedBlob); }, outputMime, outputQuality ); } else { resolve({ data: buf, needsConversion: false, wasResized: false, mime }); } } catch (e) { resolve({ data: buf, needsConversion: false, wasResized: false, mime }); } }; img.onerror = () => { clearTimeout(timeout); URL.revokeObjectURL(url); resolve({ data: buf, needsConversion: false, wasResized: false, mime }); }; img.src = url; } catch (err) { resolve({ data: buf, needsConversion: false, wasResized: false, mime: getMime(ext || '') }); } }); } /* ========== mime 映射 ========== */ const getMime = ext => ({ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', svg: 'image/svg+xml', webp: 'image/webp', bmp: 'image/bmp' }[ext.toLowerCase()] || 'application/octet-stream'); function shouldKeepOriginalImage(ext, mime, buf, maxWidth, platformKey = USER_CONFIG.currentPlatform) { const normalizedExt = (ext || '').toLowerCase(); const strategy = getImageStrategyConfig(platformKey); if (!mime.startsWith('image/')) { return true; } if (normalizedExt === 'png' && IMAGE_CONFIG.FORMAT === 'auto' && strategy.keepPng) { return true; } if ((normalizedExt === 'jpg' || normalizedExt === 'jpeg' || normalizedExt === 'webp') && buf && buf.byteLength < strategy.smallImageThreshold) { return true; } if (maxWidth >= 1200 + strategy.resizeThresholdBoost) { return true; } return false; } /* ========== 单张图内嵌 ========== */ async function inlineImage(imgName, alt, context) { const { plugin, baseFolder, assetsFolder } = context; const candidates = [ `${assetsFolder}/${imgName}`, `${baseFolder}/${imgName}`, imgName.startsWith('/') ? imgName.slice(1) : imgName ]; let imgFile = candidates .map(p => { try { return plugin.app.vault.getAbstractFileByPath(p); } catch (e) { return null; } }) .find(f => f instanceof TFile); if (!imgFile) { try { const all = plugin.app.vault.getFiles(); imgFile = all.find(f => f.name === imgName); } catch (e) { // ignore } } if (!imgFile) { console.warn('[bulk-copy] 未找到图片文件', imgName); return `![${alt}](${imgName})`; } try { const buf = await plugin.app.vault.readBinary(imgFile); const ext = imgFile.extension; const mime = getMime(ext); if (ext && ext.toLowerCase() === 'svg') { try { const svgContent = await plugin.app.vault.read(imgFile); const base64Svg = btoa(unescape(encodeURIComponent(svgContent))); const currentPreset = PLATFORM_PRESETS[USER_CONFIG.currentPlatform]; const maxWidth = currentPreset?.maxImageWidth || IMAGE_CONFIG.DISPLAY_MAX_WIDTH; return `${alt}`; } catch (e) { return `![${alt}](${imgName})`; } } if (ext && ext.toLowerCase() === 'gif') { try { const data = await buf2base64(buf); const currentPreset = PLATFORM_PRESETS[USER_CONFIG.currentPlatform]; const maxWidth = currentPreset?.maxImageWidth || IMAGE_CONFIG.DISPLAY_MAX_WIDTH; return `${alt}`; } catch (e) { return `![${alt}](${imgName})`; } } const currentPlatform = USER_CONFIG.currentPlatform; const currentPreset = PLATFORM_PRESETS[currentPlatform]; const maxWidth = currentPreset?.maxImageWidth || IMAGE_CONFIG.MAX_WIDTH; const strategy = getImageStrategyConfig(currentPlatform); const baseQuality = currentPreset?.imageQuality || IMAGE_CONFIG.QUALITY; const quality = Math.max(0.55, Math.min(0.98, baseQuality + strategy.qualityBoost)); const shouldKeepOriginal = shouldKeepOriginalImage(ext, mime, buf, maxWidth, currentPlatform); const compressed = shouldKeepOriginal ? { data: buf, needsConversion: false, wasResized: false, mime } : await compressImage(buf, ext, maxWidth, quality); let finalMime = mime; let finalData; if (compressed && compressed.needsConversion) { finalMime = compressed.mime; finalData = await buf2base64(compressed.data); } else if (compressed && compressed.data) { finalData = await buf2base64(compressed.data); } else { finalData = await buf2base64(buf); } if (!finalData) { return `![${alt}](${imgName})`; } const displayWidth = currentPreset?.displayMaxWidth || IMAGE_CONFIG.DISPLAY_MAX_WIDTH; const imgStyle = `max-width: ${typeof displayWidth === 'number' ? `${displayWidth}px` : displayWidth}; width: 100%; height: auto; display: block; margin: 15px auto; border-radius: 4px;`; const altText = alt ? alt.replace(/"/g, '"') : ''; return `${altText}`; } catch (e) { console.error('[bulk-copy] 图片处理失败:', imgName, e); return `![${alt}](${imgName})`; } } /* ========== Markdown 转 HTML(简化版) ========== */ function markdownToHtml(md) { if (!md) return ''; let html = md; // 转义 HTML 特殊字符 const escapeHtml = (text) => { if (typeof text !== 'string') return ''; return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); }; // 代码块(先处理,避免内部语法被转换) const codeBlocks = []; html = html.replace(/```[\s\S]*?```/g, (match) => { const id = codeBlocks.length; const code = match.replace(/```/g, '').trim(); codeBlocks.push(`
${escapeHtml(code)}
`); return `\n\n`; }); // 行内代码 const inlineCodes = []; html = html.replace(/`([^`\n]+)`/g, (match, code) => { const id = inlineCodes.length; inlineCodes.push(`${escapeHtml(code)}`); return ``; }); // Wiki 链接 [[link]] html = html.replace(/\[\[([^\]|]+?)(?:\|([^\]]+))?\]\]/g, (match, link, text) => { const display = text || link; return `${display}`; }); // 标签 #tag html = html.replace(/(? { return `#${tag}`; }); // 图片 html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => { return `${alt}`; }); // Wiki 图片 ![[img]] html = html.replace(/!\[\[([^\]|]+?)(?:\|[^\]]+)?\]\]/g, (match, img) => { const name = img.split('|')[0].trim(); return `${name}`; }); // 标题 html = html.replace(/^######\s+(.+)$/gm, '
$1
'); html = html.replace(/^#####\s+(.+)$/gm, '
$1
'); html = html.replace(/^####\s+(.+)$/gm, '

$1

'); html = html.replace(/^###\s+(.+)$/gm, '

$1

'); html = html.replace(/^##\s+(.+)$/gm, '

$1

'); html = html.replace(/^#\s+(.+)$/gm, '

$1

'); // 粗体 html = html.replace(/\*\*(.+?)\*\*/g, '$1'); html = html.replace(/__(.+?)__/g, '$1'); // 粗斜体 html = html.replace(/\*\*\*(.+?)\*\*\*/g, '$1'); html = html.replace(/___(.+?)___/g, '$1'); // 斜体 html = html.replace(/\*([^\s*][^*]*?)\*/g, '$1'); html = html.replace(/\b_([^\s_][^_]*?)_\b/g, '$1'); // 删除线 html = html.replace(/~~(.+?)~~/g, '$1'); // 高亮 html = html.replace(/==(.+?)==/g, '$1'); // 引用 html = html.replace(/^>\s+(.+)$/gm, '
$1
'); // 分割线 html = html.replace(/^(---|\*\*\*|___)$/gm, '
'); // 列表(先处理无序列表) html = html.replace(/^[\*\-\+]\s+(.+)$/gm, '
  • $1
  • '); // 处理有序列表 html = html.replace(/^\d+\.\s+(.+)$/gm, '
  • $1
  • '); // 任务列表 html = html.replace(/^[\*\-\+]\s+\[x\]\s+(.+)$/gm, '
  • $1
  • '); html = html.replace(/^[\*\-\+]\s+\[\s\]\s+(.+)$/gm, '
  • $1
  • '); html = html.replace(/^\d+\.\s+\[x\]\s+(.+)$/gm, '
  • $1
  • '); html = html.replace(/^\d+\.\s+\[\s\]\s+(.+)$/gm, '
  • $1
  • '); // 链接 html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // 表格 html = html.replace(/^\|(.+)\|[ \t]*$\n^\|[-:\s|]+\|[ \t]*$\n((?:^\|.+\|[ \t]*$\n?)*)/gm, (match, header, body) => { const headers = header.split('|').map(h => h.trim()).filter(h => h); const headerHtml = `${headers.map(h => `${h}`).join('')}`; const rows = body.trim().split('\n').map(row => { const cells = row.split('|').map(c => c.trim()).filter(c => c); return `${cells.map(c => `${c}`).join('')}`; }).join(''); return `${headerHtml}${rows}
    `; }); // 关键修复:处理换行 // 将单个换行符转换为
    ,保留段落结构 const lines = html.split('\n'); const processed = []; let inParagraph = false; let paragraphContent = []; for (const line of lines) { const trimmed = line.trim(); // 检查是否是块级元素或占位符或空行 const isBlockElement = trimmed.match(/^<(h[1-6]|ul|ol|li|blockquote|pre|hr|table|\/table|\/tbody|\/thead|div|\/div|section|article)/) || trimmed.match(/^`, 'g'), code); }); inlineCodes.forEach((code, i) => { html = html.replace(new RegExp(``, 'g'), code); }); return html; } /* ========== 替换图片为 HTML ========== */ async function replaceImagesWithHtml(md, context) { if (!md) return { html: md, stats: { count: 0, totalSize: '0KB' } }; const wikiMatches = []; const wikiRegex = /!\[\[([^\]|]+?)(?:\|[^\]]+)?\]\]/g; let match; while ((match = wikiRegex.exec(md)) !== null) { const name = match[1].split('|')[0].trim(); const altMatch = match[1].match(/\|(.+)$/); const alt = altMatch ? altMatch[1] : ''; wikiMatches.push({ name, alt, original: match[0] }); } const standardMatches = []; const standardRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; while ((match = standardRegex.exec(md)) !== null) { const alt = match[1]; const src = decodeURIComponent(match[2]).trim(); standardMatches.push({ name: src, alt, original: match[0] }); } if (wikiMatches.length === 0 && standardMatches.length === 0) { return { html: md, stats: { count: 0, totalSize: '0KB' } }; } if (USER_CONFIG.enableProgressNotice) { const totalImages = wikiMatches.length + standardMatches.length; new Notice(`📷 发现 ${totalImages} 张图片,正在处理...`); } const processAll = async (matches) => { const results = []; for (let i = 0; i < matches.length; i++) { if (USER_CONFIG.enableProgressNotice && i > 0 && i % 3 === 0) { new Notice(`正在处理图片 ${i + 1}/${matches.length}...`); } try { const result = await inlineImage(matches[i].name, matches[i].alt, context); results.push(result); } catch (e) { console.error('[bulk-copy] 图片处理失败:', matches[i].name, e); results.push(matches[i].original); } } return results; }; const wikiResults = await processAll(wikiMatches); const standardResults = await processAll(standardMatches); let result = md; wikiMatches.forEach((match, index) => { result = result.replace(match.original, wikiResults[index] || match.original); }); standardMatches.forEach((match, index) => { result = result.replace(match.original, standardResults[index] || match.original); }); const totalImages = wikiMatches.length + standardMatches.length; const totalSize = totalImages * 150; // KB per image (estimated) const formattedSize = totalSize > 1024 ? `${(totalSize / 1024).toFixed(1)}MB` : `${totalSize}KB`; return { html: result, stats: { count: totalImages, totalSize: formattedSize } }; } function resolveActiveMarkdownView(plugin) { const activeView = plugin.app.workspace.getActiveViewOfType?.(MarkdownView); if (!activeView) { return null; } const mode = activeView.getMode?.() || (activeView.currentMode ? activeView.currentMode.type : 'unknown'); return { view: activeView, mode }; } function getPreviewRoot(view, mode) { if (!view || !view.containerEl) { return null; } const normalizedMode = (mode || '').toLowerCase(); if (normalizedMode !== 'preview' && normalizedMode !== 'reading') { return null; } return view.containerEl.querySelector('.markdown-reading-view .markdown-preview-view, .markdown-reading-view .markdown-rendered, .markdown-preview-view.markdown-rendered'); } function cloneExportRoot(root, options) { if (!root) { return null; } const clone = root.cloneNode(true); if (!options || options.mode !== 'selection') { return clone; } return filterSelectionFromDom(clone, options.selectedText || ''); } function filterSelectionFromDom(root, selectedText) { if (!root || !selectedText) { return root; } const normalizedSelected = selectedText.replace(/\s+/g, ' ').trim(); if (!normalizedSelected) { return root; } const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null); const matches = []; while (walker.nextNode()) { const node = walker.currentNode; const text = (node.textContent || '').replace(/\s+/g, ' ').trim(); if (text && (text === normalizedSelected || text.includes(normalizedSelected))) { matches.push(node); } } if (matches.length === 0) { return root; } const wrapper = document.createElement('div'); matches.forEach(node => { if (node.childNodes.length === 1 && node.firstChild && node.firstChild.nodeType === Node.TEXT_NODE) { wrapper.appendChild(node.cloneNode(true)); } }); return wrapper.childNodes.length > 0 ? wrapper : root; } function sanitizeObsidianDom(root) { if (!root) { return root; } REMOVE_SELECTORS.forEach(selector => { root.querySelectorAll(selector).forEach(node => node.remove()); }); root.querySelectorAll('[contenteditable="true"], .is-collapsed').forEach(node => { node.removeAttribute('contenteditable'); node.classList.remove('is-collapsed'); }); root.querySelectorAll('*').forEach(node => { Array.from(node.attributes).forEach(attr => { if (attr.name.startsWith('data-') || attr.name === 'spellcheck' || attr.name === 'dir') { node.removeAttribute(attr.name); } }); }); return root; } function buildStylePolicy(platformPreset) { return { props: BASE_STYLE_PROPS, tagProps: TAG_STYLE_PROPS, allowClassName: !!platformPreset?.supports?.className, preserveTheme: platformPreset?.preserveThemeLevel !== 'none' }; } function inlineNodeStyle(targetNode, sourceNode, stylePolicy) { if (!(targetNode instanceof HTMLElement) || !(sourceNode instanceof HTMLElement)) { return; } const computed = window.getComputedStyle(sourceNode); const tagName = targetNode.tagName.toLowerCase(); const propSet = new Set([...(stylePolicy.props || []), ...((stylePolicy.tagProps && stylePolicy.tagProps[tagName]) || [])]); propSet.forEach(prop => { const value = computed.getPropertyValue(prop); if (!value) { return; } const trimmed = value.trim(); if (!trimmed || trimmed === 'none' || trimmed === 'normal' || trimmed === 'rgba(0, 0, 0, 0)') { return; } targetNode.style.setProperty(prop, trimmed); }); if (!stylePolicy.allowClassName) { targetNode.removeAttribute('class'); } } function inlineStylesFromSource(clonedRoot, sourceRoot, platformPreset) { if (!clonedRoot || !sourceRoot) { return clonedRoot; } const stylePolicy = buildStylePolicy(platformPreset); const clonedNodes = [clonedRoot, ...clonedRoot.querySelectorAll('*')]; const sourceNodes = [sourceRoot, ...sourceRoot.querySelectorAll('*')]; const total = Math.min(clonedNodes.length, sourceNodes.length); for (let i = 0; i < total; i++) { inlineNodeStyle(clonedNodes[i], sourceNodes[i], stylePolicy); } return clonedRoot; } function captureThemeTokens() { if (Cache.themeStyles) { return Cache.themeStyles; } const computed = window.getComputedStyle(document.body); Cache.themeStyles = { textNormal: computed.getPropertyValue('--text-normal').trim() || computed.color, textMuted: computed.getPropertyValue('--text-muted').trim(), backgroundPrimary: computed.getPropertyValue('--background-primary').trim() || '#ffffff', backgroundSecondary: computed.getPropertyValue('--background-secondary').trim(), borderColor: computed.getPropertyValue('--background-modifier-border').trim() || '#d0d7de', accentColor: computed.getPropertyValue('--interactive-accent').trim() || '#2f80ed', codeBackground: computed.getPropertyValue('--code-background').trim() || computed.getPropertyValue('--background-secondary').trim(), blockquoteBorder: computed.getPropertyValue('--blockquote-border-color').trim() || computed.getPropertyValue('--background-modifier-border').trim() }; return Cache.themeStyles; } function transformCallouts(root, platformPreset, themeStyle) { if (!platformPreset?.transforms?.flattenCallout) { return; } root.querySelectorAll('.callout').forEach(callout => { const title = callout.querySelector('.callout-title'); const content = callout.querySelector('.callout-content'); const wrapper = document.createElement('blockquote'); wrapper.style.borderLeft = `4px solid ${themeStyle.blockquoteBorder || themeStyle.accentColor || '#2f80ed'}`; wrapper.style.backgroundColor = themeStyle.blockquoteBackground || themeStyle.backgroundColor || '#f6f8fa'; wrapper.style.padding = '12px 16px'; wrapper.style.margin = '16px 0'; wrapper.style.borderRadius = themeStyle.borderRadius || '6px'; if (title) { const heading = document.createElement('p'); heading.textContent = title.textContent || ''; heading.style.fontWeight = '600'; heading.style.margin = '0 0 8px 0'; wrapper.appendChild(heading); } if (content) { Array.from(content.childNodes).forEach(node => wrapper.appendChild(node.cloneNode(true))); } else { Array.from(callout.childNodes).forEach(node => wrapper.appendChild(node.cloneNode(true))); } callout.replaceWith(wrapper); }); } function transformCodeBlocks(root, platformPreset, themeStyle) { root.querySelectorAll('pre').forEach(pre => { pre.style.backgroundColor = themeStyle.codeBackground || '#f6f8fa'; pre.style.color = themeStyle.codeColor || pre.style.color; pre.style.borderRadius = pre.style.borderRadius || '6px'; pre.style.padding = pre.style.padding || '12px 16px'; pre.style.overflowX = 'auto'; pre.style.border = themeStyle.codeBlockBorder ? `1px solid ${themeStyle.borderColor || '#d0d7de'}` : 'none'; if (platformPreset?.supports?.codeBlock === 'partial') { pre.querySelectorAll('[class]').forEach(node => node.removeAttribute('class')); } if (platformPreset?.name === '微信公众号') { pre.style.backgroundColor = themeStyle.codeBackground || '#f7f7f7'; pre.style.borderLeft = `4px solid ${themeStyle.accentColor || '#07c160'}`; pre.style.borderTop = 'none'; pre.style.borderRight = 'none'; pre.style.borderBottom = 'none'; pre.style.borderRadius = '0 8px 8px 0'; pre.style.whiteSpace = 'pre-wrap'; pre.style.wordBreak = 'break-word'; pre.style.fontSize = '13px'; pre.querySelectorAll('code').forEach(code => { code.removeAttribute('class'); code.style.backgroundColor = 'transparent'; code.style.padding = '0'; code.style.borderRadius = '0'; code.style.fontSize = '13px'; code.style.color = themeStyle.codeColor || '#1f2937'; }); } }); } function transformTables(root, platformPreset, themeStyle) { root.querySelectorAll('table').forEach(table => { table.style.width = '100%'; table.style.borderCollapse = 'collapse'; table.querySelectorAll('th, td').forEach(cell => { cell.style.border = `1px solid ${themeStyle.borderColor || '#d0d7de'}`; cell.style.padding = '8px 12px'; }); table.querySelectorAll('th').forEach(cell => { cell.style.backgroundColor = themeStyle.tableHeaderFill ? (themeStyle.codeBackground || '#f6f8fa') : 'transparent'; cell.style.color = themeStyle.headingColor || cell.style.color; }); if (platformPreset?.transforms?.simplifyTable) { table.removeAttribute('class'); } }); } function transformTaskLists(root, platformPreset, themeStyle) { root.querySelectorAll('li.task-list-item').forEach(item => { const checkbox = item.querySelector('input[type="checkbox"]'); if (!checkbox) { return; } const checked = checkbox.checked; checkbox.remove(); const marker = document.createElement('span'); marker.textContent = checked ? '☑ ' : '☐ '; marker.style.color = themeStyle.accentColor || themeStyle.textColor || '#333'; marker.style.fontWeight = '600'; if (checked) { item.style.opacity = '0.8'; item.style.textDecoration = 'line-through'; } else { item.style.opacity = '1'; item.style.textDecoration = 'none'; } item.insertBefore(marker, item.firstChild); }); } function transformNestedLists(root, platformPreset, themeStyle) { root.querySelectorAll('ul ul, ul ol, ol ul, ol ol').forEach(list => { list.style.marginTop = '6px'; list.style.marginBottom = '6px'; list.style.paddingLeft = '1.2em'; }); if (platformPreset?.name === '微信公众号') { root.querySelectorAll('li ul, li ol').forEach(list => { list.style.borderLeft = `2px solid ${themeStyle.borderColor || '#d0d7de'}`; list.style.paddingLeft = '12px'; }); } } function transformFootnotes(root, platformPreset, themeStyle) { root.querySelectorAll('.footnotes, section.footnotes').forEach(section => { const title = document.createElement('p'); title.textContent = '注释'; title.style.fontWeight = '700'; title.style.margin = '20px 0 10px'; section.insertBefore(title, section.firstChild); }); root.querySelectorAll('sup a.footnote-link, a.footnote-backref').forEach(link => { link.style.color = themeStyle.linkColor || themeStyle.accentColor || '#576b95'; link.style.textDecoration = 'none'; }); } function transformNestedBlockquotes(root, platformPreset, themeStyle) { root.querySelectorAll('blockquote blockquote').forEach(blockquote => { blockquote.style.margin = '10px 0 0'; blockquote.style.backgroundColor = themeStyle.backgroundColor || '#fff'; blockquote.style.borderLeft = `3px solid ${themeStyle.borderColor || '#d0d7de'}`; blockquote.style.opacity = '0.95'; }); if (platformPreset?.name === '微信公众号') { root.querySelectorAll('blockquote blockquote blockquote').forEach(blockquote => { const text = document.createElement('p'); text.textContent = blockquote.textContent?.trim() || ''; text.style.margin = '8px 0 0'; blockquote.replaceWith(text); }); } } function transformEmbeds(root, platformPreset, themeStyle) { root.querySelectorAll('.internal-embed, .markdown-embed').forEach(embed => { const replacement = document.createElement('blockquote'); replacement.style.borderLeft = `4px solid ${themeStyle.blockquoteBorder || themeStyle.accentColor || '#999'}`; replacement.style.backgroundColor = themeStyle.blockquoteBackground || '#f8f8f8'; replacement.style.padding = '12px 16px'; replacement.style.margin = '16px 0'; replacement.style.borderRadius = themeStyle.borderRadius || '6px'; const image = embed.querySelector('img'); if (image) { const title = document.createElement('p'); title.textContent = '图片嵌入'; title.style.fontWeight = '700'; title.style.margin = '0 0 8px'; replacement.appendChild(title); replacement.appendChild(image.cloneNode(true)); const caption = document.createElement('p'); caption.textContent = image.getAttribute('alt') || '嵌入图片说明'; caption.style.margin = '10px 0 0'; caption.style.fontSize = '13px'; caption.style.color = themeStyle.mutedColor || '#666'; replacement.appendChild(caption); } else { const title = document.createElement('p'); title.textContent = '内容嵌入'; title.style.fontWeight = '700'; title.style.margin = '0 0 8px'; replacement.appendChild(title); const body = document.createElement('p'); body.textContent = embed.textContent?.trim() || '嵌入内容'; body.style.margin = '0'; replacement.appendChild(body); } embed.replaceWith(replacement); }); } function transformMathBlocks(root, platformPreset, themeStyle) { root.querySelectorAll('.math, .katex, mjx-container').forEach(node => { const replacement = document.createElement('div'); replacement.style.backgroundColor = themeStyle.codeBackground || '#f6f8fa'; replacement.style.color = themeStyle.codeColor || '#333'; replacement.style.padding = '12px 16px'; replacement.style.borderRadius = themeStyle.borderRadius || '6px'; replacement.style.margin = '16px 0'; replacement.style.borderLeft = `4px solid ${themeStyle.accentColor || '#07c160'}`; const title = document.createElement('p'); title.textContent = '公式'; title.style.margin = '0 0 8px'; title.style.fontWeight = '700'; replacement.appendChild(title); const body = document.createElement('pre'); body.textContent = node.textContent?.trim() || '数学公式'; body.style.margin = '0'; body.style.whiteSpace = 'pre-wrap'; body.style.background = 'transparent'; body.style.border = 'none'; body.style.padding = '0'; replacement.appendChild(body); node.replaceWith(replacement); }); } function transformHorizontalRules(root, platformPreset, themeStyle) { root.querySelectorAll('hr').forEach(node => { node.style.border = 'none'; node.style.borderTop = `1px solid ${themeStyle.borderColor || '#d0d7de'}`; node.style.margin = '24px 0'; }); } function transformWechatTablesToCards(root, platformPreset, themeStyle) { if (platformPreset?.name !== '微信公众号') { return; } root.querySelectorAll('table').forEach(table => { const wrapper = document.createElement('div'); wrapper.style.margin = '16px 0'; wrapper.style.border = `1px solid ${themeStyle.borderColor || '#d0d7de'}`; wrapper.style.borderRadius = themeStyle.borderRadius || '8px'; wrapper.style.overflow = 'hidden'; wrapper.style.backgroundColor = themeStyle.backgroundColor || '#fff'; const header = document.createElement('div'); header.textContent = '表格内容'; header.style.padding = '10px 12px'; header.style.fontWeight = '700'; header.style.backgroundColor = themeStyle.codeBackground || '#f6f8fa'; header.style.borderBottom = `1px solid ${themeStyle.borderColor || '#d0d7de'}`; wrapper.appendChild(header); const rows = Array.from(table.querySelectorAll('tr')); rows.forEach((row, rowIndex) => { const rowCard = document.createElement('div'); rowCard.style.display = 'block'; rowCard.style.padding = '10px 12px'; rowCard.style.borderBottom = rowIndex < rows.length - 1 ? `1px solid ${themeStyle.borderColor || '#d0d7de'}` : 'none'; rowCard.style.backgroundColor = rowIndex % 2 === 0 ? (themeStyle.backgroundColor || '#fff') : (themeStyle.blockquoteBackground || '#fafafa'); const rowTitle = document.createElement('p'); rowTitle.textContent = rowIndex === 0 ? '表头' : `第 ${rowIndex} 行`; rowTitle.style.margin = '0 0 6px'; rowTitle.style.fontWeight = '600'; rowTitle.style.color = themeStyle.headingColor || '#111'; rowCard.appendChild(rowTitle); const cells = Array.from(row.children); const compactTwoColumn = rowIndex > 0 && cells.length === 2; if (compactTwoColumn) { const compact = document.createElement('p'); compact.style.margin = '4px 0'; compact.style.lineHeight = themeStyle.lineHeight || '1.7'; compact.innerHTML = `${cells[0].textContent?.trim() || '字段'}:${cells[1].textContent?.trim() || ''}`; rowCard.appendChild(compact); } else { cells.forEach((cell, cellIndex) => { const line = document.createElement('p'); line.style.margin = '4px 0'; line.style.lineHeight = themeStyle.lineHeight || '1.7'; const labelPrefix = rowIndex === 0 ? `字段 ${cellIndex + 1}` : `${rows[0]?.children[cellIndex]?.textContent?.trim() || `列 ${cellIndex + 1}`}`; line.innerHTML = `${labelPrefix}:${cell.textContent?.trim() || ''}`; rowCard.appendChild(line); }); } wrapper.appendChild(rowCard); }); table.replaceWith(wrapper); }); } function transformWechatSpecificBlocks(root, platformPreset, themeStyle) { if (platformPreset?.name !== '微信公众号') { return; } transformTaskLists(root, platformPreset, themeStyle); transformNestedLists(root, platformPreset, themeStyle); transformFootnotes(root, platformPreset, themeStyle); transformNestedBlockquotes(root, platformPreset, themeStyle); transformEmbeds(root, platformPreset, themeStyle); transformMathBlocks(root, platformPreset, themeStyle); transformHorizontalRules(root, platformPreset, themeStyle); transformWechatTablesToCards(root, platformPreset, themeStyle); } function normalizeLinks(root) { root.querySelectorAll('a').forEach(link => { link.setAttribute('target', '_blank'); link.setAttribute('rel', 'noopener noreferrer'); const href = link.getAttribute('href') || ''; if (href.startsWith('app://') || href.startsWith('obsidian://')) { link.removeAttribute('href'); } }); } function applyThemeToRoot(root, theme) { if (!root || !theme) { return root; } root.style.fontFamily = theme.fontFamily; root.style.color = theme.textColor; root.style.backgroundColor = theme.backgroundColor; root.style.lineHeight = theme.lineHeight; root.querySelectorAll('p').forEach(node => { node.style.color = theme.textColor; node.style.margin = `${theme.paragraphSpacing} 0`; node.style.lineHeight = theme.lineHeight; }); root.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(node => { node.style.color = theme.headingColor; node.style.fontFamily = theme.fontFamily; node.style.fontWeight = '700'; node.style.lineHeight = '1.35'; node.style.marginTop = '1.4em'; node.style.marginBottom = '0.7em'; }); root.querySelectorAll('h1, h2').forEach(node => { node.style.paddingBottom = theme.headingDivider ? '0.25em' : '0'; node.style.borderBottom = theme.headingDivider ? `1px solid ${theme.borderColor}` : 'none'; }); root.querySelectorAll('h1').forEach(node => { node.style.fontSize = '2em'; }); root.querySelectorAll('h2').forEach(node => { node.style.fontSize = '1.6em'; }); root.querySelectorAll('h3').forEach(node => { node.style.fontSize = '1.3em'; }); root.querySelectorAll('h4, h5, h6').forEach(node => { node.style.fontSize = '1.05em'; }); root.querySelectorAll('a').forEach(node => { node.style.color = theme.linkColor; node.style.textDecoration = 'none'; }); root.querySelectorAll('ul, ol').forEach(node => { node.style.paddingLeft = '1.6em'; node.style.margin = `${theme.paragraphSpacing} 0`; }); root.querySelectorAll('li').forEach(node => { node.style.margin = '0.35em 0'; node.style.color = theme.textColor; node.style.lineHeight = theme.lineHeight; }); root.querySelectorAll('blockquote').forEach(node => { node.style.borderLeft = `4px solid ${theme.blockquoteBorder}`; node.style.backgroundColor = theme.blockquoteBackground; node.style.padding = '12px 16px'; node.style.borderRadius = theme.borderRadius; node.style.color = theme.mutedColor; }); root.querySelectorAll('pre').forEach(node => { node.style.backgroundColor = theme.codeBackground; node.style.color = theme.codeColor; node.style.borderRadius = theme.borderRadius; node.style.border = theme.codeBlockBorder ? `1px solid ${theme.borderColor}` : 'none'; node.style.padding = '14px 16px'; node.style.margin = `${theme.paragraphSpacing} 0`; node.style.overflowX = 'auto'; }); root.querySelectorAll('code').forEach(node => { node.style.backgroundColor = theme.codeBackground; node.style.color = theme.codeColor; node.style.borderRadius = '4px'; node.style.padding = '0.15em 0.35em'; node.style.fontSize = '0.92em'; }); root.querySelectorAll('table').forEach(node => { node.style.borderCollapse = 'collapse'; node.style.border = `1px solid ${theme.borderColor}`; node.style.width = '100%'; node.style.margin = `${theme.paragraphSpacing} 0`; node.style.overflow = 'hidden'; node.style.borderRadius = theme.borderRadius; }); root.querySelectorAll('th, td').forEach(node => { node.style.border = `1px solid ${theme.borderColor}`; node.style.padding = '10px 12px'; }); root.querySelectorAll('th').forEach(node => { node.style.backgroundColor = theme.tableHeaderFill ? theme.codeBackground : 'transparent'; node.style.color = theme.headingColor; node.style.fontWeight = '600'; }); root.querySelectorAll('img').forEach(node => { node.style.borderRadius = theme.imageRounded ? theme.borderRadius : '0'; node.style.margin = `${theme.paragraphSpacing} auto`; node.style.display = 'block'; }); root.querySelectorAll('span, strong, em, del, mark').forEach(node => { if (!node.style.color || node.style.color === 'inherit') { node.style.color = theme.textColor; } node.style.fontFamily = theme.fontFamily; }); return root; } function collectExportWarnings(root, platformPreset, context) { const warnings = []; if (root.querySelector('.math, .katex, mjx-container')) { warnings.push(`检测到数学公式,${platformPreset?.name || '当前平台'} 可能无法完整保留公式样式`); } if (root.querySelector('table') && platformPreset?.supports?.table === 'partial') { warnings.push(`检测到表格,${platformPreset?.name || '当前平台'} 可能会简化表格样式`); } if (root.querySelector('pre') && platformPreset?.supports?.codeBlock === 'partial') { warnings.push(`检测到代码块,${platformPreset?.name || '当前平台'} 可能无法完整保留语法高亮`); } if (root.querySelector('.callout') && platformPreset?.transforms?.flattenCallout) { warnings.push(`检测到 callout,已为 ${platformPreset?.name || '当前平台'} 自动降级为普通卡片样式`); } if (platformPreset?.name === '微信公众号') { if (root.querySelector('li.task-list-item')) { warnings.push('检测到任务列表,微信公众号已降级为符号清单样式'); } if (root.querySelector('ul ul, ul ol, ol ul, ol ol')) { warnings.push('检测到嵌套列表,微信公众号已简化缩进与层级表现'); } if (root.querySelector('blockquote blockquote')) { warnings.push('检测到多层引用,微信公众号已压平为更稳定的引用结构'); } if (root.querySelector('table')) { warnings.push('检测到表格,微信公众号已降级为卡片式字段展示'); } if (root.querySelector('.internal-embed, .markdown-embed')) { warnings.push('检测到嵌入块,微信公众号已按图片嵌入或内容嵌入降级展示'); } if (root.querySelector('.footnotes, section.footnotes')) { warnings.push('检测到脚注,微信公众号已降级为普通注释区块'); } } return warnings; } function adaptDomForPlatform(root, platformPreset, context) { const themeStyle = context?.platformTheme || captureThemeTokens(); transformCallouts(root, platformPreset, themeStyle); transformCodeBlocks(root, platformPreset, themeStyle); transformTables(root, platformPreset, themeStyle); transformWechatSpecificBlocks(root, platformPreset, themeStyle); normalizeLinks(root, platformPreset, context); return root; } async function resolveImageSource(imgEl, context) { const src = imgEl.getAttribute('src') || ''; if (!src || src.startsWith('data:')) { return { kind: 'passthrough', src }; } if (/^https?:\/\//i.test(src)) { return { kind: 'remote', src }; } const alt = imgEl.getAttribute('alt') || ''; // 处理 Obsidian 预览 DOM 中的内部资源 URL(app://local/...) // 以及 blob: URL,提取文件名后在 vault 中查找 if (src.startsWith('app://') || src.startsWith('blob:')) { let filename = ''; if (src.startsWith('app://')) { // app://local/绝对路径/文件名.ext?参数 → 取最后一段去掉查询参数 const pathPart = src.replace(/^app:\/\/[^/]+/, ''); // 去掉 app://local const lastSegment = pathPart.split('/').pop() || ''; filename = decodeURIComponent(lastSegment.split('?')[0]); } // blob: URL 无法提取文件名,尝试用 alt 作为备选 if (!filename && alt) { filename = alt; } if (filename) { const html = await inlineImage(filename, alt, context); return { kind: 'vault', html }; } return { kind: 'passthrough', src }; } const decoded = decodeURIComponent(src).replace(/^\.\//, ''); const html = await inlineImage(decoded, alt, context); return { kind: 'vault', html }; } async function replaceImageSrcWithBase64(imgEl, context) { const resolved = await resolveImageSource(imgEl, context); if (resolved.kind === 'passthrough' || resolved.kind === 'remote') { return resolved.kind; } const wrapper = document.createElement('div'); wrapper.innerHTML = resolved.html; const newImg = wrapper.querySelector('img'); if (newImg) { imgEl.replaceWith(newImg); return 'inlined'; } // resolved.html 可能是 Markdown 回退文本(找不到文件时 inlineImage 返回 ![alt](name)) // 记录一下原始 src 便于排查 console.warn('[bulk-copy] 图片内联失败,无法在 vault 中找到文件。原始 src:', imgEl.getAttribute('src'), '解析结果:', resolved.html); return 'failed'; } async function inlineImagesInDom(root, context) { const images = Array.from(root.querySelectorAll('img')); let inlined = 0; for (const img of images) { try { const result = await replaceImageSrcWithBase64(img, context); if (result === 'inlined') { inlined++; } } catch (e) { console.error('[bulk-copy] DOM 图片内联失败:', e); } } return { count: inlined, totalSize: `${inlined * 150}KB` }; } async function buildOffscreenRenderedDom(plugin, markdown, context) { if (!markdown || !MarkdownRenderer || typeof MarkdownRenderer.render !== 'function') { return null; } const container = document.createElement('div'); container.addClass('markdown-rendered'); try { await MarkdownRenderer.render(plugin.app, markdown, container, context.file?.path || '', plugin); sanitizeObsidianDom(container); return container; } catch (e) { console.error('[bulk-copy] 离屏渲染失败:', e); return null; } } function buildClipboardPayload(exportResult) { const payload = {}; if (USER_CONFIG.clipboardFormats.includes('text/plain')) { payload['text/plain'] = new Blob([exportResult.plainText], { type: 'text/plain' }); } if (USER_CONFIG.clipboardFormats.includes('text/html')) { payload['text/html'] = new Blob([exportResult.html], { type: 'text/html' }); } return payload; } function legacyCopyToClipboard(html, text) { return new Promise((resolve, reject) => { const listener = (event) => { event.preventDefault(); try { if (event.clipboardData) { if (html) { event.clipboardData.setData('text/html', html); } event.clipboardData.setData('text/plain', text || ''); resolve(); return; } } catch (error) { reject(error); } reject(new Error('无法访问 clipboardData')); }; document.addEventListener('copy', listener, { once: true }); try { const successful = document.execCommand('copy'); if (!successful) { document.removeEventListener('copy', listener); reject(new Error('execCommand(copy) 返回失败')); } } catch (error) { document.removeEventListener('copy', listener); reject(error); } }); } function writeWithElectronClipboard(html, text) { if (!electronClipboard) { return false; } try { const wrappedHtml = html ? `${html}` : ''; electronClipboard.write({ html: wrappedHtml, text: text || '' }); console.log('[bulk-copy] 已使用 electron clipboard 写入系统剪贴板'); return true; } catch (error) { console.warn('[bulk-copy] electron clipboard 写入失败:', error); return false; } } async function writeClipboardPayload(payload, exportResult) { const fallbackText = exportResult.plainText || ''; const fallbackHtml = exportResult.html || ''; if (navigator.clipboard && window.ClipboardItem) { try { const clipboardItem = new ClipboardItem(payload); await navigator.clipboard.write([clipboardItem]); console.log('[bulk-copy] 已使用 ClipboardItem 写入剪贴板'); return; } catch (error) { console.warn('[bulk-copy] ClipboardItem 写入失败,尝试回退:', error); } } if (writeWithElectronClipboard(fallbackHtml, fallbackText)) { return; } if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { try { await navigator.clipboard.writeText(fallbackText); console.log('[bulk-copy] 已使用 writeText 写入纯文本剪贴板'); return; } catch (error) { console.warn('[bulk-copy] writeText 纯文本写入失败,尝试 execCommand 回退:', error); } } try { await legacyCopyToClipboard(fallbackHtml, fallbackText); console.log('[bulk-copy] 已使用 execCommand 回退写入剪贴板'); return; } catch (error) { console.warn('[bulk-copy] execCommand 富文本复制失败:', error); } throw new Error('当前环境不支持可用的剪贴板写入方式'); } async function buildExportDom(plugin, options, context) { const resolvedView = resolveActiveMarkdownView(plugin); const previewRoot = getPreviewRoot(resolvedView && resolvedView.view, resolvedView && resolvedView.mode); if (previewRoot && options.mode !== 'selection') { console.log('[bulk-copy] 使用阅读视图预览 DOM 导出', { mode: resolvedView && resolvedView.mode }); const clonedRoot = cloneExportRoot(previewRoot, options); sanitizeObsidianDom(clonedRoot); inlineStylesFromSource(clonedRoot, previewRoot, PLATFORM_PRESETS[options.platform]); adaptDomForPlatform(clonedRoot, PLATFORM_PRESETS[options.platform], context); const imageStats = await inlineImagesInDom(clonedRoot, context); const warnings = collectExportWarnings(clonedRoot, PLATFORM_PRESETS[options.platform], context); return { root: clonedRoot, meta: { source: 'preview-dom', imageCount: imageStats.count, warnings } }; } const markdown = options.selectedText || await plugin.app.vault.read(context.file); const offscreenRoot = await buildOffscreenRenderedDom(plugin, markdown, context); if (offscreenRoot) { console.log('[bulk-copy] 使用离屏渲染导出', { mode: resolvedView && resolvedView.mode }); inlineStylesFromSource(offscreenRoot, offscreenRoot, PLATFORM_PRESETS[options.platform]); adaptDomForPlatform(offscreenRoot, PLATFORM_PRESETS[options.platform], context); const imageStats = await inlineImagesInDom(offscreenRoot, context); const warnings = collectExportWarnings(offscreenRoot, PLATFORM_PRESETS[options.platform], context); return { root: offscreenRoot, meta: { source: 'offscreen-render', imageCount: imageStats.count, warnings } }; } return null; } async function exportContentForClipboard(plugin, options) { const file = plugin.app.workspace.getActiveFile(); const platformPreset = PLATFORM_PRESETS[options.platform] || PLATFORM_PRESETS.feishu; const platformTheme = getPlatformThemeConfig(options.platform); console.log('[bulk-copy] 当前导出主题', { platform: options.platform, themeKey: getCurrentPlatformTheme(options.platform), themeName: platformTheme.name }); const baseFolder = file.parent?.path || ''; const assetsFolder = `${baseFolder}/${file.basename}.assets`; const context = { plugin, baseFolder, assetsFolder, file, platformPreset, platformTheme }; const domResult = await buildExportDom(plugin, options, context); if (domResult && domResult.root) { const container = document.createElement('div'); container.style.maxWidth = typeof platformPreset.displayMaxWidth === 'number' ? `${platformPreset.displayMaxWidth}px` : String(platformPreset.displayMaxWidth || '800px'); container.style.margin = '0 auto'; container.style.fontSize = platformPreset.fontSize || '16px'; container.style.lineHeight = platformTheme.lineHeight || platformPreset.lineHeight || '1.6'; container.style.fontFamily = platformTheme.fontFamily; container.style.color = platformTheme.textColor; container.style.backgroundColor = platformTheme.backgroundColor; applyThemeToRoot(domResult.root, platformTheme); container.appendChild(domResult.root); return { html: container.outerHTML, plainText: options.selectedText || domResult.root.textContent || '', meta: { ...domResult.meta, theme: getCurrentPlatformTheme(options.platform) } }; } let md = options.selectedText; if (!md) { md = await plugin.app.vault.read(file); } const imageResult = await replaceImagesWithHtml(md, context); const htmlContent = markdownToHtml(imageResult.html); const wrappedHtml = `
    ${htmlContent}
    `; return { html: wrappedHtml, plainText: md, meta: { source: 'markdown-fallback', imageCount: imageResult.stats.count, warnings: ['当前未检测到可用预览 DOM,已回退到 Markdown 转 HTML 路径'], theme: getCurrentPlatformTheme(options.platform) } }; } /* ========== 平台选择 Modal ========== */ class PlatformSelectorModal extends Modal { constructor(app, callback) { super(app); this.callback = callback; this.selectedPlatform = USER_CONFIG.currentPlatform || 'feishu'; } onOpen() { const { contentEl } = this; contentEl.empty(); contentEl.addClass('platform-selector-modal'); const header = contentEl.createEl('div', { cls: 'modal-header' }); header.createEl('h2', { text: '📋 选择目标平台' }); header.createEl('p', { text: '选择你要复制到的平台,将自动应用最佳样式' }); const platformContainer = contentEl.createEl('div', { cls: 'platform-options' }); Object.keys(PLATFORM_PRESETS).forEach(platformKey => { const preset = PLATFORM_PRESETS[platformKey]; const strategyLabel = getImageStrategyLabel(platformKey); const themeLabel = getPlatformThemeLabel(platformKey); const recommendedThemeLabels = getRecommendedThemeLabels(platformKey); const qualityLabel = typeof preset.imageQuality === 'number' ? preset.imageQuality.toFixed(2) : String(preset.imageQuality || '-'); const widthLabel = typeof preset.imageMaxWidth === 'number' ? `${preset.imageMaxWidth}px` : String(preset.imageMaxWidth || '-'); const option = platformContainer.createEl('div', { cls: 'platform-option' }); const icons = { wechat: '💬', feishu: '📝', zhihu: '💡', yuque: '📚' }; option.innerHTML = `
    ${icons[platformKey] || '📱'}
    ${preset.name}
    ${preset.description}
    ${themeLabel}
    推荐 ${recommendedThemeLabels.join(' / ') || '无'}
    `; option.title = `图片策略: ${strategyLabel} | 压缩质量: ${qualityLabel} | 最大宽度: ${widthLabel}`; // 设置初始选中状态 if (this.selectedPlatform === platformKey) { option.classList.add('selected'); } // 点击事件:直接选择并关闭 option.addEventListener('click', async () => { this.selectedPlatform = platformKey; option.addClass('selected'); try { new Notice(`⏳ 正在复制到 ${PLATFORM_PRESETS[platformKey].name}...`); await this.callback(platformKey); } finally { this.close(); } }); }); // 应用样式 this.applyStyles(contentEl); } applyStyles(container) { const style = document.createElement('style'); style.innerHTML = ` .platform-selector-modal { max-width: 560px; } .platform-selector-modal .modal-header { text-align: center; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid var(--background-modifier-border); } .platform-selector-modal .modal-header h2 { margin-top: 0; margin-bottom: 6px; font-size: 1.1rem; } .platform-selector-modal .modal-header p { color: var(--text-muted); margin: 0; } .platform-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-bottom: 8px; } .platform-option { padding: 10px 12px; border: 1px solid var(--background-modifier-border); border-radius: 10px; cursor: pointer; transition: all 0.2s ease; background: var(--background-primary); } .platform-option:hover { border-color: var(--interactive-accent); background: var(--background-secondary); } .platform-option.selected { border-color: var(--interactive-accent); background: rgba(var(--interactive-accent-rgb), 0.1); } .platform-option .option-content { display: flex; align-items: flex-start; gap: 8px; } .platform-option .platform-icon { font-size: 22px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: var(--background-secondary); border-radius: 8px; } .platform-option .platform-info { flex: 1; } .platform-option .platform-name { font-weight: 600; font-size: 0.92rem; margin-bottom: 2px; color: var(--text-normal); } .platform-option .platform-desc { font-size: 0.76rem; color: var(--text-muted); line-height: 1.45; } .platform-option .platform-meta-row { display: flex; flex-wrap: wrap; gap: 4px 8px; margin-top: 4px; } .platform-option .platform-meta-row.muted { color: var(--text-muted); } .platform-option .platform-strategy { font-size: 0.78rem; color: var(--interactive-accent); } .platform-option .platform-theme { font-size: 0.75rem; color: var(--text-normal); padding: 2px 6px; border-radius: 999px; background: var(--background-secondary); } .platform-option .platform-recommended-theme { font-size: 0.72rem; } .platform-option .platform-metrics { font-size: 0.75rem; } .platform-option .check-icon { font-size: 18px; color: var(--interactive-accent); font-weight: bold; opacity: 0; transition: opacity 0.2s; } @media (max-width: 640px) { .platform-selector-modal { max-width: 92vw; } .platform-options { grid-template-columns: 1fr; } } .platform-option.selected .check-icon { opacity: 1; } `; container.appendChild(style); } onClose() { const { contentEl } = this; contentEl.empty(); } } class ThemeJsonModal extends Modal { constructor(app, options) { super(app); this.options = options; } onOpen() { const { contentEl } = this; contentEl.empty(); contentEl.addClass('bulk-copy-theme-json-modal'); contentEl.createEl('h2', { text: this.options.title || '主题 JSON' }); if (this.options.description) { contentEl.createEl('p', { text: this.options.description, cls: 'setting-item-description' }); } const textarea = contentEl.createEl('textarea'); textarea.style.width = '100%'; textarea.style.minHeight = '320px'; textarea.style.marginTop = '12px'; textarea.style.fontFamily = 'monospace'; textarea.style.fontSize = '13px'; textarea.style.lineHeight = '1.5'; textarea.value = this.options.initialValue || ''; if (this.options.readOnly) { textarea.readOnly = true; } const actions = contentEl.createDiv(); actions.style.display = 'flex'; actions.style.gap = '10px'; actions.style.marginTop = '14px'; if (this.options.readOnly) { const copyButton = actions.createEl('button', { text: '复制到剪贴板' }); copyButton.addEventListener('click', async () => { await navigator.clipboard.writeText(textarea.value); new Notice('✅ JSON 已复制到剪贴板'); }); } else { const importButton = actions.createEl('button', { text: '确认导入' }); importButton.addEventListener('click', async () => { await this.options.onConfirm(textarea.value); this.close(); }); } const closeButton = actions.createEl('button', { text: '关闭' }); closeButton.addEventListener('click', () => this.close()); } onClose() { this.contentEl.empty(); } } class ThemeManagerModal extends Modal { constructor(app, plugin, onChange) { super(app); this.plugin = plugin; this.onChange = onChange; this.activeTab = 'basic'; } onOpen() { const { contentEl } = this; this.modalEl.style.width = 'min(1280px, 92vw)'; this.modalEl.style.maxWidth = '92vw'; this.modalEl.style.height = 'min(860px, 88vh)'; this.modalEl.style.maxHeight = '88vh'; this.modalEl.style.resize = 'both'; this.modalEl.style.overflow = 'hidden'; if (this.contentEl && this.contentEl.parentElement) { this.contentEl.parentElement.style.height = '100%'; } contentEl.empty(); contentEl.addClass('bulk-copy-theme-manager-modal'); const style = document.createElement('style'); style.innerHTML = ` .bulk-copy-theme-manager-modal { height: 100%; overflow: auto; } .bulk-copy-theme-manager-layout { display: grid; grid-template-columns: 280px 1fr; gap: 18px; margin-top: 16px; min-height: calc(100% - 72px); } .bulk-copy-theme-manager-layout.collapsed { grid-template-columns: 1fr; } .bulk-copy-theme-list { border: 1px solid var(--background-modifier-border); border-radius: 14px; background: var(--background-secondary); padding: 10px; overflow: auto; } .bulk-copy-theme-sidebar-wrap { position: relative; min-width: 180px; } .bulk-copy-theme-resizer { position: absolute; top: 0; right: -9px; width: 10px; height: 100%; cursor: col-resize; z-index: 2; } .bulk-copy-theme-list-item { padding: 10px 12px; border-radius: 10px; cursor: pointer; margin-bottom: 8px; border: 1px solid transparent; } .bulk-copy-theme-list-item:hover { background: var(--background-primary); } .bulk-copy-theme-list-item.active { background: rgba(var(--interactive-accent-rgb), 0.12); border-color: var(--interactive-accent); } .bulk-copy-theme-list-name { font-weight: 600; } .bulk-copy-theme-list-meta { font-size: 12px; color: var(--text-muted); margin-top: 4px; } .bulk-copy-theme-badge { display: inline-block; margin-top: 6px; padding: 2px 8px; border-radius: 999px; font-size: 11px; color: var(--interactive-accent); background: rgba(var(--interactive-accent-rgb), 0.1); cursor: pointer; } .bulk-copy-theme-swatches { display: flex; gap: 6px; margin-top: 8px; } .bulk-copy-theme-swatch { width: 14px; height: 14px; border-radius: 999px; border: 1px solid rgba(0, 0, 0, 0.08); } .bulk-copy-theme-editor-panel { border: 1px solid var(--background-modifier-border); border-radius: 14px; background: var(--background-primary); padding: 14px 16px; overflow: auto; } .bulk-copy-theme-tabs { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; } .bulk-copy-theme-tab { padding: 8px 12px; border-radius: 10px; border: 1px solid var(--background-modifier-border); background: var(--background-secondary); cursor: pointer; } .bulk-copy-theme-tab.active { border-color: var(--interactive-accent); color: var(--interactive-accent); background: rgba(var(--interactive-accent-rgb), 0.08); } .bulk-copy-preview-compare-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px; margin-top: 12px; } .bulk-copy-preview-compare-card { padding: 12px; border: 1px solid var(--background-modifier-border); border-radius: 12px; background: var(--background-primary); } .bulk-copy-preview-compare-card h4 { margin: 0 0 8px; } .bulk-copy-preview-compare-note { margin: 0 0 10px; color: var(--text-muted); font-size: 12px; } `; contentEl.appendChild(style); contentEl.createEl('h2', { text: '主题模板管理器' }); contentEl.createEl('p', { text: '集中管理自定义主题模板:搜索、切换、复制、排序、导入导出与编辑。', cls: 'setting-item-description' }); const toolbar = contentEl.createDiv(); toolbar.style.display = 'flex'; toolbar.style.justifyContent = 'flex-end'; toolbar.style.marginTop = '8px'; const toggleButton = toolbar.createEl('button', { text: USER_CONFIG.themeManagerListCollapsed ? '展开左侧列表' : '折叠左侧列表' }); toggleButton.addEventListener('click', async () => { USER_CONFIG.themeManagerListCollapsed = !USER_CONFIG.themeManagerListCollapsed; await this.plugin.saveUserConfig(); this.onOpen(); }); const layout = contentEl.createDiv({ cls: `bulk-copy-theme-manager-layout${USER_CONFIG.themeManagerListCollapsed ? ' collapsed' : ''}` }); layout.style.gridTemplateColumns = USER_CONFIG.themeManagerListCollapsed ? '1fr' : `${USER_CONFIG.themeManagerSidebarWidth || 280}px 1fr`; const sidebarWrap = layout.createDiv({ cls: 'bulk-copy-theme-sidebar-wrap' }); sidebarWrap.style.width = `${USER_CONFIG.themeManagerSidebarWidth || 280}px`; const listPane = sidebarWrap.createDiv({ cls: 'bulk-copy-theme-list' }); const editorPane = layout.createDiv({ cls: 'bulk-copy-theme-editor-panel' }); if (USER_CONFIG.themeManagerListCollapsed) { sidebarWrap.style.display = 'none'; } else { const resizer = sidebarWrap.createDiv({ cls: 'bulk-copy-theme-resizer' }); resizer.addEventListener('mousedown', (event) => { event.preventDefault(); const startX = event.clientX; const startWidth = USER_CONFIG.themeManagerSidebarWidth || 280; const onMouseMove = (moveEvent) => { const nextWidth = Math.max(200, Math.min(420, startWidth + (moveEvent.clientX - startX))); USER_CONFIG.themeManagerSidebarWidth = nextWidth; layout.style.gridTemplateColumns = `${nextWidth}px 1fr`; sidebarWrap.style.width = `${nextWidth}px`; }; const onMouseUp = async () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); await this.plugin.saveUserConfig(); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); } const customThemes = USER_CONFIG.customThemes || DEFAULT_USER_CONFIG.customThemes; new Setting(listPane) .setName('仅看使用中') .addToggle(toggle => { toggle.setValue(!!USER_CONFIG.themeListShowUsedOnly); toggle.onChange(async value => { USER_CONFIG.themeListShowUsedOnly = value; await this.plugin.saveUserConfig(); this.onOpen(); }); }); new Setting(listPane) .setName('使用中优先') .addToggle(toggle => { toggle.setValue(!!USER_CONFIG.themeListUsedFirst); toggle.onChange(async value => { USER_CONFIG.themeListUsedFirst = value; await this.plugin.saveUserConfig(); this.onOpen(); }); }); new Setting(listPane) .setName('仅看当前平台推荐') .setDesc(`按 ${PLATFORM_PRESETS[USER_CONFIG.themePreviewPlatform || 'wechat']?.name || '当前平台'} 的推荐主题过滤`) .addToggle(toggle => { toggle.setValue(!!USER_CONFIG.themeListShowRecommendedOnly); toggle.onChange(async value => { USER_CONFIG.themeListShowRecommendedOnly = value; await this.plugin.saveUserConfig(); this.onOpen(); }); }); let themeKeys = getOrderedCustomThemeKeys(); if (USER_CONFIG.themeListShowUsedOnly) { themeKeys = themeKeys.filter(themeKey => Object.keys(PLATFORM_PRESETS).some(platformKey => USER_CONFIG.platformThemes?.[platformKey] === themeKey)); } if (USER_CONFIG.themeListShowRecommendedOnly) { const recommendedKeys = new Set(getRecommendedThemeKeys(USER_CONFIG.themePreviewPlatform || 'wechat')); themeKeys = themeKeys.filter(themeKey => recommendedKeys.has(themeKey)); } if (USER_CONFIG.themeListUsedFirst) { themeKeys = [...themeKeys].sort((a, b) => { const aUsed = Object.keys(PLATFORM_PRESETS).some(platformKey => USER_CONFIG.platformThemes?.[platformKey] === a); const bUsed = Object.keys(PLATFORM_PRESETS).some(platformKey => USER_CONFIG.platformThemes?.[platformKey] === b); return Number(bUsed) - Number(aUsed); }); } themeKeys.forEach(themeKey => { const theme = customThemes[themeKey] || DEFAULT_USER_CONFIG.customThemes.custom; const usedBy = Object.keys(PLATFORM_PRESETS).filter(platformKey => USER_CONFIG.platformThemes?.[platformKey] === themeKey); const recommendedBy = usedBy.filter(platformKey => getRecommendedThemeKeys(platformKey)[0] === themeKey); const item = listPane.createDiv({ cls: `bulk-copy-theme-list-item${themeKey === getEditableCustomThemeKey() ? ' active' : ''}` }); item.createDiv({ text: customThemes[themeKey]?.name || themeKey, cls: 'bulk-copy-theme-list-name' }); item.createDiv({ text: themeKey, cls: 'bulk-copy-theme-list-meta' }); item.createDiv({ text: usedBy.length > 0 ? `使用平台: ${usedBy.map(platformKey => PLATFORM_PRESETS[platformKey].name).join(' / ')}` : '使用平台: 未绑定', cls: 'bulk-copy-theme-list-meta' }); if (recommendedBy.length > 0) { const badge = item.createDiv({ text: `推荐中: ${recommendedBy.map(platformKey => PLATFORM_PRESETS[platformKey].name).join(' / ')}`, cls: 'bulk-copy-theme-badge' }); badge.title = '点击切换到对应平台预览'; badge.addEventListener('click', async (event) => { event.stopPropagation(); const firstPlatform = recommendedBy[0]; USER_CONFIG.themePreviewPlatform = firstPlatform; await this.plugin.saveUserConfig(); new Notice(`已切换到 ${PLATFORM_PRESETS[firstPlatform]?.name || firstPlatform} 预览,可继续查看该平台配置效果`); this.onOpen(); }); } const swatches = item.createDiv({ cls: 'bulk-copy-theme-swatches' }); [theme.accentColor, theme.backgroundColor, theme.codeBackground].forEach(color => { const swatch = swatches.createDiv({ cls: 'bulk-copy-theme-swatch' }); swatch.style.backgroundColor = color || '#cccccc'; swatch.title = color || '#cccccc'; }); item.addEventListener('click', async () => { USER_CONFIG.customThemeEditorKey = themeKey; await this.plugin.saveUserConfig(); this.onOpen(); }); }); const tabs = editorPane.createDiv({ cls: 'bulk-copy-theme-tabs' }); [ ['basic', '基础'], ['color', '颜色'], ['details', '细节'], ['preview', '预览'], ['manage', '导入导出'] ].forEach(([key, label]) => { const tab = tabs.createDiv({ cls: `bulk-copy-theme-tab${this.activeTab === key ? ' active' : ''}`, text: label }); tab.addEventListener('click', () => { this.activeTab = key; this.onOpen(); }); }); const visibleTabs = this.activeTab === 'basic' ? ['basic'] : this.activeTab === 'color' ? ['color'] : this.activeTab === 'details' ? ['details'] : this.activeTab === 'preview' ? ['preview'] : ['manage']; this.plugin.settingTab.renderCustomThemeEditor(editorPane, { visibleTabs }); } onClose() { if (typeof this.onChange === 'function') { this.onChange(); } this.contentEl.empty(); } } class BulkCopySettingTab extends PluginSettingTab { constructor(app, plugin) { super(app, plugin); this.plugin = plugin; } createSection(containerEl, title, description) { const section = containerEl.createDiv({ cls: 'bulk-copy-settings-section' }); section.createEl('h3', { text: title, cls: 'bulk-copy-settings-section-title' }); if (description) { section.createEl('p', { text: description, cls: 'bulk-copy-settings-section-desc' }); } return section; } renderSettingsStyles(containerEl) { const style = document.createElement('style'); style.innerHTML = ` .bulk-copy-settings-root { padding-bottom: 28px; } .bulk-copy-settings-hero { padding: 18px 20px; margin-bottom: 18px; border: 1px solid var(--background-modifier-border); border-radius: 14px; background: linear-gradient(180deg, var(--background-primary), var(--background-secondary)); } .bulk-copy-settings-hero h2 { margin: 0 0 6px; } .bulk-copy-settings-hero p { margin: 0; color: var(--text-muted); line-height: 1.6; } .bulk-copy-settings-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin: 16px 0 0; } .bulk-copy-settings-stat { padding: 12px 14px; border-radius: 12px; background: var(--background-primary-alt, var(--background-primary)); border: 1px solid var(--background-modifier-border); } .bulk-copy-settings-stat-label { font-size: 12px; color: var(--text-muted); margin-bottom: 4px; } .bulk-copy-settings-stat-value { font-size: 14px; font-weight: 600; color: var(--text-normal); } .bulk-copy-settings-section { margin: 20px 0; padding: 16px 18px 6px; border: 1px solid var(--background-modifier-border); border-radius: 14px; background: var(--background-primary); } .bulk-copy-settings-section-title { margin: 0 0 6px; font-size: 16px; } .bulk-copy-settings-section-desc { margin: 0 0 14px; color: var(--text-muted); line-height: 1.6; } .bulk-copy-platform-section { margin: 12px 0 16px; border: 1px solid var(--background-modifier-border); border-radius: 12px; overflow: hidden; background: var(--background-primary-alt, var(--background-primary)); } .bulk-copy-platform-section summary { cursor: pointer; padding: 14px 16px; font-weight: 600; list-style: none; background: var(--background-secondary); } .bulk-copy-platform-section summary::-webkit-details-marker { display: none; } .bulk-copy-platform-section[open] summary { border-bottom: 1px solid var(--background-modifier-border); } .bulk-copy-platform-section .setting-item, .bulk-copy-settings-section .setting-item { padding-top: 12px; padding-bottom: 12px; } .bulk-copy-platform-section h3, .bulk-copy-settings-section h3 { margin-top: 14px; } .bulk-copy-subtle-note { margin: 2px 0 10px; color: var(--text-muted); font-size: 13px; line-height: 1.5; } .bulk-copy-theme-manager-box { margin-top: 10px; padding: 14px 16px; border: 1px dashed var(--background-modifier-border); border-radius: 12px; background: var(--background-secondary); } .bulk-copy-theme-manager-box details summary { cursor: pointer; font-weight: 600; list-style: none; } .bulk-copy-theme-manager-box details summary::-webkit-details-marker { display: none; } `; containerEl.appendChild(style); } renderThemeDetailSettings(containerEl, platformKey, title, description) { const theme = getPlatformThemeConfig(platformKey); containerEl.createEl('h3', { text: title }); if (description) { containerEl.createEl('p', { text: description, cls: 'setting-item-description' }); } new Setting(containerEl) .setName('标题分隔线') .setDesc('控制 H1/H2 是否显示下边线') .addToggle(toggle => { toggle.setValue(!!theme.headingDivider); toggle.onChange(async value => { updatePlatformThemeOverride(platformKey, 'headingDivider', value); await this.plugin.saveUserConfig(); }); }); new Setting(containerEl) .setName('表头底色') .setDesc('控制表格表头是否使用底色强调') .addToggle(toggle => { toggle.setValue(!!theme.tableHeaderFill); toggle.onChange(async value => { updatePlatformThemeOverride(platformKey, 'tableHeaderFill', value); await this.plugin.saveUserConfig(); }); }); new Setting(containerEl) .setName('图片圆角') .setDesc('控制导出图片是否保留圆角') .addToggle(toggle => { toggle.setValue(!!theme.imageRounded); toggle.onChange(async value => { updatePlatformThemeOverride(platformKey, 'imageRounded', value); await this.plugin.saveUserConfig(); }); }); new Setting(containerEl) .setName('代码块边框') .setDesc('控制代码块是否显示边框') .addToggle(toggle => { toggle.setValue(!!theme.codeBlockBorder); toggle.onChange(async value => { updatePlatformThemeOverride(platformKey, 'codeBlockBorder', value); await this.plugin.saveUserConfig(); }); }); } renderThemePreview(containerEl, platformKey) { const theme = getPlatformThemeConfig(platformKey); const preset = PLATFORM_PRESETS[platformKey]; const frame = preset.previewFrame || {}; const outer = containerEl.createDiv({ cls: 'bulk-copy-theme-preview-frame' }); outer.style.background = frame.background || '#f5f5f5'; outer.style.padding = frame.padding || '20px'; outer.style.margin = '12px 0 20px'; outer.style.borderRadius = '14px'; const preview = outer.createDiv({ cls: 'bulk-copy-theme-preview' }); preview.style.padding = '16px 18px'; preview.style.maxWidth = typeof preset.displayMaxWidth === 'number' ? `${preset.displayMaxWidth}px` : String(preset.displayMaxWidth || '800px'); preview.style.margin = '0 auto'; preview.style.border = frame.contentBorder || `1px solid ${theme.borderColor}`; preview.style.boxShadow = frame.contentShadow || 'none'; preview.style.borderRadius = theme.borderRadius; preview.style.backgroundColor = frame.contentBackground || theme.backgroundColor; preview.style.color = theme.textColor; preview.style.fontFamily = theme.fontFamily; preview.innerHTML = `

    示例标题

    这是一段用于预览导出效果的正文内容,展示当前平台主题的字体、颜色、间距和整体观感,并包含 行内代码链接样式

    二级内容分组

    下面的区块用于观察更复杂的富文本结构在当前主题下的表现。

    这是一段引用示例,用于观察边框、背景和文本色。
    1. 有序列表第一项
    2. 有序列表第二项
    const message = 'theme preview';
    字段说明
    主题${theme.name}
    平台${PLATFORM_PRESETS[platformKey].name}
    preview

    图片说明文字:用于观察图片上下间距、圆角和说明段落是否协调。

    `; applyThemeToRoot(preview, theme); } addThemeOptions(dropdown, platformKey) { const allThemes = getAllThemeOptions(); const builtInKeys = Object.keys(PLATFORM_THEMES); const customKeys = getOrderedCustomThemeKeys(); [...builtInKeys, ...customKeys].forEach(themeKey => { const label = isRecommendedTheme(platformKey, themeKey) ? `${allThemes[themeKey].name} (推荐)` : allThemes[themeKey].name; dropdown.addOption(themeKey, label); }); } renderCustomThemeEditor(containerEl, options = {}) { const editorKey = getEditableCustomThemeKey(); const customThemes = USER_CONFIG.customThemes || DEFAULT_USER_CONFIG.customThemes; const customTheme = getEditableCustomTheme(); const searchQuery = String(USER_CONFIG.customThemeSearchQuery || '').trim().toLowerCase(); const filteredThemeKeys = getOrderedCustomThemeKeys().filter(themeKey => { const name = String(customThemes[themeKey]?.name || themeKey).toLowerCase(); return !searchQuery || name.includes(searchQuery) || themeKey.toLowerCase().includes(searchQuery); }); const visibleTabs = options.visibleTabs || ['basic', 'color', 'manage']; const headerBox = containerEl.createDiv({ cls: 'bulk-copy-settings-section' }); headerBox.createEl('h3', { text: customTheme.name || '我的主题', cls: 'bulk-copy-settings-section-title' }); headerBox.createEl('p', { text: `主题 Key: ${editorKey}`, cls: 'bulk-copy-subtle-note' }); headerBox.createEl('p', { text: '可以维护多个自定义主题模板,并分配给任意平台。建议先调整字体、标题颜色和背景色,再微调代码背景与圆角。', cls: 'setting-item-description' }); if (visibleTabs.includes('preview')) { const previewSection = containerEl.createDiv({ cls: 'bulk-copy-settings-section' }); previewSection.createEl('h3', { text: '主题预览', cls: 'bulk-copy-settings-section-title' }); previewSection.createEl('p', { text: '当前主题可在不同平台容器下进行预览,不会影响全局默认平台。', cls: 'bulk-copy-subtle-note' }); new Setting(previewSection) .setName('预览平台容器') .setDesc('仅用于主题管理器中的预览切换') .addDropdown(dropdown => { Object.keys(PLATFORM_PRESETS).forEach(platformKey => { dropdown.addOption(platformKey, PLATFORM_PRESETS[platformKey].name); }); dropdown.setValue(USER_CONFIG.themePreviewPlatform || 'wechat'); dropdown.onChange(async value => { USER_CONFIG.themePreviewPlatform = value; await this.plugin.saveUserConfig(); this.display(); }); }); const previewTheme = getPlatformThemeConfig(USER_CONFIG.themePreviewPlatform || 'wechat'); const statusGrid = previewSection.createDiv({ cls: 'bulk-copy-settings-grid' }); [ ['标题分隔线', previewTheme.headingDivider ? '开启' : '关闭'], ['表头底色', previewTheme.tableHeaderFill ? '开启' : '关闭'], ['图片圆角', previewTheme.imageRounded ? '开启' : '关闭'], ['代码块边框', previewTheme.codeBlockBorder ? '开启' : '关闭'] ].forEach(([label, value]) => { const stat = statusGrid.createDiv({ cls: 'bulk-copy-settings-stat' }); stat.createDiv({ text: label, cls: 'bulk-copy-settings-stat-label' }); stat.createDiv({ text: value, cls: 'bulk-copy-settings-stat-value' }); }); const compareGrid = previewSection.createDiv({ cls: 'bulk-copy-preview-compare-grid' }); const currentCard = compareGrid.createDiv({ cls: 'bulk-copy-preview-compare-card' }); currentCard.createEl('h4', { text: '当前主题' }); currentCard.createEl('p', { text: customTheme.name || '我的主题', cls: 'bulk-copy-preview-compare-note' }); this.renderThemePreview(currentCard, USER_CONFIG.themePreviewPlatform || 'wechat'); const recommendedKey = getRecommendedThemeKeys(USER_CONFIG.themePreviewPlatform || 'wechat')[0]; if (recommendedKey) { const recommendedTheme = getAllThemeOptions()[recommendedKey]; const diffs = getThemeDetailDiffs(customTheme, recommendedTheme); const colorDiffs = getThemeColorDiffs(customTheme, recommendedTheme); if (diffs.length > 0) { const diffSection = previewSection.createDiv({ cls: 'bulk-copy-settings-section' }); diffSection.createEl('h3', { text: '差异提示', cls: 'bulk-copy-settings-section-title' }); diffs.forEach(text => diffSection.createEl('p', { text, cls: 'bulk-copy-subtle-note' })); } if (colorDiffs.length > 0) { const colorDiffSection = previewSection.createDiv({ cls: 'bulk-copy-settings-section' }); colorDiffSection.createEl('h3', { text: '颜色差异', cls: 'bulk-copy-settings-section-title' }); colorDiffs.forEach(text => colorDiffSection.createEl('p', { text, cls: 'bulk-copy-subtle-note' })); } const compareSection = compareGrid.createDiv({ cls: 'bulk-copy-preview-compare-card' }); compareSection.createEl('h4', { text: '推荐主题' }); compareSection.createEl('p', { text: recommendedTheme?.name || recommendedKey, cls: 'bulk-copy-preview-compare-note' }); const actionRow = compareSection.createDiv(); actionRow.style.marginBottom = '10px'; const applyButton = actionRow.createEl('button', { text: '应用到当前预览平台' }); applyButton.addEventListener('click', async () => { const previewPlatform = USER_CONFIG.themePreviewPlatform || 'wechat'; USER_CONFIG.lastAppliedRecommendedTheme = { platformKey: previewPlatform, previousThemeKey: USER_CONFIG.platformThemes?.[previewPlatform], previousOverrides: USER_CONFIG.platformThemeOverrides?.[previewPlatform] || {} }; USER_CONFIG.platformThemes = { ...DEFAULT_USER_CONFIG.platformThemes, ...(USER_CONFIG.platformThemes || {}), [previewPlatform]: recommendedKey }; USER_CONFIG.platformThemeOverrides = { ...DEFAULT_USER_CONFIG.platformThemeOverrides, ...(USER_CONFIG.platformThemeOverrides || {}), [previewPlatform]: {} }; await this.plugin.saveUserConfig(); new Notice(`✅ 已将推荐主题应用到 ${PLATFORM_PRESETS[previewPlatform]?.name || previewPlatform}`); this.display(); }); if (USER_CONFIG.lastAppliedRecommendedTheme && USER_CONFIG.lastAppliedRecommendedTheme.platformKey === (USER_CONFIG.themePreviewPlatform || 'wechat')) { const undoButton = actionRow.createEl('button', { text: '撤销刚才应用' }); undoButton.style.marginLeft = '8px'; undoButton.addEventListener('click', async () => { const snapshot = USER_CONFIG.lastAppliedRecommendedTheme; USER_CONFIG.platformThemes = { ...DEFAULT_USER_CONFIG.platformThemes, ...(USER_CONFIG.platformThemes || {}), [snapshot.platformKey]: snapshot.previousThemeKey || DEFAULT_USER_CONFIG.platformThemes[snapshot.platformKey] }; USER_CONFIG.platformThemeOverrides = { ...DEFAULT_USER_CONFIG.platformThemeOverrides, ...(USER_CONFIG.platformThemeOverrides || {}), [snapshot.platformKey]: snapshot.previousOverrides || {} }; USER_CONFIG.lastAppliedRecommendedTheme = null; await this.plugin.saveUserConfig(); new Notice(`✅ 已撤销 ${PLATFORM_PRESETS[snapshot.platformKey]?.name || snapshot.platformKey} 的推荐主题应用`); this.display(); }); } const previewThemeKeyBackup = USER_CONFIG.platformThemes?.[USER_CONFIG.themePreviewPlatform || 'wechat']; USER_CONFIG.platformThemes = { ...USER_CONFIG.platformThemes, [USER_CONFIG.themePreviewPlatform || 'wechat']: recommendedKey }; this.renderThemePreview(compareSection, USER_CONFIG.themePreviewPlatform || 'wechat'); USER_CONFIG.platformThemes = { ...USER_CONFIG.platformThemes, [USER_CONFIG.themePreviewPlatform || 'wechat']: previewThemeKeyBackup }; } return; } if (visibleTabs.includes('details')) { const detailSection = containerEl.createDiv({ cls: 'bulk-copy-settings-section' }); detailSection.createEl('h3', { text: '主题细节', cls: 'bulk-copy-settings-section-title' }); detailSection.createEl('p', { text: '这些开关属于当前自定义主题本身,会影响不同平台绑定该主题后的基础表现。', cls: 'bulk-copy-subtle-note' }); new Setting(detailSection) .setName('标题分隔线') .setDesc('控制 H1/H2 是否显示下边线') .addToggle(toggle => { toggle.setValue(!!customTheme.headingDivider); toggle.onChange(async value => { await updateCustomThemeField('headingDivider', value); this.display(); }); }); new Setting(detailSection) .setName('表头底色') .setDesc('控制表格表头是否显示底色') .addToggle(toggle => { toggle.setValue(!!customTheme.tableHeaderFill); toggle.onChange(async value => { await updateCustomThemeField('tableHeaderFill', value); this.display(); }); }); new Setting(detailSection) .setName('图片圆角') .setDesc('控制图片是否保留圆角') .addToggle(toggle => { toggle.setValue(!!customTheme.imageRounded); toggle.onChange(async value => { await updateCustomThemeField('imageRounded', value); this.display(); }); }); new Setting(detailSection) .setName('代码块边框') .setDesc('控制代码块是否显示边框') .addToggle(toggle => { toggle.setValue(!!customTheme.codeBlockBorder); toggle.onChange(async value => { await updateCustomThemeField('codeBlockBorder', value); this.display(); }); }); return; } const searchSection = containerEl.createDiv({ cls: 'bulk-copy-settings-section' }); searchSection.createEl('h3', { text: '搜索与切换', cls: 'bulk-copy-settings-section-title' }); if (visibleTabs.includes('manage')) { new Setting(searchSection) .setName('主题搜索') .setDesc('按名称快速筛选自定义主题模板') .addText(text => { text.setPlaceholder('输入主题名称'); text.setValue(String(USER_CONFIG.customThemeSearchQuery || '')); text.onChange(async value => { USER_CONFIG.customThemeSearchQuery = value; await this.plugin.saveUserConfig(); this.display(); }); }); } const updateCustomThemeField = async (key, value) => { USER_CONFIG.customThemes = { ...DEFAULT_USER_CONFIG.customThemes, ...(USER_CONFIG.customThemes || {}), [editorKey]: { ...DEFAULT_USER_CONFIG.customThemes.custom, ...(USER_CONFIG.customThemes?.[editorKey] || {}), [key]: value } }; await this.plugin.saveUserConfig(); }; if (visibleTabs.includes('manage')) { new Setting(searchSection) .setName('当前编辑主题') .setDesc('选择一个自定义主题进行编辑') .addDropdown(dropdown => { filteredThemeKeys.forEach(themeKey => { dropdown.addOption(themeKey, customThemes[themeKey].name || themeKey); }); dropdown.setValue(filteredThemeKeys.includes(editorKey) ? editorKey : (filteredThemeKeys[0] || editorKey)); dropdown.onChange(async value => { USER_CONFIG.customThemeEditorKey = value; await this.plugin.saveUserConfig(); this.display(); }); }); } const manageSection = containerEl.createDiv({ cls: 'bulk-copy-settings-section' }); manageSection.createEl('h3', { text: '模板管理', cls: 'bulk-copy-settings-section-title' }); if (visibleTabs.includes('manage')) { new Setting(manageSection) .setName('主题模板管理') .setDesc('新增、复制、排序、导出或导入自定义主题模板') .addButton(button => { button.setButtonText('新增主题'); button.onClick(async () => { const newKey = `custom-${Date.now()}`; USER_CONFIG.customThemes = { ...DEFAULT_USER_CONFIG.customThemes, ...(USER_CONFIG.customThemes || {}), [newKey]: { ...DEFAULT_USER_CONFIG.customThemes.custom, name: `我的主题 ${Object.keys(USER_CONFIG.customThemes || {}).length + 1}` } }; USER_CONFIG.customThemeOrder = [...getOrderedCustomThemeKeys(), newKey]; USER_CONFIG.customThemeEditorKey = newKey; await this.plugin.saveUserConfig(); this.display(); }); }) .addButton(button => { button.setButtonText('复制当前主题'); button.onClick(async () => { const newKey = `custom-${Date.now()}`; USER_CONFIG.customThemes = { ...DEFAULT_USER_CONFIG.customThemes, ...(USER_CONFIG.customThemes || {}), [newKey]: { ...customTheme, name: `${customTheme.name || '我的主题'} 副本` } }; USER_CONFIG.customThemeOrder = [...getOrderedCustomThemeKeys(), newKey]; USER_CONFIG.customThemeEditorKey = newKey; await this.plugin.saveUserConfig(); this.display(); }); }) .addButton(button => { button.setButtonText('上移'); button.onClick(async () => { const order = [...getOrderedCustomThemeKeys()]; const index = order.indexOf(editorKey); if (index > 0) { [order[index - 1], order[index]] = [order[index], order[index - 1]]; USER_CONFIG.customThemeOrder = order; await this.plugin.saveUserConfig(); this.display(); } }); }) .addButton(button => { button.setButtonText('下移'); button.onClick(async () => { const order = [...getOrderedCustomThemeKeys()]; const index = order.indexOf(editorKey); if (index >= 0 && index < order.length - 1) { [order[index + 1], order[index]] = [order[index], order[index + 1]]; USER_CONFIG.customThemeOrder = order; await this.plugin.saveUserConfig(); this.display(); } }); }) .addButton(button => { button.setButtonText('导出主题'); button.onClick(() => { const exportPayload = JSON.stringify({ key: editorKey, theme: customTheme }, null, 2); new ThemeJsonModal(this.app, { title: '导出当前主题', description: '复制下面的 JSON,可在其他环境中导入该主题。', initialValue: exportPayload, readOnly: true }).open(); }); }) .addButton(button => { button.setButtonText('导出全部'); button.onClick(() => { const exportPayload = JSON.stringify({ customThemes: USER_CONFIG.customThemes || DEFAULT_USER_CONFIG.customThemes, customThemeOrder: getOrderedCustomThemeKeys(), customThemeEditorKey: getEditableCustomThemeKey() }, null, 2); new ThemeJsonModal(this.app, { title: '导出全部自定义主题', description: '复制下面的 JSON,可完整备份当前的自定义主题库。', initialValue: exportPayload, readOnly: true }).open(); }); }) .addButton(button => { button.setButtonText('导入主题'); button.onClick(() => { new ThemeJsonModal(this.app, { title: '导入当前主题', description: '粘贴单个主题 JSON 后确认导入。', initialValue: '', readOnly: false, onConfirm: async (raw) => { try { const parsed = JSON.parse(raw); const validationError = validateSingleThemeImport(parsed); if (validationError) { new Notice(`❌ 主题导入失败:${validationError}`); return; } const importedKey = parsed.key || `custom-${Date.now()}`; const importedTheme = { ...DEFAULT_USER_CONFIG.customThemes.custom, ...(parsed.theme || {}) }; USER_CONFIG.customThemes = { ...DEFAULT_USER_CONFIG.customThemes, ...(USER_CONFIG.customThemes || {}), [importedKey]: importedTheme }; USER_CONFIG.customThemeOrder = [...getOrderedCustomThemeKeys().filter(key => key !== importedKey), importedKey]; USER_CONFIG.customThemeEditorKey = importedKey; await this.plugin.saveUserConfig(); this.display(); new Notice('✅ 主题导入成功'); } catch (error) { new Notice('❌ 主题导入失败:JSON 格式无效'); } } }).open(); }); }) .addButton(button => { button.setButtonText('导入全部'); button.onClick(() => { new ThemeJsonModal(this.app, { title: '导入全部自定义主题', description: '粘贴完整主题集合 JSON 后确认导入。', initialValue: '', readOnly: false, onConfirm: async (raw) => { try { const parsed = JSON.parse(raw); const validationError = validateThemeCollectionImport(parsed); if (validationError) { new Notice(`❌ 全部主题导入失败:${validationError}`); return; } const importedThemes = parsed.customThemes || {}; const importedOrder = Array.isArray(parsed.customThemeOrder) ? parsed.customThemeOrder : Object.keys(importedThemes); const importedEditorKey = parsed.customThemeEditorKey || importedOrder[0] || 'custom'; USER_CONFIG.customThemes = Object.keys(importedThemes).length > 0 ? importedThemes : { ...DEFAULT_USER_CONFIG.customThemes }; USER_CONFIG.customThemeOrder = importedOrder.length > 0 ? importedOrder : [...DEFAULT_USER_CONFIG.customThemeOrder]; USER_CONFIG.customThemeEditorKey = importedEditorKey; await this.plugin.saveUserConfig(); this.display(); new Notice('✅ 全部自定义主题导入成功'); } catch (error) { new Notice('❌ 全部主题导入失败:JSON 格式无效'); } } }).open(); }); }) .addButton(button => { button.setButtonText('删除当前主题'); button.setWarning(); button.setDisabled(editorKey === 'custom' && Object.keys(customThemes).length <= 1); button.onClick(async () => { const themeEntries = { ...(USER_CONFIG.customThemes || {}) }; delete themeEntries[editorKey]; const boundPlatforms = Object.keys(USER_CONFIG.platformThemes || {}).filter(platformKey => USER_CONFIG.platformThemes[platformKey] === editorKey); if (boundPlatforms.length > 0) { const confirmed = window.confirm(`当前主题正被以下平台使用:${boundPlatforms.map(platformKey => PLATFORM_PRESETS[platformKey]?.name || platformKey).join(' / ')}。删除后这些平台将恢复到默认推荐主题,是否继续?`); if (!confirmed) { return; } } Object.keys(USER_CONFIG.platformThemes || {}).forEach(platformKey => { if (USER_CONFIG.platformThemes[platformKey] === editorKey) { USER_CONFIG.platformThemes[platformKey] = DEFAULT_USER_CONFIG.platformThemes[platformKey] || 'official'; } }); USER_CONFIG.customThemes = Object.keys(themeEntries).length > 0 ? themeEntries : { ...DEFAULT_USER_CONFIG.customThemes }; USER_CONFIG.customThemeOrder = getOrderedCustomThemeKeys().filter(key => key !== editorKey); if (USER_CONFIG.customThemeOrder.length === 0) { USER_CONFIG.customThemeOrder = [...DEFAULT_USER_CONFIG.customThemeOrder]; } USER_CONFIG.customThemeEditorKey = Object.keys(USER_CONFIG.customThemes)[0]; await this.plugin.saveUserConfig(); this.display(); }); }); } const isHexColor = (value) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(String(value || '').trim()); const fontPresets = { system: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif", pingfang: "'PingFang SC', 'Microsoft YaHei', sans-serif", serif: "'Noto Serif SC', Georgia, serif", inter: "Inter, 'PingFang SC', 'Segoe UI', sans-serif" }; const basicSection = containerEl.createDiv({ cls: 'bulk-copy-settings-section' }); basicSection.createEl('h3', { text: '基础样式', cls: 'bulk-copy-settings-section-title' }); if (visibleTabs.includes('basic')) { new Setting(basicSection) .setName('主题名称') .setDesc('显示在平台主题下拉中的名称') .addText(text => { text.setValue(String(customTheme.name || '我的主题')); text.onChange(async value => { await updateCustomThemeField('name', value || '我的主题'); }); }); new Setting(basicSection) .setName('字体预设') .setDesc('选择常用字体组合,可继续在下方自定义字体文本') .addDropdown(dropdown => { dropdown.addOption('system', '系统无衬线'); dropdown.addOption('pingfang', '苹方/雅黑'); dropdown.addOption('serif', '中文衬线'); dropdown.addOption('inter', 'Inter 文档风'); const matchedKey = Object.keys(fontPresets).find(key => fontPresets[key] === customTheme.fontFamily) || 'pingfang'; dropdown.setValue(matchedKey); dropdown.onChange(async value => { await updateCustomThemeField('fontFamily', fontPresets[value] || fontPresets.pingfang); this.display(); }); }); new Setting(basicSection) .setName('字体族') .setDesc('支持直接输入完整 font-family 字符串') .addText(text => { text.setValue(String(customTheme.fontFamily || '')); text.onChange(async value => { await updateCustomThemeField('fontFamily', value); }); }); new Setting(basicSection) .setName('圆角') .setDesc('例如 0px、6px、8px、12px') .addDropdown(dropdown => { ['0px', '4px', '6px', '8px', '10px', '12px', '16px'].forEach(value => { dropdown.addOption(value, value); }); dropdown.setValue(String(customTheme.borderRadius || '8px')); dropdown.onChange(async value => { await updateCustomThemeField('borderRadius', value); }); }) .addText(text => { text.setPlaceholder('8px'); text.setValue(String(customTheme.borderRadius || '')); text.onChange(async value => { await updateCustomThemeField('borderRadius', value); }); }); new Setting(basicSection) .setName('行高') .setDesc('推荐 1.6 - 1.9') .addDropdown(dropdown => { ['1.6', '1.7', '1.75', '1.8', '1.85', '1.9'].forEach(value => { dropdown.addOption(value, value); }); dropdown.setValue(String(customTheme.lineHeight || '1.75')); dropdown.onChange(async value => { await updateCustomThemeField('lineHeight', value); }); }) .addText(text => { text.setPlaceholder('1.75'); text.setValue(String(customTheme.lineHeight || '')); text.onChange(async value => { await updateCustomThemeField('lineHeight', value); }); }); } const colorSection = containerEl.createDiv({ cls: 'bulk-copy-settings-section' }); colorSection.createEl('h3', { text: '颜色设置', cls: 'bulk-copy-settings-section-title' }); colorSection.createEl('p', { text: '支持点击色块选择颜色,也可以直接输入十六进制色值。', cls: 'bulk-copy-subtle-note' }); if (visibleTabs.includes('color')) { [ ['textColor', '正文颜色'], ['headingColor', '标题颜色'], ['backgroundColor', '背景颜色'], ['linkColor', '链接颜色'], ['blockquoteBackground', '引用背景'], ['codeBackground', '代码背景'], ['borderColor', '边框颜色'] ].forEach(([key, label]) => { new Setting(colorSection) .setName(label) .setDesc(`自定义主题的 ${label}`) .addColorPicker(color => { color.setValue(isHexColor(customTheme[key]) ? customTheme[key] : '#000000'); color.onChange(async value => { await updateCustomThemeField(key, value); }); }) .addText(text => { text.setPlaceholder('#000000'); text.setValue(String(customTheme[key] || '')); text.onChange(async value => { if (!value || isHexColor(value)) { await updateCustomThemeField(key, value); } }); }); }); } } createPlatformSection(containerEl, platformKey) { const section = containerEl.createEl('details', { cls: 'bulk-copy-platform-section' }); if (platformKey === USER_CONFIG.currentPlatform) { section.open = true; } const summary = section.createEl('summary'); summary.textContent = `${PLATFORM_PRESETS[platformKey].name} 配置`; new Setting(section) .setName('图片导出策略') .setDesc('设置该平台复制时使用的图片导出策略') .addDropdown(dropdown => { dropdown.addOption('quality', '高清优先'); dropdown.addOption('balanced', '平衡'); dropdown.addOption('size', '体积优先'); dropdown.setValue(getCurrentImageStrategy(platformKey)); dropdown.onChange(async value => { USER_CONFIG.platformImageStrategies = { ...DEFAULT_USER_CONFIG.platformImageStrategies, ...(USER_CONFIG.platformImageStrategies || {}), [platformKey]: value }; await this.plugin.saveUserConfig(); }); }); new Setting(section) .setName('平台主题') .setDesc('设置该平台导出时使用的主题样式') .addDropdown(dropdown => { this.addThemeOptions(dropdown, platformKey); dropdown.setValue(getCurrentPlatformTheme(platformKey)); dropdown.onChange(async value => { USER_CONFIG.platformThemes = { ...DEFAULT_USER_CONFIG.platformThemes, ...(USER_CONFIG.platformThemes || {}), [platformKey]: value }; await this.plugin.saveUserConfig(); this.display(); }); }); new Setting(section) .setName('恢复推荐主题') .setDesc(`一键恢复到 ${getRecommendedThemeLabels(platformKey).join(' / ') || '平台推荐主题'},并清空该平台的主题细节覆盖`) .addButton(button => { button.setButtonText('恢复推荐'); button.onClick(async () => { resetPlatformToRecommendedTheme(platformKey); await this.plugin.saveUserConfig(); this.display(); }); }); this.renderThemeDetailSettings(section, platformKey, '主题细节', '单独调整该平台导出时的标题、表格、图片和代码块表现。'); this.renderThemePreview(section, platformKey); } display() { const { containerEl } = this; containerEl.empty(); containerEl.addClass('bulk-copy-settings-root'); this.renderSettingsStyles(containerEl); const hero = containerEl.createDiv({ cls: 'bulk-copy-settings-hero' }); hero.createEl('h2', { text: 'Bulk Copy with Images 设置' }); hero.createEl('p', { text: '按平台管理复制主题、图片策略和导出细节。推荐先确定平台,再调整主题与局部风格。' }); const stats = hero.createDiv({ cls: 'bulk-copy-settings-grid' }); [ ['当前默认平台', PLATFORM_PRESETS[USER_CONFIG.currentPlatform]?.name || USER_CONFIG.currentPlatform], ['当前主题', getPlatformThemeLabel(USER_CONFIG.currentPlatform)], ['图片策略', getImageStrategyLabel(USER_CONFIG.currentPlatform)] ].forEach(([label, value]) => { const stat = stats.createDiv({ cls: 'bulk-copy-settings-stat' }); stat.createDiv({ text: label, cls: 'bulk-copy-settings-stat-label' }); stat.createDiv({ text: value, cls: 'bulk-copy-settings-stat-value' }); }); const basicSection = this.createSection(containerEl, '基础配置', '设置默认平台和基础图片参数。'); new Setting(basicSection) .setName('默认平台') .setDesc('快速复制命令会直接使用这里设置的平台预设') .addDropdown(dropdown => { Object.keys(PLATFORM_PRESETS).forEach(key => { dropdown.addOption(key, PLATFORM_PRESETS[key].name); }); dropdown.setValue(USER_CONFIG.currentPlatform); dropdown.onChange(async value => { applyPlatformPreset(value); await this.plugin.saveUserConfig(); this.display(); }); }); new Setting(basicSection) .setName('图片最大宽度') .setDesc('超过此宽度的图片会被等比压缩') .addText(text => { text.setPlaceholder('1000'); text.setValue(String(USER_CONFIG.imageMaxWidth)); text.onChange(async value => { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { return; } USER_CONFIG.imageMaxWidth = parsed; syncImageConfigFromUserConfig(); await this.plugin.saveUserConfig(); }); }); new Setting(basicSection) .setName('图片压缩质量') .setDesc('范围 0.1 - 1.0,值越高越清晰,体积也越大') .addText(text => { text.setPlaceholder('0.92'); text.setValue(String(USER_CONFIG.imageQuality)); text.onChange(async value => { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1) { return; } USER_CONFIG.imageQuality = parsed; syncImageConfigFromUserConfig(); await this.plugin.saveUserConfig(); }); }); const currentPlatformSection = this.createSection(containerEl, '当前平台快捷配置', '快速调整当前默认平台的主题和图片策略。'); new Setting(currentPlatformSection) .setName('图片导出策略') .setDesc('为当前默认平台设置图片导出策略') .addDropdown(dropdown => { dropdown.addOption('quality', '高清优先'); dropdown.addOption('balanced', '平衡'); dropdown.addOption('size', '体积优先'); dropdown.setValue(getCurrentImageStrategy(USER_CONFIG.currentPlatform)); dropdown.onChange(async value => { USER_CONFIG.platformImageStrategies = { ...DEFAULT_USER_CONFIG.platformImageStrategies, ...(USER_CONFIG.platformImageStrategies || {}), [USER_CONFIG.currentPlatform]: value }; await this.plugin.saveUserConfig(); this.display(); }); }); new Setting(currentPlatformSection) .setName('平台主题') .setDesc('为当前默认平台设置导出主题样式') .addDropdown(dropdown => { this.addThemeOptions(dropdown, USER_CONFIG.currentPlatform); dropdown.setValue(getCurrentPlatformTheme(USER_CONFIG.currentPlatform)); dropdown.onChange(async value => { USER_CONFIG.platformThemes = { ...DEFAULT_USER_CONFIG.platformThemes, ...(USER_CONFIG.platformThemes || {}), [USER_CONFIG.currentPlatform]: value }; await this.plugin.saveUserConfig(); this.display(); }); }); this.renderThemeDetailSettings( currentPlatformSection, USER_CONFIG.currentPlatform, '当前平台主题细节', '这里的细节设置会应用到当前默认平台。' ); const customThemeSection = this.createSection(containerEl, '自定义主题', '自定义主题模板已迁移到专门的管理器中,便于集中编辑和维护。'); new Setting(customThemeSection) .setName('主题模板管理器') .setDesc('打开独立管理器,集中处理搜索、排序、导入导出和主题编辑') .addButton(button => { button.setButtonText('打开管理器'); button.setCta(); button.onClick(() => { new ThemeManagerModal(this.app, this.plugin, () => this.display()).open(); }); }); const allPlatformsSection = this.createSection(containerEl, '各平台配置', '每个平台都可以独立设置图片策略、主题、细节参数,并查看预览。'); Object.keys(PLATFORM_PRESETS).forEach(platformKey => { this.createPlatformSection(allPlatformsSection, platformKey); }); const behaviorSection = this.createSection(containerEl, '行为偏好', '控制复制时的辅助行为与提示。'); new Setting(behaviorSection) .setName('启用主题样式保留') .setDesc('导出时尽量保留当前 Obsidian 主题的视觉效果') .addToggle(toggle => { toggle.setValue(!!USER_CONFIG.enableThemeStyles); toggle.onChange(async value => { USER_CONFIG.enableThemeStyles = value; await this.plugin.saveUserConfig(); }); }); new Setting(behaviorSection) .setName('显示进度提示') .setDesc('处理多张图片时显示通知提示') .addToggle(toggle => { toggle.setValue(!!USER_CONFIG.enableProgressNotice); toggle.onChange(async value => { USER_CONFIG.enableProgressNotice = value; await this.plugin.saveUserConfig(); }); }); } } /* ========== 复制内容函数 ========== */ async function copyContentWithImages(plugin, content, isFullFile) { const startTime = performance.now(); try { const file = plugin.app.workspace.getActiveFile(); if (!file) { new Notice('❌ 请先打开一篇 Markdown 笔记'); return; } console.log('[bulk-copy] 🚀 开始复制内容...'); const exportResult = await exportContentForClipboard(plugin, { mode: isFullFile ? 'full' : 'selection', selectedText: content || '', platform: USER_CONFIG.currentPlatform, includeImages: true, preserveTheme: USER_CONFIG.enableThemeStyles }); // 写入剪贴板 try { const payload = buildClipboardPayload(exportResult); if (Object.keys(payload).length === 0) { throw new Error('没有可用的剪贴板格式'); } await writeClipboardPayload(payload, exportResult); console.log('[bulk-copy] ✅ 复制成功', exportResult.meta); let sourceLabel = 'Markdown 兜底'; if (exportResult.meta && exportResult.meta.source === 'preview-dom') { sourceLabel = '预览 DOM'; } else if (exportResult.meta && exportResult.meta.source === 'offscreen-render') { sourceLabel = '离屏渲染'; } const strategyLabel = getImageStrategyLabel(USER_CONFIG.currentPlatform); const themeLabel = getPlatformThemeLabel(USER_CONFIG.currentPlatform); const imageCount = exportResult.meta?.imageCount ?? 0; new Notice(`✅ 已复制内容(来源: ${sourceLabel},主题: ${themeLabel},图片策略: ${strategyLabel},图片数: ${imageCount})`); if (exportResult.meta && exportResult.meta.warnings && exportResult.meta.warnings.length > 0) { exportResult.meta.warnings.forEach(warning => new Notice(`⚠️ ${warning}`)); } } catch (err) { console.error('[bulk-copy] 复制失败:', err); new Notice('❌ 复制失败: ' + err.message); } const endTime = performance.now(); const duration = ((endTime - startTime) / 1000).toFixed(2); console.log(`[bulk-copy] ✅ 复制完成,耗时: ${duration}秒`); } catch (err) { console.error('[bulk-copy] 复制过程发生错误:', err); new Notice('❌ 复制失败: ' + err.message); } } /* ========== 快速复制 ========== */ async function quickCopyActiveFileWithImages(plugin) { const file = plugin.app.workspace.getActiveFile(); if (!file) { new Notice('❌ 请先打开一篇 Markdown 笔记'); return; } await copyContentWithImages(plugin, null, true); } /* ========== 复制全文(带平台选择) ========== */ async function copyActiveFileWithImages(plugin) { const file = plugin.app.workspace.getActiveFile(); if (!file) { new Notice('❌ 请先打开一篇 Markdown 笔记'); return; } new PlatformSelectorModal(plugin.app, async (selectedPlatform) => { applyPlatformPreset(selectedPlatform); await copyContentWithImages(plugin, null, true); }).open(); } /* ========== 复制选定内容(带平台选择) ========== */ async function copySelectedContentWithImages(plugin) { const file = plugin.app.workspace.getActiveFile(); if (!file) { new Notice('❌ 请先打开一篇 Markdown 笔记'); return; } const activeEditor = plugin.app.workspace.activeEditor; if (!activeEditor || !activeEditor.editor) { new Notice('❌ 无法获取编辑器实例'); return; } const editor = activeEditor.editor; const selectedText = editor.getSelection(); if (!selectedText || selectedText.trim() === '') { new Notice('❌ 请先选择要复制的文本内容'); return; } new PlatformSelectorModal(plugin.app, async (selectedPlatform) => { applyPlatformPreset(selectedPlatform); await copyContentWithImages(plugin, selectedText, false); }).open(); } /* ========== 快速复制选定内容 ========== */ async function quickCopySelectedContentWithImages(plugin) { const file = plugin.app.workspace.getActiveFile(); if (!file) { new Notice('❌ 请先打开一篇 Markdown 笔记'); return; } const activeEditor = plugin.app.workspace.activeEditor; if (!activeEditor || !activeEditor.editor) { new Notice('❌ 无法获取编辑器实例'); return; } const editor = activeEditor.editor; const selectedText = editor.getSelection(); if (!selectedText || selectedText.trim() === '') { new Notice('❌ 请先选择要复制的文本内容'); return; } await copyContentWithImages(plugin, selectedText, false); } /* ========== 插件入口 ========== */ module.exports = class BulkCopyImagesPlugin extends Plugin { async onload() { console.log('[bulk-copy] Bulk Copy with Images 插件已加载'); await this.loadUserConfig(); new Notice('✅ Bulk Copy with Images 插件已加载'); // 主命令:复制全文(带平台选择) this.addCommand({ id: 'bulk-copy-with-images', name: '复制全文(选择平台)', hotkeys: [{ modifiers: ['Ctrl', 'Shift'], key: 'c' }], callback: () => copyActiveFileWithImages(this) }); // 主命令:复制选定内容(带平台选择) this.addCommand({ id: 'bulk-copy-selected-with-images', name: '复制选定内容(选择平台)', hotkeys: [{ modifiers: ['Ctrl', 'Shift'], key: 'v' }], callback: () => copySelectedContentWithImages(this) }); // 快速命令:复制全文(使用默认平台) this.addCommand({ id: 'quick-copy-with-images', name: '快速复制全文(使用默认平台)', hotkeys: [{ modifiers: ['Ctrl', 'Alt'], key: 'c' }], callback: () => quickCopyActiveFileWithImages(this) }); // 快速命令:复制选定内容(使用默认平台) this.addCommand({ id: 'quick-copy-selected-with-images', name: '快速复制选定内容(使用默认平台)', hotkeys: [{ modifiers: ['Ctrl', 'Alt'], key: 'v' }], callback: () => quickCopySelectedContentWithImages(this) }); // 平台选择命令 Object.keys(PLATFORM_PRESETS).forEach(platformKey => { const preset = PLATFORM_PRESETS[platformKey]; this.addCommand({ id: `switch-platform-${platformKey}`, name: `切换默认平台: ${preset.name}`, callback: async () => { applyPlatformPreset(platformKey); await this.saveUserConfig(); } }); }); this.settingTab = new BulkCopySettingTab(this.app, this); this.addSettingTab(this.settingTab); // 监听主题切换 this.registerEvent( this.app.workspace.on('css-change', () => { Cache.clear(); console.log('[bulk-copy] 主题已切换,清除样式缓存'); }) ); } async loadUserConfig() { const savedConfig = await this.loadData(); USER_CONFIG = { ...DEFAULT_USER_CONFIG, ...(savedConfig || {}) }; USER_CONFIG.platformImageStrategies = { ...DEFAULT_USER_CONFIG.platformImageStrategies, ...(savedConfig?.platformImageStrategies || {}) }; USER_CONFIG.platformThemes = { ...DEFAULT_USER_CONFIG.platformThemes, ...(savedConfig?.platformThemes || {}) }; USER_CONFIG.customThemes = { ...DEFAULT_USER_CONFIG.customThemes, ...(savedConfig?.customThemes || {}) }; USER_CONFIG.customThemeEditorKey = savedConfig?.customThemeEditorKey || DEFAULT_USER_CONFIG.customThemeEditorKey; USER_CONFIG.customThemeSearchQuery = savedConfig?.customThemeSearchQuery || DEFAULT_USER_CONFIG.customThemeSearchQuery; USER_CONFIG.customThemeOrder = savedConfig?.customThemeOrder || DEFAULT_USER_CONFIG.customThemeOrder; USER_CONFIG.themePreviewPlatform = savedConfig?.themePreviewPlatform || DEFAULT_USER_CONFIG.themePreviewPlatform; USER_CONFIG.themeListShowUsedOnly = typeof savedConfig?.themeListShowUsedOnly === 'boolean' ? savedConfig.themeListShowUsedOnly : DEFAULT_USER_CONFIG.themeListShowUsedOnly; USER_CONFIG.themeListUsedFirst = typeof savedConfig?.themeListUsedFirst === 'boolean' ? savedConfig.themeListUsedFirst : DEFAULT_USER_CONFIG.themeListUsedFirst; USER_CONFIG.themeListShowRecommendedOnly = typeof savedConfig?.themeListShowRecommendedOnly === 'boolean' ? savedConfig.themeListShowRecommendedOnly : DEFAULT_USER_CONFIG.themeListShowRecommendedOnly; USER_CONFIG.themeManagerListCollapsed = typeof savedConfig?.themeManagerListCollapsed === 'boolean' ? savedConfig.themeManagerListCollapsed : DEFAULT_USER_CONFIG.themeManagerListCollapsed; USER_CONFIG.lastAppliedRecommendedTheme = savedConfig?.lastAppliedRecommendedTheme || DEFAULT_USER_CONFIG.lastAppliedRecommendedTheme; USER_CONFIG.themeManagerSidebarWidth = savedConfig?.themeManagerSidebarWidth || DEFAULT_USER_CONFIG.themeManagerSidebarWidth; USER_CONFIG.platformThemeOverrides = { ...DEFAULT_USER_CONFIG.platformThemeOverrides, ...(savedConfig?.platformThemeOverrides || {}) }; applyCurrentPlatformConfig(); } async saveUserConfig() { syncImageConfigFromUserConfig(); await this.saveData(USER_CONFIG); } onunload() { console.log('[bulk-copy] Bulk Copy with Images 插件已卸载'); Cache.clear(); } };