4173 lines
147 KiB
JavaScript
4173 lines
147 KiB
JavaScript
/* 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 ``;
|
||
}
|
||
|
||
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 `<img src="data:${mime};base64,${base64Svg}" alt="${alt}" style="max-width: ${maxWidth}px; width: 100%; height: auto; display: block; margin: 15px auto;" />`;
|
||
} catch (e) {
|
||
return ``;
|
||
}
|
||
}
|
||
|
||
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 `<img src="data:${mime};base64,${data}" alt="${alt}" style="max-width: ${maxWidth}px; width: 100%; height: auto; display: block; margin: 15px auto;" />`;
|
||
} catch (e) {
|
||
return ``;
|
||
}
|
||
}
|
||
|
||
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 ``;
|
||
}
|
||
|
||
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 `<img src="data:${finalMime};base64,${finalData}" alt="${altText}" style="${imgStyle}" />`;
|
||
} catch (e) {
|
||
console.error('[bulk-copy] 图片处理失败:', imgName, e);
|
||
return ``;
|
||
}
|
||
}
|
||
|
||
/* ========== 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, '>')
|
||
.replace(/"/g, '"');
|
||
};
|
||
|
||
// 代码块(先处理,避免内部语法被转换)
|
||
const codeBlocks = [];
|
||
html = html.replace(/```[\s\S]*?```/g, (match) => {
|
||
const id = codeBlocks.length;
|
||
const code = match.replace(/```/g, '').trim();
|
||
codeBlocks.push(`<pre style="background: #f6f8fa; padding: 16px; border-radius: 6px; overflow-x: auto;"><code style="font-family: monospace;">${escapeHtml(code)}</code></pre>`);
|
||
return `\n<!--CODEBLOCK${id}-->\n`;
|
||
});
|
||
|
||
// 行内代码
|
||
const inlineCodes = [];
|
||
html = html.replace(/`([^`\n]+)`/g, (match, code) => {
|
||
const id = inlineCodes.length;
|
||
inlineCodes.push(`<code style="background: rgba(175,184,193,0.2); padding: 2px 4px; border-radius: 3px; font-family: monospace;">${escapeHtml(code)}</code>`);
|
||
return `<!--INLINECODE${id}-->`;
|
||
});
|
||
|
||
// Wiki 链接 [[link]]
|
||
html = html.replace(/\[\[([^\]|]+?)(?:\|([^\]]+))?\]\]/g, (match, link, text) => {
|
||
const display = text || link;
|
||
return `<a href="${link}" target="_blank">${display}</a>`;
|
||
});
|
||
|
||
// 标签 #tag
|
||
html = html.replace(/(?<!\w)#(\w+)/g, (match, tag) => {
|
||
return `<span style="background-color: rgba(0, 123, 255, 0.1); color: #007bff; padding: 2px 6px; border-radius: 4px; font-size: 0.85em;">#${tag}</span>`;
|
||
});
|
||
|
||
// 图片
|
||
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
||
return `<img src="${src}" alt="${alt}" style="max-width: 100%; height: auto; display: block; margin: 15px auto;">`;
|
||
});
|
||
|
||
// Wiki 图片 ![[img]]
|
||
html = html.replace(/!\[\[([^\]|]+?)(?:\|[^\]]+)?\]\]/g, (match, img) => {
|
||
const name = img.split('|')[0].trim();
|
||
return `<img src="${name}" alt="${name}" style="max-width: 100%; height: auto; display: block; margin: 15px auto;">`;
|
||
});
|
||
|
||
// 标题
|
||
html = html.replace(/^######\s+(.+)$/gm, '<h6>$1</h6>');
|
||
html = html.replace(/^#####\s+(.+)$/gm, '<h5>$1</h5>');
|
||
html = html.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>');
|
||
html = html.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
|
||
html = html.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>');
|
||
html = html.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>');
|
||
|
||
// 粗体
|
||
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
|
||
|
||
// 粗斜体
|
||
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
||
html = html.replace(/___(.+?)___/g, '<strong><em>$1</em></strong>');
|
||
|
||
// 斜体
|
||
html = html.replace(/\*([^\s*][^*]*?)\*/g, '<em>$1</em>');
|
||
html = html.replace(/\b_([^\s_][^_]*?)_\b/g, '<em>$1</em>');
|
||
|
||
// 删除线
|
||
html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
|
||
|
||
// 高亮
|
||
html = html.replace(/==(.+?)==/g, '<mark>$1</mark>');
|
||
|
||
// 引用
|
||
html = html.replace(/^>\s+(.+)$/gm, '<blockquote style="border-left: 4px solid #ddd; padding: 10px 15px; margin: 15px 0; color: #666;">$1</blockquote>');
|
||
|
||
// 分割线
|
||
html = html.replace(/^(---|\*\*\*|___)$/gm, '<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">');
|
||
|
||
// 列表(先处理无序列表)
|
||
html = html.replace(/^[\*\-\+]\s+(.+)$/gm, '<li>$1</li>');
|
||
|
||
// 处理有序列表
|
||
html = html.replace(/^\d+\.\s+(.+)$/gm, '<li>$1</li>');
|
||
|
||
// 任务列表
|
||
html = html.replace(/^[\*\-\+]\s+\[x\]\s+(.+)$/gm, '<li><input type="checkbox" checked disabled style="margin-right: 8px;"> $1</li>');
|
||
html = html.replace(/^[\*\-\+]\s+\[\s\]\s+(.+)$/gm, '<li><input type="checkbox" disabled style="margin-right: 8px;"> $1</li>');
|
||
html = html.replace(/^\d+\.\s+\[x\]\s+(.+)$/gm, '<li><input type="checkbox" checked disabled style="margin-right: 8px;"> $1</li>');
|
||
html = html.replace(/^\d+\.\s+\[\s\]\s+(.+)$/gm, '<li><input type="checkbox" disabled style="margin-right: 8px;"> $1</li>');
|
||
|
||
// 链接
|
||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||
|
||
// 表格
|
||
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 = `<tr>${headers.map(h => `<th style="border: 1px solid #ddd; padding: 8px 12px; background: #f6f8fa; font-weight: 600;">${h}</th>`).join('')}</tr>`;
|
||
const rows = body.trim().split('\n').map(row => {
|
||
const cells = row.split('|').map(c => c.trim()).filter(c => c);
|
||
return `<tr>${cells.map(c => `<td style="border: 1px solid #ddd; padding: 8px 12px;">${c}</td>`).join('')}</tr>`;
|
||
}).join('');
|
||
return `<table style="border-collapse: collapse; width: 100%; margin: 15px 0;"><thead>${headerHtml}</thead><tbody>${rows}</tbody></table>`;
|
||
});
|
||
|
||
// 关键修复:处理换行
|
||
// 将单个换行符转换为 <br>,保留段落结构
|
||
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(/^<!--CODEBLOCK|INLINECODE/) ||
|
||
trimmed === '';
|
||
|
||
if (isBlockElement) {
|
||
// 如果在段落中,先结束段落
|
||
if (inParagraph && paragraphContent.length > 0) {
|
||
// 段落内容用 <br> 连接(保留 Obsidian 的换行)
|
||
processed.push(`<p style="margin: 15px 0; line-height: 1.6;">${paragraphContent.join('<br>')}</p>`);
|
||
paragraphContent = [];
|
||
inParagraph = false;
|
||
}
|
||
|
||
// 如果不是空行,添加块级元素
|
||
if (trimmed !== '') {
|
||
processed.push(line);
|
||
}
|
||
} else {
|
||
// 普通文本行
|
||
if (!inParagraph) {
|
||
inParagraph = true;
|
||
}
|
||
paragraphContent.push(line);
|
||
}
|
||
}
|
||
|
||
// 处理末尾的段落
|
||
if (inParagraph && paragraphContent.length > 0) {
|
||
processed.push(`<p style="margin: 15px 0; line-height: 1.6;">${paragraphContent.join('<br>')}</p>`);
|
||
}
|
||
|
||
html = processed.join('\n');
|
||
|
||
// 还原占位符
|
||
codeBlocks.forEach((code, i) => {
|
||
html = html.replace(new RegExp(`<!--CODEBLOCK${i}-->`, 'g'), code);
|
||
});
|
||
|
||
inlineCodes.forEach((code, i) => {
|
||
html = html.replace(new RegExp(`<!--INLINECODE${i}-->`, '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 = `<strong>${cells[0].textContent?.trim() || '字段'}:</strong>${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 = `<strong>${labelPrefix}:</strong>${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 返回 )
|
||
// 记录一下原始 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
|
||
? `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>${html}</body></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 = `<div style="font-family: ${platformTheme.fontFamily}; font-size: ${platformPreset.fontSize || '16px'}; line-height: ${platformTheme.lineHeight || platformPreset.lineHeight || '1.6'}; color: ${platformTheme.textColor}; background-color: ${platformTheme.backgroundColor}; max-width: ${typeof platformPreset.displayMaxWidth === 'number' ? `${platformPreset.displayMaxWidth}px` : platformPreset.displayMaxWidth}; margin: 0 auto;">${htmlContent}</div>`;
|
||
|
||
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 = `
|
||
<div class="option-content">
|
||
<div class="platform-icon">${icons[platformKey] || '📱'}</div>
|
||
<div class="platform-info">
|
||
<div class="platform-name">${preset.name}</div>
|
||
<div class="platform-desc">${preset.description}</div>
|
||
<div class="platform-meta-row">
|
||
<span class="platform-theme">${themeLabel}</span>
|
||
</div>
|
||
<div class="platform-meta-row muted">
|
||
<span class="platform-recommended-theme">推荐 ${recommendedThemeLabels.join(' / ') || '无'}</span>
|
||
</div>
|
||
</div>
|
||
<div class="check-icon">✓</div>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<h2>示例标题</h2>
|
||
<p>这是一段用于预览导出效果的正文内容,展示当前平台主题的字体、颜色、间距和整体观感,并包含 <code>行内代码</code> 与 <a href="#">链接样式</a>。</p>
|
||
<h3>二级内容分组</h3>
|
||
<p>下面的区块用于观察更复杂的富文本结构在当前主题下的表现。</p>
|
||
<blockquote>这是一段引用示例,用于观察边框、背景和文本色。</blockquote>
|
||
<ul>
|
||
<li>无序列表第一项</li>
|
||
<li>无序列表第二项,包含 <code>inline code</code></li>
|
||
</ul>
|
||
<ol>
|
||
<li>有序列表第一项</li>
|
||
<li>有序列表第二项</li>
|
||
</ol>
|
||
<pre><code>const message = 'theme preview';</code></pre>
|
||
<table>
|
||
<thead>
|
||
<tr><th>字段</th><th>说明</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr><td>主题</td><td>${theme.name}</td></tr>
|
||
<tr><td>平台</td><td>${PLATFORM_PRESETS[platformKey].name}</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<img alt="preview" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nNzIwJyBoZWlnaHQ9JzM2MCcgdmlld0JveD0nMCAwIDcyMCAzNjAnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zyc+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSdnJyB4MT0nMCcgeTE9JzAnIHgyPScxJyB5Mj0nMSc+PHN0b3Agb2Zmc2V0PScwJScgc3RvcC1jb2xvcj0nI2UzZThmZicvPjxzdG9wIG9mZnNldD0nMTAwJScgc3RvcC1jb2xvcj0nI2QxZmFlNScvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHdpZHRoPSc3MjAnIGhlaWdodD0nMzYwJyByeD0nMjQnIGZpbGw9J3VybCgjZyknLz48cmVjdCB4PSc2MCcgeT0nNjAnIHdpZHRoPSc2MDAnIGhlaWdodD0nMTgwJyByeD0nMTYnIGZpbGw9JyNmZmZmZmYnIG9wYWNpdHk9JzAuODUnLz48dGV4dCB4PSc5MCcgeT0nMTMwJyBmb250LXNpemU9JzM2JyBmaWxsPScjMWYyOTM3JyBmb250LWZhbWlseT0nQXJpYWwsIHNhbnMtc2VyaWYnPuWbvueJh+inhOiIoumihuWfnyDpooTonIh+</dGV4dD48dGV4dCB4PSc5MCcgeT0nMTgwJyBmb250LXNpemU9JzIwJyBmaWxsPScjNjQ3NDhiJyBmb250LWZhbWlseT0nQXJpYWwsIHNhbnMtc2VyaWYnPuWPr+eUqOS6juinguWvn+aWh+eroOeCueOAgeS7o+eggeWdl+WSjOWbvueJh+WxleekuuOAguWImO+8mTwvdGV4dD48Y2lyY2xlIGN4PSc1ODAnIGN5PScyODAnIHI9JzQ4JyBmaWxsPScjOTMzM2VhJyBvcGFjaXR5PScwLjE1Jy8+PGNpcmNsZSBjeD0nNjIwJyBjeT0nMjQwJyByPScyOCcgZmlsbD0nI2Y1OWUwYicgb3BhY2l0eT0nMC4yNScvPjwvc3ZnPg==" />
|
||
<p>图片说明文字:用于观察图片上下间距、圆角和说明段落是否协调。</p>
|
||
`;
|
||
|
||
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();
|
||
}
|
||
};
|