├── .gitignore ├── LICENSE ├── README.md ├── build.sh ├── esbuild.config.mjs ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── assets │ ├── donate.ts │ ├── donate │ │ ├── alipay.png │ │ └── wechat_pay.png │ └── qrcode.ts ├── backgroundManager.ts ├── backgrounds │ └── index.ts ├── converter.ts ├── copyManager.ts ├── donateManager.ts ├── main.ts ├── settings │ ├── ConfirmModal.ts │ ├── CreateBackgroundModal.ts │ ├── CreateFontModal.ts │ ├── CreateTemplateModal.ts │ ├── MPSettingTab.ts │ ├── settings.ts │ └── templatePreviewModal.ts ├── styles │ ├── index.css │ ├── settings │ │ ├── background-modal.css │ │ ├── font-modal.css │ │ ├── settings.css │ │ ├── template-modal.css │ │ └── template-preview-modal.css │ └── view │ │ ├── layout.css │ │ └── preview.css ├── templateManager.ts ├── templates │ ├── academic.json │ ├── brown.json │ ├── dark.json │ ├── darkgreen.json │ ├── default.json │ ├── elegant.json │ ├── index.ts │ ├── minimal.json │ ├── orange.json │ ├── scarlet.json │ ├── yeban-orange.json │ └── yeban.json ├── utils │ └── nanoid.ts └── view.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # 依赖目录 2 | node_modules/ 3 | 4 | # 构建输出 5 | dist/ 6 | main.js 7 | data.json 8 | styles.css 9 | # 操作系统文件 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # IDE 配置 14 | .vscode/ 15 | .idea/ 16 | 17 | # 日志文件 18 | *.log 19 | 20 | # 环境变量 21 | .env 22 | .env.local -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 夜半Yeban 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MP Preview 2 | 一键将 Obsidian 笔记转换为微信公众号格式的插件。 3 | 4 | ![downloads](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=downloads&query=%24%5B%22mp-preview%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json&style=flat) ![version](https://img.shields.io/github/v/tag/Yeban8090/mp-preview?color=blue&label=version&style=flat) ![license](https://img.shields.io/badge/license-MIT-green) [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-支持作者-yellow)](#支持作者) 5 | 6 | ## 功能特点 7 | - 📝 一键将 Markdown 文档转换为微信公众号格式 8 | - 🎨 提供多种精美模板,支持自定义字体和字号 9 | - 🖼️ 支持自定义背景和样式 10 | - 🔄 实时预览编辑效果 11 | - 📋 一键复制到剪贴板,直接粘贴到公众号 12 | - 🔒 锁定功能避免预览刷新打断书写 13 | 14 | ## 使用方法 15 | 1. 打开任意 Markdown 文档 16 | 2. 点击侧边栏的公众号图标打开预览面板 17 | 3. 选择喜欢的模板和字体 18 | 4. 调整字号大小以适应内容 19 | 5. 实时预览编辑效果 20 | 6. 点击【复制到公众号】按钮 21 | 7. 直接粘贴到微信公众号编辑器中 22 | 23 | ## 安装方法 24 | ### 从 Obsidian 社区插件安装(推荐) 25 | 1. 打开 Obsidian 设置 26 | 2. 转到第三方插件设置 27 | 3. 关闭安全模式 28 | 4. 点击浏览社区插件 29 | 5. 搜索 "MP Preview" 30 | 6. 点击安装并启用插件 31 | 32 | ### 手动安装 33 | 1. 下载最新版本的 release 文件:https://github.com/Yeban8090/mp-preview/releases 34 | 2. 解压后将文件夹复制到 Obsidian 插件目录:`{vault}/.obsidian/plugins/` 35 | 3. 重启 Obsidian 36 | 4. 在设置中启用插件 37 | 38 | ## 使用技巧 39 | - 使用锁定按钮(🔒)可以暂停实时预览,避免编辑大文档时频繁刷新 40 | - 调整字号大小以获得最佳阅读体验 41 | - 不同模板适合不同类型的文章,可以根据内容选择合适的模板 42 | - 复制前可以预览最终效果,确保格式正确 43 | - 支持代码块、表格、图片等 Markdown 元素 44 | 45 | ## 支持的语言 46 | 插件界面目前支持: 47 | - 简体中文 48 | 49 | ## 支持作者 50 | 如果这个插件对你有所帮助,可以考虑请作者喝杯咖啡 ☕: 51 | 52 |
53 |
54 |
55 | 微信支付
56 | 微信支付 57 |
58 |
59 | 支付宝
60 | 支付宝 61 |
62 |
63 | Buy Me a Coffee
64 | 65 | Buy Me a Coffee 66 | 67 |
68 |
69 |
70 | 71 | 您的支持是我持续改进这个插件的动力! 72 | 73 | ## 许可证 74 | MIT License。查看 [LICENSE](LICENSE) 获取更多信息。 -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 获取版本号 4 | version=$(grep '"version"' manifest.json | cut -d '"' -f 4) 5 | zip_name="mp-preview-${version}.zip" 6 | 7 | # 检查目标文件是否存在 8 | if [ -f "../../${zip_name}" ]; then # 修改路径检查到上两层 9 | read -p "文件 ${zip_name} 已存在,是否覆盖?(y/n) " answer 10 | if [ "$answer" != "y" ]; then 11 | echo "打包已取消" 12 | exit 1 13 | fi 14 | fi 15 | 16 | # 创建临时目录 17 | mkdir -p ../temp/mp-preview 18 | 19 | # 复制必要文件 20 | cp main.js manifest.json styles.css ../temp/mp-preview/ 21 | cp -r assets ../temp/mp-preview/ 22 | 23 | # 切换到临时目录的上级目录 24 | cd ../temp 25 | 26 | # 创建 zip 文件 27 | zip -r "${zip_name}" mp-preview 28 | 29 | # 移动 zip 文件到上两层目录 30 | mv "${zip_name}" ../../ 31 | 32 | echo "打包完成:${zip_name}" -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = process.argv[2] === "production"; 13 | 14 | const config = { 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["src/main.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | ...builtins 25 | ], 26 | format: "cjs", 27 | target: "es2018", 28 | logLevel: "info", 29 | sourcemap: prod ? false : "inline", 30 | treeShaking: true, 31 | outfile: "main.js", 32 | }; 33 | 34 | // 修改 CSS 配置部分 35 | const cssConfig = { 36 | entryPoints: ['src/styles/index.css'], 37 | bundle: true, 38 | outfile: 'styles.css', // 直接输出到项目根目录 39 | allowOverwrite: true 40 | }; 41 | 42 | if (prod) { 43 | await Promise.all([ 44 | esbuild.build(config), 45 | esbuild.build(cssConfig) 46 | ]).catch(() => process.exit(1)); 47 | } else { 48 | const [jsContext, cssContext] = await Promise.all([ 49 | esbuild.context(config), 50 | esbuild.context(cssConfig) 51 | ]); 52 | 53 | await Promise.all([ 54 | jsContext.watch(), 55 | cssContext.watch() 56 | ]); 57 | 58 | console.log("⚡ 正在监听 JavaScript 和 CSS 变更..."); 59 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "mp-preview", 3 | "name": "MP Preview", 4 | "version": "1.0.4", 5 | "minAppVersion": "0.15.0", 6 | "description": "Preview and convert Markdown files to MP format", 7 | "author": "Yeban", 8 | "isDesktopOnly": true 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mp-preview", 3 | "version": "1.0.2", 4 | "description": "一键将 Markdown 文档转换为微信公众号格式", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [ 12 | "obsidian", 13 | "plugin" 14 | ], 15 | "author": "", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@types/node": "^16.11.6", 19 | "@typescript-eslint/eslint-plugin": "5.29.0", 20 | "@typescript-eslint/parser": "5.29.0", 21 | "builtin-modules": "3.3.0", 22 | "esbuild": "0.17.3", 23 | "obsidian": "latest", 24 | "tslib": "2.4.0", 25 | "typescript": "4.7.4" 26 | }, 27 | "dependencies": { 28 | "nanoid": "^5.1.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/assets/donate/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeban8090/mp-preview/cb2d236f946abd10b4a6a07fb56342f61b11e3f3/src/assets/donate/alipay.png -------------------------------------------------------------------------------- /src/assets/donate/wechat_pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeban8090/mp-preview/cb2d236f946abd10b4a6a07fb56342f61b11e3f3/src/assets/donate/wechat_pay.png -------------------------------------------------------------------------------- /src/backgroundManager.ts: -------------------------------------------------------------------------------- 1 | import { SettingsManager } from "./settings/settings"; 2 | 3 | export interface Background { 4 | id: string; 5 | name: string; 6 | style: string; 7 | isPreset?: boolean; 8 | isVisible?: boolean; 9 | } 10 | 11 | export class BackgroundManager { 12 | private currentBackground: Background | null = null; 13 | private settingsManager: SettingsManager; 14 | 15 | constructor(settingsManager: SettingsManager) { 16 | this.settingsManager = settingsManager; 17 | } 18 | 19 | public setBackground(id: string | null): boolean { 20 | if (!id) { 21 | this.currentBackground = null; 22 | return true; 23 | } 24 | 25 | const background = this.settingsManager.getBackground(id); 26 | if (background) { 27 | // 检查背景是否可见 28 | if (background.isVisible === false) { 29 | console.warn(`尝试设置不可见的背景: ${id}`); 30 | return false; 31 | } 32 | 33 | this.currentBackground = background; 34 | return true; 35 | } 36 | 37 | console.warn(`未找到背景: ${id}`); 38 | return false; 39 | } 40 | 41 | public applyBackground(element: HTMLElement) { 42 | const section = element.querySelector('.mp-content-section'); 43 | if (section) { 44 | if (!this.currentBackground) { 45 | section.setAttribute('style', ''); // 当没有背景时,清除样式 46 | return; 47 | } 48 | section.setAttribute('style', this.currentBackground.style); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/backgrounds/index.ts: -------------------------------------------------------------------------------- 1 | export const backgrounds = { 2 | backgrounds: [ 3 | { 4 | id: "grid", 5 | name: "网格", 6 | style: "box-sizing: border-box; margin: 0; padding: 0; background-image: linear-gradient(90deg, rgba(50, 0, 0, 0.03) 2%, rgba(0, 0, 0, 0) 2%), linear-gradient(360deg, rgba(50, 0, 0, 0.03) 2%, rgba(0, 0, 0, 0) 2%); background-size: 20px 20px; background-position: center center;" 7 | }, 8 | { 9 | id: "crosshatch", 10 | name: "交叉", 11 | style: "box-sizing: border-box; margin: 0; padding: 0; background-image: repeating-linear-gradient(45deg, rgba(50, 0, 0, 0.02) 0, rgba(50, 0, 0, 0.02) 1px, transparent 1px, transparent 50%), repeating-linear-gradient(-45deg, rgba(50, 0, 0, 0.02) 0, rgba(50, 0, 0, 0.02) 1px, transparent 1px, transparent 50%); background-size: 20px 20px;" 12 | }, 13 | { 14 | id: "dots", 15 | name: "圆点", 16 | style: "box-sizing: border-box; margin: 0; padding: 0; background-image: radial-gradient(rgba(50, 0, 0, 0.03) 1px, transparent 1px); background-size: 20px 20px; background-position: center center;" 17 | }, 18 | { 19 | id: "dash", 20 | name: "虚线", 21 | style: "box-sizing: border-box; margin: 0; padding: 0; background-image: linear-gradient(90deg, rgba(50, 0, 0, 0.03) 50%, transparent 50%); background-size: 8px 1px; background-position: center center;" 22 | }, 23 | { 24 | id: "wave", 25 | name: "波浪", 26 | style: "box-sizing: border-box; margin: 0; padding: 0; background-image: linear-gradient(45deg, rgba(50, 0, 0, 0.04) 12%, transparent 12%, transparent 88%, rgba(50, 0, 0, 0.04) 88%), linear-gradient(135deg, rgba(50, 0, 0, 0.04) 12%, transparent 12%, transparent 88%, rgba(50, 0, 0, 0.04) 88%), linear-gradient(45deg, rgba(50, 0, 0, 0.04) 12%, transparent 12%, transparent 88%, rgba(50, 0, 0, 0.04) 88%), linear-gradient(135deg, rgba(50, 0, 0, 0.04) 12%, transparent 12%, transparent 88%, rgba(50, 0, 0, 0.04) 88%); background-size: 30px 30px; background-position: 0 0, 0 0, 15px 15px, 15px 15px;" 27 | }, 28 | { 29 | id: "checkerboard", 30 | name: "棋盘", 31 | style: "box-sizing: border-box; margin: 0; padding: 0; background-image: linear-gradient(45deg, rgba(50, 0, 0, 0.04) 25%, transparent 25%), linear-gradient(-45deg, rgba(50, 0, 0, 0.04) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, rgba(50, 0, 0, 0.04) 75%), linear-gradient(-45deg, transparent 75%, rgba(50, 0, 0, 0.04) 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px;" 32 | } 33 | ] 34 | }; -------------------------------------------------------------------------------- /src/converter.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'obsidian'; 2 | 3 | export class MPConverter { 4 | private static app: App; 5 | 6 | static initialize(app: App) { 7 | this.app = app; 8 | } 9 | 10 | static formatContent(element: HTMLElement): void { 11 | // 创建 section 容器 12 | const section = document.createElement('section'); 13 | section.className = 'mp-content-section'; 14 | // 移动原有内容到 section 中 15 | while (element.firstChild) { 16 | section.appendChild(element.firstChild); 17 | } 18 | element.appendChild(section); 19 | 20 | // 处理元素 21 | this.processElements(section); 22 | } 23 | 24 | private static processElements(container: HTMLElement | null): void { 25 | if (!container) return; 26 | // 处理列表项内部元素,用section包裹 27 | container.querySelectorAll('li').forEach(li => { 28 | // 创建section元素 29 | const section = document.createElement('section'); 30 | // 将li的所有子元素移动到section中 31 | while (li.firstChild) { 32 | section.appendChild(li.firstChild); 33 | } 34 | // 将section添加到li中 35 | li.appendChild(section); 36 | }); 37 | 38 | // 处理代码块 39 | container.querySelectorAll('pre code').forEach(el => { 40 | const pre = el.parentElement; 41 | if (pre) { 42 | // 添加 macOS 风格的窗口按钮 43 | const header = document.createElement('div'); 44 | header.className = 'mp-code-header'; 45 | 46 | // 添加三个窗口按钮 47 | for (let i = 0; i < 3; i++) { 48 | const dot = document.createElement('span'); 49 | dot.className = 'mp-code-dot'; 50 | header.appendChild(dot); 51 | } 52 | 53 | pre.insertBefore(header, pre.firstChild); 54 | 55 | // 移除原有的复制按钮 56 | const copyButton = pre.querySelector('.copy-code-button'); 57 | if (copyButton) { 58 | copyButton.remove(); 59 | } 60 | } 61 | }); 62 | 63 | // 处理图片 64 | container.querySelectorAll('span.internal-embed[alt][src]').forEach(async el => { 65 | const originalSpan = el as HTMLElement; 66 | const src = originalSpan.getAttribute('src'); 67 | const alt = originalSpan.getAttribute('alt'); 68 | 69 | if (!src) return; 70 | 71 | try { 72 | const linktext = src.split('|')[0]; 73 | const file = this.app.metadataCache.getFirstLinkpathDest(linktext, ''); 74 | if (file) { 75 | const absolutePath = this.app.vault.adapter.getResourcePath(file.path); 76 | const newImg = document.createElement('img'); 77 | newImg.src = absolutePath; 78 | if (alt) newImg.alt = alt; 79 | originalSpan.parentNode?.replaceChild(newImg, originalSpan); 80 | } 81 | } catch (error) { 82 | console.error('图片处理失败:', error); 83 | } 84 | }); 85 | } 86 | } -------------------------------------------------------------------------------- /src/copyManager.ts: -------------------------------------------------------------------------------- 1 | import { Notice } from 'obsidian'; 2 | 3 | export class CopyManager { 4 | private static cleanupHtml(element: HTMLElement): string { 5 | // 创建克隆以避免修改原始元素 6 | const clone = element.cloneNode(true) as HTMLElement; 7 | 8 | // 移除所有的 data-* 属性 9 | clone.querySelectorAll('*').forEach(el => { 10 | Array.from(el.attributes).forEach(attr => { 11 | if (attr.name.startsWith('data-')) { 12 | el.removeAttribute(attr.name); 13 | } 14 | }); 15 | }); 16 | 17 | // 移除所有的 class 属性 18 | clone.querySelectorAll('*').forEach(el => { 19 | el.removeAttribute('class'); 20 | }); 21 | 22 | // 移除所有的 id 属性 23 | clone.querySelectorAll('*').forEach(el => { 24 | el.removeAttribute('id'); 25 | }); 26 | 27 | // 使用 XMLSerializer 安全地转换为字符串 28 | const serializer = new XMLSerializer(); 29 | return serializer.serializeToString(clone); 30 | } 31 | 32 | private static async processImages(container: HTMLElement): Promise { 33 | const images = container.querySelectorAll('img'); 34 | const imageArray = Array.from(images); 35 | 36 | for (const img of imageArray) { 37 | try { 38 | const response = await fetch(img.src); 39 | const blob = await response.blob(); 40 | const reader = new FileReader(); 41 | await new Promise((resolve, reject) => { 42 | reader.onload = () => { 43 | img.src = reader.result as string; 44 | resolve(null); 45 | }; 46 | reader.onerror = reject; 47 | reader.readAsDataURL(blob); 48 | }); 49 | } catch (error) { 50 | console.error('图片转换失败:', error); 51 | } 52 | } 53 | } 54 | 55 | public static async copyToClipboard(element: HTMLElement): Promise { 56 | try { 57 | const clone = element.cloneNode(true) as HTMLElement; 58 | await this.processImages(clone); 59 | 60 | const contentSection = clone.querySelector('.mp-content-section'); 61 | if (!contentSection) { 62 | throw new Error('找不到内容区域'); 63 | } 64 | // 使用新的 cleanupHtml 方法 65 | const cleanHtml = this.cleanupHtml(contentSection as HTMLElement); 66 | 67 | const clipData = new ClipboardItem({ 68 | 'text/html': new Blob([cleanHtml], { type: 'text/html' }), 69 | 'text/plain': new Blob([clone.textContent || ''], { type: 'text/plain' }) 70 | }); 71 | 72 | await navigator.clipboard.write([clipData]); 73 | new Notice('已复制到剪贴板'); 74 | } catch (error) { 75 | new Notice('复制失败'); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/donateManager.ts: -------------------------------------------------------------------------------- 1 | 2 | import { App, Plugin } from 'obsidian'; 3 | import { DONATE_QR } from './assets/donate'; 4 | import { QRCODE_QR } from './assets/qrcode'; 5 | 6 | export class DonateManager { 7 | private static overlay: HTMLElement; 8 | private static modal: HTMLElement; 9 | private static app: App; 10 | private static plugin: Plugin; 11 | 12 | public static initialize(app: App, plugin: Plugin) { 13 | this.app = app; 14 | this.plugin = plugin; 15 | } 16 | 17 | public static showDonateModal(container: HTMLElement) { 18 | this.overlay = container.createEl('div', { 19 | cls: 'mp-donate-overlay' 20 | }); 21 | 22 | this.modal = this.overlay.createEl('div', { 23 | cls: 'mp-about-modal' 24 | }); 25 | 26 | // 添加关闭按钮 27 | const closeButton = this.modal.createEl('button', { 28 | cls: 'mp-donate-close', 29 | text: '×' 30 | }); 31 | 32 | // 添加作者信息区域 33 | const authorSection = this.modal.createEl('div', { 34 | cls: 'mp-about-section mp-about-intro-section' 35 | }); 36 | 37 | authorSection.createEl('h4', { 38 | text: '关于作者', 39 | cls: 'mp-about-title' 40 | }); 41 | 42 | const introEl = authorSection.createEl('p', { 43 | cls: 'mp-about-intro' 44 | }); 45 | 46 | // 使用 createEl 替代 innerHTML 47 | introEl.createSpan({ text: '你好,我是' }); 48 | introEl.createSpan({ text: '【夜半】', cls: 'mp-about-name' }); 49 | introEl.createSpan({ text: ',一名' }); 50 | introEl.createSpan({ text: '全职写作与独立开发者', cls: 'mp-about-identity' }); 51 | introEl.createSpan({ text: '。' }); 52 | 53 | const roleList = authorSection.createEl('div', { 54 | cls: 'mp-about-roles' 55 | }); 56 | 57 | const roleEl = roleList.createEl('p', { 58 | cls: 'mp-about-role' 59 | }); 60 | 61 | // 使用 createEl 和 createSpan 替代 innerHTML 62 | roleEl.createSpan({ text: '这款插件是我为了在 Obsidian 写作后,' }); 63 | roleEl.createEl('br'); 64 | roleEl.createSpan({ text: '无需繁琐排版一键即可发布到公众号而开发的工具,' }); 65 | roleEl.createEl('br'); 66 | roleEl.createSpan({ text: '希望能让你的' }); 67 | roleEl.createSpan({ text: '排版更轻松', cls: 'mp-about-highlight' }); 68 | roleEl.createSpan({ text: ',让你的' }); 69 | roleEl.createSpan({ text: '创作更高效', cls: 'mp-about-value' }); 70 | roleEl.createSpan({ text: '。' }); 71 | 72 | // 添加插件介绍 73 | const descEl = authorSection.createEl('p', { 74 | cls: 'mp-about-desc' 75 | }); 76 | 77 | // 使用 createEl 替代 innerHTML 78 | descEl.createSpan({ text: '如果这款插件对你有帮助,' }); 79 | descEl.createEl('br'); 80 | descEl.createSpan({ text: '或者你愿意支持我的独立开发与写作,欢迎请我喝咖啡☕️。' }); 81 | descEl.createEl('br'); 82 | descEl.createSpan({ text: '你的支持来说意义重大,它能让我更专注地开发、写作。' }); 83 | 84 | // 添加打赏区域 85 | const donateSection = this.modal.createEl('div', { 86 | cls: 'mp-about-section mp-about-donate-section' 87 | }); 88 | 89 | donateSection.createEl('h4', { 90 | text: '请我喝咖啡', 91 | cls: 'mp-about-subtitle' 92 | }); 93 | 94 | const donateQR = donateSection.createEl('div', { 95 | cls: 'mp-about-qr' 96 | }); 97 | donateQR.createEl('img', { 98 | attr: { 99 | src: DONATE_QR, 100 | alt: '打赏二维码' 101 | } 102 | }); 103 | 104 | // 添加公众号区域 105 | const mpSection = this.modal.createEl('div', { 106 | cls: 'mp-about-section mp-about-mp-section' 107 | }); 108 | 109 | const mpDescEl = mpSection.createEl('p', { 110 | cls: 'mp-about-desc' 111 | }); 112 | 113 | // 使用 createEl 替代 innerHTML 114 | mpDescEl.createSpan({ text: '如果你想了解更多关于创作、效率工具的小技巧,' }); 115 | mpDescEl.createEl('br'); 116 | mpDescEl.createSpan({ text: '或者关注我未来的写作动态,欢迎关注我的微信公众号。' }); 117 | 118 | mpSection.createEl('h4', { 119 | text: '微信公众号', 120 | cls: 'mp-about-subtitle' 121 | }); 122 | 123 | const mpQR = mpSection.createEl('div', { 124 | cls: 'mp-about-qr' 125 | }); 126 | mpQR.createEl('img', { 127 | attr: { 128 | src: QRCODE_QR, 129 | alt: '公众号二维码' 130 | } 131 | }); 132 | 133 | const footerEl = mpSection.createEl('p', { 134 | cls: 'mp-about-footer' 135 | }); 136 | 137 | // 使用 createEl 替代 innerHTML 138 | footerEl.createSpan({ text: '期待与你一起,在创作的世界里' }); 139 | const strongText = footerEl.createEl('strong'); 140 | strongText.createSpan({ text: '找到属于自己的意义' }); 141 | footerEl.createSpan({ text: '。' }); 142 | 143 | // 添加关闭事件 144 | closeButton.addEventListener('click', () => this.closeDonateModal()); 145 | this.overlay.addEventListener('click', (e) => { 146 | if (e.target === this.overlay) { 147 | this.closeDonateModal(); 148 | } 149 | }); 150 | } 151 | 152 | private static closeDonateModal() { 153 | if (this.overlay) { 154 | this.overlay.remove(); 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, Notice } from 'obsidian'; 2 | import { MPView, VIEW_TYPE_MP } from './view'; 3 | import { TemplateManager } from './templateManager'; 4 | import { SettingsManager } from './settings/settings'; 5 | import { MPConverter } from './converter'; 6 | import { DonateManager } from './donateManager'; 7 | import { MPSettingTab } from './settings/MPSettingTab'; 8 | export default class MPPlugin extends Plugin { 9 | settingsManager: SettingsManager; 10 | templateManager: TemplateManager; 11 | async onload() { 12 | // 初始化设置管理器 13 | this.settingsManager = new SettingsManager(this); 14 | await this.settingsManager.loadSettings(); 15 | 16 | // 初始化模板管理器 17 | this.templateManager = new TemplateManager(this.app, this.settingsManager); 18 | 19 | // 初始化转换器 20 | MPConverter.initialize(this.app); 21 | 22 | DonateManager.initialize(this.app, this); 23 | 24 | // 注册视图 25 | this.registerView( 26 | VIEW_TYPE_MP, 27 | (leaf) => new MPView(leaf, this.templateManager, this.settingsManager) 28 | ); 29 | 30 | // 自动打开视图但不聚焦 31 | this.app.workspace.onLayoutReady(() => { 32 | const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_MP); 33 | if (leaves.length === 0) { 34 | const rightLeaf = this.app.workspace.getRightLeaf(false); 35 | if (rightLeaf) { 36 | rightLeaf.setViewState({ 37 | type: VIEW_TYPE_MP, 38 | active: false, 39 | }); 40 | } 41 | } 42 | }); 43 | 44 | // 添加命令到命令面板 45 | this.addCommand({ 46 | id: 'open-mp-preview', 47 | name: '打开公众号预览插件', 48 | callback: async () => { 49 | await this.activateView(); 50 | } 51 | }); 52 | 53 | // 在插件的 onload 方法中添加: 54 | this.addSettingTab(new MPSettingTab(this.app, this)); 55 | } 56 | 57 | async activateView() { 58 | // 如果视图已经存在,激活它 59 | const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_MP); 60 | if (leaves.length > 0) { 61 | this.app.workspace.revealLeaf(leaves[0]); 62 | return; 63 | } 64 | 65 | // 创建新视图 66 | const rightLeaf = this.app.workspace.getRightLeaf(false); 67 | if (rightLeaf) { 68 | await rightLeaf.setViewState({ 69 | type: VIEW_TYPE_MP, 70 | active: true, 71 | }); 72 | } else { 73 | // 如果无法获取右侧面板,显示错误提示 74 | new Notice('无法创建视图面板'); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /src/settings/ConfirmModal.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal, Setting } from 'obsidian'; 2 | 3 | export class ConfirmModal extends Modal { 4 | private message: string; 5 | private onConfirm: () => void; 6 | 7 | constructor(app: App, title: string, message: string, onConfirm: () => void) { 8 | super(app); 9 | this.titleEl.setText(title); 10 | this.message = message; 11 | this.onConfirm = onConfirm; 12 | } 13 | 14 | onOpen() { 15 | const { contentEl } = this; 16 | contentEl.createEl('p', { text: this.message }); 17 | 18 | new Setting(contentEl) 19 | .addButton(btn => btn 20 | .setButtonText('确认') 21 | .setCta() 22 | .onClick(() => { 23 | this.onConfirm(); 24 | this.close(); 25 | })) 26 | .addButton(btn => btn 27 | .setButtonText('取消') 28 | .onClick(() => this.close())); 29 | } 30 | 31 | onClose() { 32 | const { contentEl } = this; 33 | contentEl.empty(); 34 | } 35 | } -------------------------------------------------------------------------------- /src/settings/CreateBackgroundModal.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal, Setting, Notice } from 'obsidian'; 2 | import { Background } from '../backgroundManager'; 3 | import { nanoid } from '../utils/nanoid'; 4 | 5 | interface CssTemplate { 6 | name: string; 7 | style: string; 8 | settings: string[]; 9 | } 10 | 11 | export class CreateBackgroundModal extends Modal { 12 | private onSubmit: (background: Background) => void; 13 | private background: Background; 14 | private isEditing: boolean; 15 | 16 | // 背景属性 17 | private backgroundType: 'color' | 'css' = 'color'; 18 | private backgroundColor: string = '#f5f5f5'; 19 | private backgroundCssStyle: string = ''; 20 | private cssTemplateType: string = 'custom'; 21 | private patternColor: string = 'rgba(50, 0, 0, 0.03)'; 22 | private patternSize: number = 20; 23 | 24 | // 预设的CSS模板 25 | private cssTemplates: Record = { 26 | custom: { 27 | name: '自定义', 28 | style: '', 29 | settings: ['custom'] 30 | }, 31 | grid: { 32 | name: '网格', 33 | style: 'background-image: linear-gradient(90deg, rgba(50, 0, 0, 0.03) 2%, transparent 2%), linear-gradient(360deg, rgba(50, 0, 0, 0.03) 2%, transparent 2%); background-size: 20px 20px;', 34 | settings: ['color', 'size'] 35 | }, 36 | diagonalStripes: { 37 | name: '对角条纹', 38 | style: 'background-image: linear-gradient(135deg, rgba(50, 0, 0, 0.05) 25%, transparent 25%, transparent 50%, rgba(50, 0, 0, 0.05) 50%, rgba(50, 0, 0, 0.05) 75%, transparent 75%, transparent); background-size: 20px 20px;', 39 | settings: ['color', 'size'] 40 | }, 41 | polkaDots: { 42 | name: '波尔卡圆点', 43 | style: 'background-image: radial-gradient(circle, rgba(50, 0, 0, 0.05) 10%, transparent 10%); background-size: 20px 20px;', 44 | settings: ['color', 'size'] 45 | }, 46 | zigzag: { 47 | name: '锯齿形', 48 | style: 'background-image: linear-gradient(135deg, rgba(50, 0, 0, 0.05) 25%, transparent 25%, transparent 50%, rgba(50, 0, 0, 0.05) 50%, rgba(50, 0, 0, 0.05) 75%, transparent 75%, transparent); background-size: 20px 20px; background-position: 0 0, 10px 10px;', 49 | settings: ['color', 'size'] 50 | }, 51 | honeycomb: { 52 | name: '蜂窝', 53 | style: 'background-image: linear-gradient(30deg, rgba(50, 0, 0, 0.05) 12%, transparent 12%, transparent 50%, rgba(50, 0, 0, 0.05) 50%, rgba(50, 0, 0, 0.05) 62%, transparent 62%, transparent); background-size: 20px 35px;', 54 | settings: ['color', 'size'] 55 | }, 56 | wave: { 57 | name: '波浪', 58 | style: 'background-image: linear-gradient(45deg, rgba(50, 0, 0, 0.04) 12%, transparent 12%, transparent 88%, rgba(50, 0, 0, 0.04) 88%), linear-gradient(135deg, rgba(50, 0, 0, 0.04) 12%, transparent 12%, transparent 88%, rgba(50, 0, 0, 0.04) 88%); background-size: 30px 30px; background-position: 0 0, 0 0, 15px 15px, 15px 15px;', 59 | settings: ['color', 'size'] 60 | }, 61 | checkerboard: { 62 | name: '棋盘', 63 | style: 'background-image: linear-gradient(45deg, rgba(50, 0, 0, 0.04) 25%, transparent 25%), linear-gradient(-45deg, rgba(50, 0, 0, 0.04) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, rgba(50, 0, 0, 0.04) 75%), linear-gradient(-45deg, transparent 75%, rgba(50, 0, 0, 0.04) 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px;', 64 | settings: ['color', 'size'] 65 | } 66 | }; 67 | 68 | constructor( 69 | app: App, 70 | onSubmit: (background: Background) => void, 71 | background?: Background 72 | ) { 73 | super(app); 74 | this.onSubmit = onSubmit; 75 | this.isEditing = !!background; 76 | 77 | if (background) { 78 | this.background = { ...background }; 79 | // 从 style 中解析出类型和相关属性 80 | this.parseStyleToProperties(background.style); 81 | } else { 82 | this.background = { 83 | id: nanoid(), 84 | name: '', 85 | style: 'background-color: #f5f5f5;' 86 | }; 87 | this.backgroundType = 'color'; 88 | this.backgroundColor = '#f5f5f5'; 89 | } 90 | } 91 | 92 | // 从 style 字符串中解析出背景类型和相关属性 93 | private parseStyleToProperties(style: string) { 94 | if (style.includes('background-color:')) { 95 | this.backgroundType = 'color'; 96 | const colorMatch = style.match(/background-color: (#[a-fA-F0-9]+)/); 97 | if (colorMatch && colorMatch[1]) { 98 | this.backgroundColor = colorMatch[1]; 99 | } 100 | } else { 101 | this.backgroundType = 'css'; 102 | // 移除基本样式,保留自定义 CSS 103 | this.backgroundCssStyle = style.replace(/box-sizing: border-box; margin: 0; padding: 0;/, '').trim(); 104 | 105 | // 尝试匹配预设模板 106 | let matched = false; 107 | for (const [key, template] of Object.entries(this.cssTemplates)) { 108 | if (key === 'custom') continue; 109 | 110 | if (this.backgroundCssStyle === template.style) { 111 | this.cssTemplateType = key; 112 | matched = true; 113 | break; 114 | } 115 | } 116 | 117 | if (!matched) { 118 | this.cssTemplateType = 'custom'; 119 | } 120 | } 121 | } 122 | 123 | // 解析CSS样式到各个组件 124 | private parseCssToComponents() { 125 | // 提取颜色 126 | const colorMatch = this.backgroundCssStyle.match(/rgba?\([^)]+\)/); 127 | if (colorMatch) { 128 | this.patternColor = colorMatch[0]; 129 | } 130 | 131 | // 提取大小 132 | const sizeMatch = this.backgroundCssStyle.match(/background-size:\s*(\d+)px/); 133 | if (sizeMatch && sizeMatch[1]) { 134 | this.patternSize = parseInt(sizeMatch[1]); 135 | } 136 | } 137 | 138 | // 更新图案颜色(保持透明度不变) 139 | private updatePatternColor(hexColor: string) { 140 | const r = parseInt(hexColor.slice(1, 3), 16); 141 | const g = parseInt(hexColor.slice(3, 5), 16); 142 | const b = parseInt(hexColor.slice(5, 7), 16); 143 | 144 | // 提取当前透明度 145 | let alpha = 0.03; 146 | const alphaMatch = this.patternColor.match(/rgba\([^,]+,[^,]+,[^,]+,([^)]+)\)/); 147 | if (alphaMatch && alphaMatch[1]) { 148 | alpha = parseFloat(alphaMatch[1]); 149 | } 150 | 151 | this.patternColor = `rgba(${r}, ${g}, ${b}, ${alpha})`; 152 | } 153 | 154 | // 更新图案透明度(保持颜色不变) 155 | private updatePatternOpacity(opacity: number) { 156 | const colorMatch = this.patternColor.match(/rgba\(([^,]+),([^,]+),([^,]+),[^)]+\)/); 157 | if (colorMatch) { 158 | const [_, r, g, b] = colorMatch; 159 | this.patternColor = `rgba(${r}, ${g}, ${b}, ${opacity})`; 160 | } else { 161 | // 如果之前不是rgba格式,转换为rgba 162 | const r = parseInt(this.patternColor.slice(1, 3), 16); 163 | const g = parseInt(this.patternColor.slice(3, 5), 16); 164 | const b = parseInt(this.patternColor.slice(5, 7), 16); 165 | this.patternColor = `rgba(${r}, ${g}, ${b}, ${opacity})`; 166 | } 167 | } 168 | 169 | // 根据当前组件设置更新CSS样式 170 | private updateCssStyle() { 171 | if (this.cssTemplateType === 'custom') { 172 | return; // 自定义模式下不自动更新CSS 173 | } 174 | 175 | const template = this.cssTemplates[this.cssTemplateType]; 176 | if (!template) return; 177 | 178 | // 替换模板中的颜色和大小 179 | let style = template.style; 180 | 181 | // 替换所有颜色 182 | style = style.replace(/rgba\([^)]+\)/g, this.patternColor); 183 | 184 | // 替换所有大小 185 | style = style.replace(/(\d+)px/g, `${this.patternSize}px`); 186 | 187 | this.backgroundCssStyle = style; 188 | } 189 | 190 | // 根据模板类型更新设置项的可见性 191 | private updateSettingsVisibility(cssSection: HTMLElement, templateType: string) { 192 | const template = this.cssTemplates[templateType]; 193 | if (!template) return; 194 | 195 | // 获取所有可能的设置项容器 196 | const colorSettingContainer = cssSection.querySelector('.pattern-color-setting'); 197 | const sizeSettingContainer = cssSection.querySelector('.pattern-size-setting'); 198 | const customCssContainer = cssSection.querySelector('.custom-css-container'); 199 | 200 | // 根据模板需要的设置项显示或隐藏 201 | if (colorSettingContainer) { 202 | colorSettingContainer.toggleClass('is-hidden', !template.settings.includes('color')); 203 | } 204 | 205 | if (sizeSettingContainer) { 206 | sizeSettingContainer.toggleClass('is-hidden', !template.settings.includes('size')); 207 | } 208 | 209 | if (customCssContainer) { 210 | customCssContainer.toggleClass('is-hidden', !template.settings.includes('custom')); 211 | 212 | // 如果显示自定义CSS,同时显示文本区域 213 | const customCssTextArea = cssSection.querySelector('.custom-css-textarea'); 214 | if (customCssTextArea && template.settings.includes('custom')) { 215 | customCssTextArea.toggleClass('is-hidden', false); 216 | } 217 | } 218 | } 219 | 220 | // 更新类型特定设置的可见性 221 | private updateTypeSpecificSettings() { 222 | const colorSection = this.contentEl.querySelector('.background-color-section'); 223 | const cssSection = this.contentEl.querySelector('.background-css-section'); 224 | 225 | if (colorSection && cssSection) { 226 | colorSection.toggleClass('is-hidden', this.backgroundType !== 'color'); 227 | cssSection.toggleClass('is-hidden', this.backgroundType !== 'css'); 228 | 229 | // 如果切换到CSS背景类型,根据当前模板更新设置项可见性 230 | if (this.backgroundType === 'css') { 231 | this.updateSettingsVisibility(cssSection as HTMLElement, this.cssTemplateType); 232 | } 233 | } 234 | 235 | const previewEl = this.contentEl.querySelector('.background-preview'); 236 | if (previewEl) { 237 | this.updatePreview(previewEl as HTMLElement); 238 | } 239 | } 240 | 241 | // 更新预览区域 242 | private updatePreview(previewEl?: HTMLElement) { 243 | const el = previewEl || this.contentEl.querySelector('.background-preview'); 244 | if (!el) return; 245 | 246 | let style = ''; 247 | switch (this.backgroundType) { 248 | case 'color': 249 | style = `background-color: ${this.backgroundColor};`; 250 | break; 251 | case 'css': 252 | style = this.backgroundCssStyle; 253 | break; 254 | } 255 | 256 | el.setAttribute('style', style); 257 | } 258 | 259 | // 生成最终样式 260 | private generateStyle() { 261 | let style = 'box-sizing: border-box; margin: 0; padding: 0; '; 262 | 263 | switch (this.backgroundType) { 264 | case 'color': 265 | style += `background-color: ${this.backgroundColor};`; 266 | break; 267 | case 'css': 268 | style += this.backgroundCssStyle; 269 | break; 270 | } 271 | 272 | this.background.style = style; 273 | } 274 | 275 | // 表单验证 276 | private validateForm(): boolean { 277 | if (!this.background.name) { 278 | new Notice('请输入背景名称'); 279 | return false; 280 | } 281 | 282 | switch (this.backgroundType) { 283 | case 'color': 284 | if (!this.backgroundColor) { 285 | new Notice('请选择背景颜色'); 286 | return false; 287 | } 288 | break; 289 | case 'css': 290 | if (!this.backgroundCssStyle) { 291 | new Notice('请输入CSS样式'); 292 | return false; 293 | } 294 | break; 295 | } 296 | 297 | return true; 298 | } 299 | 300 | // 打开模态框时的处理 301 | onOpen() { 302 | const { contentEl } = this; 303 | contentEl.empty(); 304 | contentEl.addClass('mp-background-modal'); 305 | 306 | // 标题 307 | contentEl.createEl('h2', { text: this.isEditing ? '编辑背景' : '创建新背景' }); 308 | 309 | // 基本信息 310 | const basicSection = contentEl.createDiv('background-basic-section'); 311 | 312 | // 背景名称 313 | new Setting(basicSection) 314 | .setName('背景名称') 315 | .setDesc('输入背景的名称') 316 | .addText(text => { 317 | text.setValue(this.background.name || '') 318 | .onChange(value => { 319 | this.background.name = value; 320 | }); 321 | }); 322 | 323 | // 背景类型 324 | new Setting(basicSection) 325 | .setName('背景类型') 326 | .setDesc('选择背景的类型') 327 | .addDropdown(dropdown => { 328 | dropdown 329 | .addOption('color', '纯色背景') 330 | .addOption('css', 'CSS背景图案') 331 | .setValue(this.backgroundType) 332 | .onChange(value => { 333 | this.backgroundType = value as 'color' | 'css'; 334 | this.updateTypeSpecificSettings(); 335 | }); 336 | }); 337 | 338 | // 类型特定设置容器 339 | const typeSpecificSection = contentEl.createDiv('background-type-specific-section'); 340 | 341 | // 纯色背景设置 342 | const colorSection = typeSpecificSection.createDiv('background-color-section'); 343 | new Setting(colorSection) 344 | .setName('背景颜色') 345 | .setDesc('选择背景的颜色') 346 | .addColorPicker(color => { 347 | color.setValue(this.backgroundColor) 348 | .onChange(value => { 349 | this.backgroundColor = value; 350 | this.updatePreview(); 351 | }); 352 | }); 353 | 354 | // CSS背景图案设置 355 | const cssSection = typeSpecificSection.createDiv('background-css-section'); 356 | 357 | // 添加模板选择下拉框 358 | new Setting(cssSection) 359 | .setName('背景模板') 360 | .setDesc('选择预设的背景模板') 361 | .addDropdown(dropdown => { 362 | for (const [key, template] of Object.entries(this.cssTemplates)) { 363 | dropdown.addOption(key, template.name); 364 | } 365 | dropdown.setValue(this.cssTemplateType) 366 | .onChange(value => { 367 | this.cssTemplateType = value; 368 | if (value !== 'custom') { 369 | this.backgroundCssStyle = this.cssTemplates[value].style; 370 | 371 | // 解析CSS样式到各个组件 372 | this.parseCssToComponents(); 373 | 374 | // 更新UI控件以反映当前模板的设置 375 | const colorPicker = cssSection.querySelector('input[type="color"]'); 376 | const opacitySlider = cssSection.querySelector('.slider'); 377 | const sizeSlider = cssSection.querySelectorAll('.slider')[1]; 378 | 379 | if (colorPicker) { 380 | // 从rgba提取十六进制颜色 381 | const rgbaMatch = this.patternColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); 382 | if (rgbaMatch) { 383 | const [_, r, g, b] = rgbaMatch; 384 | const hexColor = '#' + 385 | parseInt(r).toString(16).padStart(2, '0') + 386 | parseInt(g).toString(16).padStart(2, '0') + 387 | parseInt(b).toString(16).padStart(2, '0'); 388 | (colorPicker as HTMLInputElement).value = hexColor; 389 | } 390 | } 391 | 392 | if (opacitySlider) { 393 | const opacityMatch = this.patternColor.match(/rgba\([^,]+,[^,]+,[^,]+,([^)]+)\)/); 394 | if (opacityMatch && opacityMatch[1]) { 395 | const opacity = parseFloat(opacityMatch[1]) * 100; 396 | // 使用类型断言来访问__component__属性 397 | const sliderComponent = (opacitySlider.parentElement as any).__component__; 398 | if (sliderComponent && typeof sliderComponent.setValue === 'function') { 399 | sliderComponent.setValue(opacity); 400 | } 401 | } 402 | } 403 | 404 | if (sizeSlider) { 405 | // 使用类型断言来访问__component__属性 406 | const sliderComponent = (sizeSlider.parentElement as any).__component__; 407 | if (sliderComponent && typeof sliderComponent.setValue === 'function') { 408 | sliderComponent.setValue(this.patternSize); 409 | } 410 | } 411 | } 412 | 413 | // 更新自定义CSS区域的可见性 414 | const customCssContainer = cssSection.querySelector('.custom-css-container'); 415 | const customCssTextArea = cssSection.querySelector('.custom-css-textarea'); 416 | if (customCssContainer && customCssTextArea) { 417 | const isCustom = value === 'custom'; 418 | customCssTextArea.toggleClass('is-hidden', !isCustom); 419 | } 420 | 421 | // 更新CSS设置项的可见性 422 | this.updateSettingsVisibility(cssSection, value); 423 | 424 | this.updatePreview(); 425 | }); 426 | }); 427 | 428 | // 背景颜色设置 429 | const colorSettingContainer = cssSection.createDiv('pattern-color-setting'); 430 | new Setting(colorSettingContainer) 431 | .setName('图案颜色') 432 | .setDesc('设置背景图案的颜色') 433 | .addColorPicker(color => { 434 | // 从rgba提取十六进制颜色 435 | const rgbaMatch = this.patternColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); 436 | let hexColor = '#320000'; // 默认颜色 437 | 438 | if (rgbaMatch) { 439 | const [_, r, g, b] = rgbaMatch; 440 | hexColor = '#' + 441 | parseInt(r).toString(16).padStart(2, '0') + 442 | parseInt(g).toString(16).padStart(2, '0') + 443 | parseInt(b).toString(16).padStart(2, '0'); 444 | } 445 | 446 | color.setValue(hexColor) 447 | .onChange(value => { 448 | // 更新颜色但保持透明度 449 | this.updatePatternColor(value); 450 | this.updateCssStyle(); 451 | this.updatePreview(); 452 | }); 453 | }) 454 | .addSlider(slider => { 455 | // 提取当前透明度 456 | let opacity = 3; // 默认透明度3% 457 | const alphaMatch = this.patternColor.match(/rgba\([^,]+,[^,]+,[^,]+,([^)]+)\)/); 458 | if (alphaMatch && alphaMatch[1]) { 459 | opacity = parseFloat(alphaMatch[1]) * 100; 460 | } 461 | 462 | slider.setLimits(0, 100, 1) 463 | .setValue(opacity) 464 | .setDynamicTooltip() 465 | .onChange(value => { 466 | // 更新透明度但保持颜色 467 | this.updatePatternOpacity(value / 100); 468 | this.updateCssStyle(); 469 | this.updatePreview(); 470 | }); 471 | }); 472 | 473 | // 图案大小设置 474 | const sizeSettingContainer = cssSection.createDiv('pattern-size-setting'); 475 | new Setting(sizeSettingContainer) 476 | .setName('图案大小') 477 | .setDesc('设置背景图案的大小') 478 | .addSlider(slider => { 479 | slider.setLimits(5, 50, 1) 480 | .setValue(this.patternSize) // 使用当前模板的大小 481 | .setDynamicTooltip() 482 | .onChange(value => { 483 | this.patternSize = value; 484 | this.updateCssStyle(); 485 | this.updatePreview(); 486 | }); 487 | }); 488 | 489 | // 自定义CSS容器 490 | const customCssContainer = cssSection.createDiv('custom-css-container'); 491 | const customCssTextArea = customCssContainer.createDiv('custom-css-textarea'); 492 | customCssTextArea.toggleClass('is-hidden', this.cssTemplateType !== 'custom'); 493 | 494 | new Setting(customCssTextArea) 495 | .setName('CSS代码') 496 | .setDesc('直接输入CSS样式代码') 497 | .addTextArea(text => { 498 | text.setValue(this.backgroundCssStyle) 499 | .onChange(value => { 500 | this.backgroundCssStyle = value; 501 | this.updatePreview(); 502 | }); 503 | text.inputEl.rows = 10; 504 | text.inputEl.cols = 50; 505 | }); 506 | 507 | // 背景预览 508 | const previewSection = contentEl.createDiv('background-preview-section'); 509 | previewSection.createEl('h3', { text: '预览' }); 510 | const previewEl = previewSection.createDiv('background-preview'); 511 | previewEl.createSpan({ text: '预览效果' }); 512 | this.updatePreview(previewEl); 513 | 514 | // 按钮区域 515 | const buttonSection = contentEl.createDiv('background-button-section'); 516 | new Setting(buttonSection) 517 | .addButton(btn => { 518 | btn.setButtonText('取消') 519 | .onClick(() => { 520 | this.close(); 521 | }); 522 | }) 523 | .addButton(btn => { 524 | btn.setButtonText(this.isEditing ? '保存' : '创建') 525 | .setCta() 526 | .onClick(() => { 527 | if (!this.validateForm()) { 528 | return; 529 | } 530 | this.generateStyle(); 531 | this.onSubmit(this.background); 532 | this.close(); 533 | }); 534 | }); 535 | 536 | // 初始化类型特定设置 537 | this.updateTypeSpecificSettings(); 538 | } 539 | 540 | // 关闭模态框时的处理 541 | onClose() { 542 | const { contentEl } = this; 543 | contentEl.empty(); 544 | } 545 | } -------------------------------------------------------------------------------- /src/settings/CreateFontModal.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal, Setting, setIcon } from 'obsidian'; 2 | 3 | export class CreateFontModal extends Modal { 4 | private font: { value: string; label: string; isPreset?: boolean }; 5 | private onSubmit: (font: { value: string; label: string }) => void; 6 | 7 | constructor( 8 | app: App, 9 | onSubmit: (font: { value: string; label: string }) => void, 10 | existingFont?: { value: string; label: string; isPreset?: boolean } 11 | ) { 12 | super(app); 13 | this.onSubmit = onSubmit; 14 | this.font = existingFont ?? { value: '', label: '' }; 15 | } 16 | 17 | onOpen() { 18 | const { contentEl } = this; 19 | contentEl.empty(); 20 | contentEl.addClass('mp-font-modal'); 21 | 22 | // 修改标题容器结构 23 | const headerContainer = contentEl.createDiv({ cls: 'mfd-header' }); 24 | headerContainer.createEl('h3', { text: this.font.label ? '编辑字体' : '添加字体' }); 25 | 26 | // 帮助按钮容器 27 | const helpBtnContainer = headerContainer.createDiv({ cls: 'mfd-help-trigger' }); 28 | const helpBtn = helpBtnContainer.createEl('button', { cls: 'mfd-help-btn' }); 29 | setIcon(helpBtn, 'help-circle'); 30 | 31 | // 提示框 32 | const helpTooltip = helpBtnContainer.createDiv({ cls: 'mfd-help-tooltip' }); 33 | helpTooltip.setText(`👋 字体值设置说明 34 | • 单个字体:Arial 或 "Microsoft YaHei" 35 | • 中文字体:需要同时设置中英文名称 36 | • 字体族:添加 serif/sans-serif 37 | • 多个字体用逗号分隔 38 | 示例 39 | • 宋体:SimSun, "宋体", serif 40 | • 微软雅黑:"Microsoft YaHei", "微软雅黑", sans-serif`); 41 | 42 | new Setting(contentEl) 43 | .setName('字体名称') 44 | .setDesc('显示在下拉菜单中的名称') 45 | .addText(text => text 46 | .setValue(this.font.label) 47 | .onChange(value => this.font.label = value)); 48 | 49 | new Setting(contentEl) 50 | .setName('字体值') 51 | .setDesc('CSS font-family 的值') 52 | .addText(text => text 53 | .setValue(this.font.value) 54 | .onChange(value => this.font.value = value)) 55 | 56 | new Setting(contentEl) 57 | .addButton(btn => btn 58 | .setButtonText('确定') 59 | .setCta() 60 | .onClick(() => { 61 | if (!this.font.label || !this.font.value) { 62 | return; 63 | } 64 | this.onSubmit(this.font); 65 | this.close(); 66 | })) 67 | .addButton(btn => btn 68 | .setButtonText('取消') 69 | .onClick(() => this.close())); 70 | } 71 | 72 | onClose() { 73 | const { contentEl } = this; 74 | contentEl.empty(); 75 | } 76 | } -------------------------------------------------------------------------------- /src/settings/MPSettingTab.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab, Setting, setIcon, Notice } from 'obsidian'; 2 | import MPPlugin from '../main'; // 修改插件名以匹配类名 3 | import { CreateTemplateModal } from './CreateTemplateModal'; 4 | import { CreateFontModal } from './CreateFontModal'; 5 | import { CreateBackgroundModal } from './CreateBackgroundModal'; // 添加导入 6 | import { ConfirmModal } from './ConfirmModal'; 7 | import { TemplatePreviewModal } from './templatePreviewModal'; // 添加导入 8 | export class MPSettingTab extends PluginSettingTab { 9 | plugin: MPPlugin; // 修改插件类型以匹配类名 10 | private expandedSections: Set = new Set(); 11 | 12 | constructor(app: App, plugin: MPPlugin) { // 修改插件类型以匹配类名 13 | super(app, plugin); 14 | this.plugin = plugin; 15 | } 16 | 17 | private createSection(containerEl: HTMLElement, title: string, renderContent: (contentEl: HTMLElement) => void) { 18 | const section = containerEl.createDiv('settings-section'); 19 | const header = section.createDiv('settings-section-header'); 20 | 21 | const toggle = header.createSpan('settings-section-toggle'); 22 | setIcon(toggle, 'chevron-right'); 23 | 24 | header.createEl('h4', { text: title }); 25 | 26 | const content = section.createDiv('settings-section-content'); 27 | renderContent(content); 28 | 29 | header.addEventListener('click', () => { 30 | const isExpanded = !section.hasClass('is-expanded'); 31 | section.toggleClass('is-expanded', isExpanded); 32 | setIcon(toggle, isExpanded ? 'chevron-down' : 'chevron-right'); 33 | if (isExpanded) { 34 | this.expandedSections.add(title); 35 | } else { 36 | this.expandedSections.delete(title); 37 | } 38 | }); 39 | 40 | if (this.expandedSections.has(title) || (!containerEl.querySelector('.settings-section'))) { 41 | section.addClass('is-expanded'); 42 | setIcon(toggle, 'chevron-down'); 43 | this.expandedSections.add(title); 44 | } 45 | 46 | return section; 47 | } 48 | 49 | display(): void { 50 | const { containerEl } = this; 51 | containerEl.empty(); 52 | containerEl.addClass('mp-settings'); 53 | 54 | containerEl.createEl('h2', { text: 'MP Preview' }); 55 | 56 | this.createSection(containerEl, '基本选项', el => this.renderBasicSettings(el)); 57 | this.createSection(containerEl, '模板选项', el => this.renderTemplateSettings(el)); 58 | this.createSection(containerEl, '背景选项', el => this.renderBackgroundSettings(el)); 59 | } 60 | 61 | private renderBasicSettings(containerEl: HTMLElement): void { 62 | // 字体管理区域 63 | const fontSection = containerEl.createDiv('mp-settings-subsection'); 64 | const fontHeader = fontSection.createDiv('mp-settings-subsection-header'); 65 | const fontToggle = fontHeader.createSpan('mp-settings-subsection-toggle'); 66 | setIcon(fontToggle, 'chevron-right'); 67 | 68 | fontHeader.createEl('h3', { text: '字体管理' }); 69 | 70 | const fontContent = fontSection.createDiv('mp-settings-subsection-content'); 71 | 72 | // 折叠/展开逻辑 73 | fontHeader.addEventListener('click', () => { 74 | const isExpanded = !fontSection.hasClass('is-expanded'); 75 | fontSection.toggleClass('is-expanded', isExpanded); 76 | setIcon(fontToggle, isExpanded ? 'chevron-down' : 'chevron-right'); 77 | }); 78 | 79 | // 字体列表 80 | const fontList = fontContent.createDiv('font-management'); 81 | this.plugin.settingsManager.getFontOptions().forEach(font => { 82 | const fontItem = fontList.createDiv('font-item'); 83 | const setting = new Setting(fontItem) 84 | .setName(font.label) 85 | .setDesc(font.value); 86 | 87 | // 只为非预设字体添加编辑和删除按钮 88 | if (!font.isPreset) { 89 | setting 90 | .addExtraButton(btn => 91 | btn.setIcon('pencil') 92 | .setTooltip('编辑') 93 | .onClick(() => { 94 | new CreateFontModal( 95 | this.app, 96 | async (updatedFont) => { 97 | await this.plugin.settingsManager.updateFont(font.value, updatedFont); 98 | this.display(); 99 | new Notice('请重启 Obsidian 或重新加载以使更改生效'); 100 | }, 101 | font 102 | ).open(); 103 | })) 104 | .addExtraButton(btn => 105 | btn.setIcon('trash') 106 | .setTooltip('删除') 107 | .onClick(() => { 108 | // 新增确认模态框 109 | new ConfirmModal( 110 | this.app, 111 | '确认删除字体', 112 | `确定要删除「${font.label}」字体配置吗?`, 113 | async () => { 114 | await this.plugin.settingsManager.removeFont(font.value); 115 | this.display(); 116 | new Notice('请重启 Obsidian 或重新加载以使更改生效'); 117 | } 118 | ).open(); 119 | })); 120 | } 121 | }); 122 | 123 | // 添加新字体按钮 124 | new Setting(fontContent) 125 | .addButton(btn => btn 126 | .setButtonText('+ 添加字体') 127 | .setCta() 128 | .onClick(() => { 129 | new CreateFontModal( 130 | this.app, 131 | async (newFont) => { 132 | await this.plugin.settingsManager.addCustomFont(newFont); 133 | this.display(); 134 | new Notice('请重启 Obsidian 或重新加载以使更改生效'); 135 | } 136 | ).open(); 137 | })); 138 | } 139 | 140 | private renderTemplateSettings(containerEl: HTMLElement): void { 141 | // 模板显示设置部分 - 从基本设置移动到这里 142 | const templateVisibilitySection = containerEl.createDiv('mp-settings-subsection'); 143 | const templateVisibilityHeader = templateVisibilitySection.createDiv('mp-settings-subsection-header'); 144 | 145 | const templateVisibilityToggle = templateVisibilityHeader.createSpan('mp-settings-subsection-toggle'); 146 | setIcon(templateVisibilityToggle, 'chevron-right'); 147 | 148 | templateVisibilityHeader.createEl('h3', { text: '模板显示选项' }); 149 | 150 | const templateVisibilityContent = templateVisibilitySection.createDiv('mp-settings-subsection-content'); 151 | 152 | // 折叠/展开逻辑 153 | templateVisibilityHeader.addEventListener('click', () => { 154 | const isExpanded = !templateVisibilitySection.hasClass('is-expanded'); 155 | templateVisibilitySection.toggleClass('is-expanded', isExpanded); 156 | setIcon(templateVisibilityToggle, isExpanded ? 'chevron-down' : 'chevron-right'); 157 | }); 158 | 159 | // 模板选择容器 160 | const templateSelectionContainer = templateVisibilityContent.createDiv('template-selection-container'); 161 | 162 | // 左侧:所有模板列表 163 | const allTemplatesContainer = templateSelectionContainer.createDiv('all-templates-container'); 164 | allTemplatesContainer.createEl('h4', { text: '隐藏模板' }); 165 | const allTemplatesList = allTemplatesContainer.createDiv('templates-list'); 166 | 167 | // 中间:控制按钮 168 | const controlButtonsContainer = templateSelectionContainer.createDiv('control-buttons-container'); 169 | const addButton = controlButtonsContainer.createEl('button', { text: '>' }); 170 | const removeButton = controlButtonsContainer.createEl('button', { text: '<' }); 171 | 172 | // 右侧:显示的模板列表 173 | const visibleTemplatesContainer = templateSelectionContainer.createDiv('visible-templates-container'); 174 | visibleTemplatesContainer.createEl('h4', { text: '显示模板' }); 175 | const visibleTemplatesList = visibleTemplatesContainer.createDiv('templates-list'); 176 | 177 | // 获取所有模板 178 | const allTemplates = this.plugin.settingsManager.getAllTemplates(); 179 | 180 | // 渲染模板列表 181 | const renderTemplateLists = () => { 182 | // 清空列表 183 | allTemplatesList.empty(); 184 | visibleTemplatesList.empty(); 185 | 186 | // 填充左侧列表(所有未显示的模板) 187 | allTemplates 188 | .filter(template => template.isVisible === false) 189 | .forEach(template => { 190 | const templateItem = allTemplatesList.createDiv('template-list-item'); 191 | templateItem.textContent = template.name; 192 | templateItem.dataset.templateId = template.id; 193 | 194 | // 点击选中/取消选中 195 | templateItem.addEventListener('click', () => { 196 | templateItem.toggleClass('selected', !templateItem.hasClass('selected')); 197 | }); 198 | }); 199 | 200 | // 填充右侧列表(所有显示的模板) 201 | allTemplates 202 | .filter(template => template.isVisible !== false) // 默认显示 203 | .forEach(template => { 204 | const templateItem = visibleTemplatesList.createDiv('template-list-item'); 205 | templateItem.textContent = template.name; 206 | templateItem.dataset.templateId = template.id; 207 | 208 | // 点击选中/取消选中 209 | templateItem.addEventListener('click', () => { 210 | templateItem.toggleClass('selected', !templateItem.hasClass('selected')); 211 | }); 212 | }); 213 | }; 214 | 215 | // 初始渲染 216 | renderTemplateLists(); 217 | 218 | // 添加按钮事件 219 | addButton.addEventListener('click', async () => { 220 | const selectedItems = Array.from(allTemplatesList.querySelectorAll('.template-list-item.selected')); 221 | if (selectedItems.length === 0) return; 222 | 223 | for (const item of selectedItems) { 224 | const templateId = (item as HTMLElement).dataset.templateId; 225 | if (!templateId) continue; 226 | 227 | const template = allTemplates.find(t => t.id === templateId); 228 | if (template) { 229 | template.isVisible = true; 230 | await this.plugin.settingsManager.updateTemplate(templateId, template); 231 | } 232 | } 233 | 234 | renderTemplateLists(); 235 | new Notice('请重启 Obsidian 或重新加载以使更改生效'); 236 | }); 237 | 238 | // 移除按钮事件 239 | removeButton.addEventListener('click', async () => { 240 | const selectedItems = Array.from(visibleTemplatesList.querySelectorAll('.template-list-item.selected')); 241 | if (selectedItems.length === 0) return; 242 | 243 | for (const item of selectedItems) { 244 | const templateId = (item as HTMLElement).dataset.templateId; 245 | if (!templateId) continue; 246 | 247 | const template = allTemplates.find(t => t.id === templateId); 248 | if (template) { 249 | template.isVisible = false; 250 | await this.plugin.settingsManager.updateTemplate(templateId, template); 251 | } 252 | } 253 | 254 | renderTemplateLists(); 255 | new Notice('请重启 Obsidian 或重新加载以使更改生效'); 256 | }); 257 | 258 | // 模板管理区域 259 | const templateList = containerEl.createDiv('template-management'); 260 | // 渲染自定义模板 261 | templateList.createEl('h4', { text: '自定义模板', cls: 'template-custom-header' }); 262 | this.plugin.settingsManager.getAllTemplates() 263 | .filter(template => !template.isPreset) 264 | .forEach(template => { 265 | const templateItem = templateList.createDiv('template-item'); 266 | new Setting(templateItem) 267 | .setName(template.name) 268 | .setDesc(template.description) 269 | .addExtraButton(btn => 270 | btn.setIcon('eye') 271 | .setTooltip('预览') 272 | .onClick(() => { 273 | new TemplatePreviewModal(this.app, template, this.plugin.templateManager).open(); // 修改为使用预览模态框 274 | })) 275 | .addExtraButton(btn => 276 | btn.setIcon('pencil') 277 | .setTooltip('编辑') 278 | .onClick(() => { 279 | new CreateTemplateModal( 280 | this.app, 281 | this.plugin, 282 | (updatedTemplate) => { 283 | this.plugin.settingsManager.updateTemplate(template.id, updatedTemplate); 284 | this.display(); 285 | new Notice('请重启 Obsidian 或重新加载以使更改生效'); 286 | }, 287 | template 288 | ).open(); 289 | })) 290 | .addExtraButton(btn => 291 | btn.setIcon('trash') 292 | .setTooltip('删除') 293 | .onClick(() => { 294 | // 新增确认模态框 295 | new ConfirmModal( 296 | this.app, 297 | '确认删除模板', 298 | `确定要删除「${template.name}」模板吗?此操作不可恢复。`, 299 | async () => { 300 | await this.plugin.settingsManager.removeTemplate(template.id); 301 | this.display(); 302 | new Notice('请重启 Obsidian 或重新加载以使更改生效'); 303 | } 304 | ).open(); 305 | })); 306 | }); 307 | 308 | // 添加新模板按钮 309 | new Setting(containerEl) 310 | .addButton(btn => btn 311 | .setButtonText('+ 新建模板') 312 | .setCta() 313 | .onClick(() => { 314 | new CreateTemplateModal( 315 | this.app, 316 | this.plugin, 317 | async (newTemplate) => { 318 | await this.plugin.settingsManager.addCustomTemplate(newTemplate); 319 | this.display(); 320 | new Notice('请重启 Obsidian 或重新加载以使更改生效'); 321 | } 322 | ).open(); 323 | })); 324 | } 325 | 326 | private renderBackgroundSettings(containerEl: HTMLElement): void { 327 | // 背景显示设置部分 328 | const backgroundVisibilitySection = containerEl.createDiv('mp-settings-subsection'); 329 | const backgroundVisibilityHeader = backgroundVisibilitySection.createDiv('mp-settings-subsection-header'); 330 | 331 | const backgroundVisibilityToggle = backgroundVisibilityHeader.createSpan('mp-settings-subsection-toggle'); 332 | setIcon(backgroundVisibilityToggle, 'chevron-right'); 333 | 334 | backgroundVisibilityHeader.createEl('h3', { text: '背景显示' }); 335 | 336 | const backgroundVisibilityContent = backgroundVisibilitySection.createDiv('mp-settings-subsection-content'); 337 | 338 | // 折叠/展开逻辑 339 | backgroundVisibilityHeader.addEventListener('click', () => { 340 | const isExpanded = !backgroundVisibilitySection.hasClass('is-expanded'); 341 | backgroundVisibilitySection.toggleClass('is-expanded', isExpanded); 342 | setIcon(backgroundVisibilityToggle, isExpanded ? 'chevron-down' : 'chevron-right'); 343 | }); 344 | 345 | // 背景选择容器 346 | const backgroundSelectionContainer = backgroundVisibilityContent.createDiv('background-selection-container'); 347 | 348 | // 左侧:所有背景列表 349 | const allBackgroundsContainer = backgroundSelectionContainer.createDiv('all-backgrounds-container'); 350 | allBackgroundsContainer.createEl('h4', { text: '隐藏背景' }); 351 | const allBackgroundsList = allBackgroundsContainer.createDiv('backgrounds-list'); 352 | 353 | // 中间:控制按钮 354 | const controlButtonsContainer = backgroundSelectionContainer.createDiv('control-buttons-container'); 355 | const addButton = controlButtonsContainer.createEl('button', { text: '>' }); 356 | const removeButton = controlButtonsContainer.createEl('button', { text: '<' }); 357 | 358 | // 右侧:显示的背景列表 359 | const visibleBackgroundsContainer = backgroundSelectionContainer.createDiv('visible-backgrounds-container'); 360 | visibleBackgroundsContainer.createEl('h4', { text: '显示背景' }); 361 | const visibleBackgroundsList = visibleBackgroundsContainer.createDiv('backgrounds-list'); 362 | 363 | // 获取所有背景 364 | const allBackgrounds = this.plugin.settingsManager.getAllBackgrounds(); 365 | 366 | // 渲染背景列表 367 | const renderBackgroundLists = () => { 368 | // 清空列表 369 | allBackgroundsList.empty(); 370 | visibleBackgroundsList.empty(); 371 | 372 | // 填充左侧列表(所有未显示的背景) 373 | allBackgrounds 374 | .filter(background => background.isVisible === false) 375 | .forEach(background => { 376 | const backgroundItem = allBackgroundsList.createDiv('background-list-item'); 377 | backgroundItem.textContent = background.name; 378 | backgroundItem.dataset.backgroundId = background.id; 379 | 380 | // 点击选中/取消选中 381 | backgroundItem.addEventListener('click', () => { 382 | backgroundItem.toggleClass('selected', !backgroundItem.hasClass('selected')); 383 | }); 384 | }); 385 | 386 | // 填充右侧列表(所有显示的背景) 387 | allBackgrounds 388 | .filter(background => background.isVisible !== false) // 默认显示 389 | .forEach(background => { 390 | const backgroundItem = visibleBackgroundsList.createDiv('background-list-item'); 391 | backgroundItem.textContent = background.name; 392 | backgroundItem.dataset.backgroundId = background.id; 393 | 394 | // 点击选中/取消选中 395 | backgroundItem.addEventListener('click', () => { 396 | backgroundItem.toggleClass('selected', !backgroundItem.hasClass('selected')); 397 | }); 398 | }); 399 | }; 400 | 401 | // 初始渲染 402 | renderBackgroundLists(); 403 | 404 | // 添加按钮事件 405 | addButton.addEventListener('click', async () => { 406 | const selectedItems = Array.from(allBackgroundsList.querySelectorAll('.background-list-item.selected')); 407 | if (selectedItems.length === 0) return; 408 | 409 | for (const item of selectedItems) { 410 | const backgroundId = (item as HTMLElement).dataset.backgroundId; 411 | if (!backgroundId) continue; 412 | 413 | const background = allBackgrounds.find(b => b.id === backgroundId); 414 | if (background) { 415 | background.isVisible = true; 416 | await this.plugin.settingsManager.updateBackground(backgroundId, background); 417 | } 418 | } 419 | 420 | renderBackgroundLists(); 421 | new Notice('背景显示设置已更新'); 422 | }); 423 | 424 | // 移除按钮事件 425 | removeButton.addEventListener('click', async () => { 426 | const selectedItems = Array.from(visibleBackgroundsList.querySelectorAll('.background-list-item.selected')); 427 | if (selectedItems.length === 0) return; 428 | 429 | for (const item of selectedItems) { 430 | const backgroundId = (item as HTMLElement).dataset.backgroundId; 431 | if (!backgroundId) continue; 432 | 433 | const background = allBackgrounds.find(b => b.id === backgroundId); 434 | if (background) { 435 | background.isVisible = false; 436 | await this.plugin.settingsManager.updateBackground(backgroundId, background); 437 | } 438 | } 439 | 440 | renderBackgroundLists(); 441 | new Notice('背景显示已更新'); 442 | }); 443 | 444 | // 背景管理区域 445 | const backgroundList = containerEl.createDiv('background-management'); 446 | 447 | // 渲染自定义背景 448 | backgroundList.createEl('h4', { text: '自定义背景', cls: 'background-custom-header' }); 449 | this.plugin.settingsManager.getAllBackgrounds() 450 | .filter(background => !background.isPreset) 451 | .forEach(background => { 452 | const backgroundItem = backgroundList.createDiv('background-item'); 453 | new Setting(backgroundItem) 454 | .setName(background.name) 455 | .addExtraButton(btn => 456 | btn.setIcon('pencil') 457 | .setTooltip('编辑') 458 | .onClick(() => { 459 | // 使用背景编辑模态框 460 | new CreateBackgroundModal( 461 | this.app, 462 | async (updatedBackground) => { 463 | await this.plugin.settingsManager.updateBackground(background.id, updatedBackground); 464 | this.display(); 465 | new Notice('背景已更新'); 466 | }, 467 | background 468 | ).open(); 469 | })) 470 | .addExtraButton(btn => 471 | btn.setIcon('trash') 472 | .setTooltip('删除') 473 | .onClick(() => { 474 | new ConfirmModal( 475 | this.app, 476 | '确认删除背景', 477 | `确定要删除「${background.name}」背景吗?此操作不可恢复。`, 478 | async () => { 479 | await this.plugin.settingsManager.removeBackground(background.id); 480 | this.display(); 481 | new Notice('背景已删除'); 482 | } 483 | ).open(); 484 | })); 485 | 486 | // 添加背景预览 487 | const previewEl = backgroundItem.createDiv('background-preview'); 488 | previewEl.setAttribute('style', background.style); 489 | }); 490 | 491 | // 添加新背景按钮 492 | new Setting(containerEl) 493 | .addButton(btn => btn 494 | .setButtonText('+ 新建背景') 495 | .setCta() 496 | .onClick(() => { 497 | // 使用新的背景创建模态框 498 | new CreateBackgroundModal( 499 | this.app, 500 | async (newBackground) => { 501 | await this.plugin.settingsManager.addCustomBackground(newBackground); 502 | this.display(); 503 | new Notice('背景已创建'); 504 | } 505 | ).open(); 506 | })); 507 | } 508 | } -------------------------------------------------------------------------------- /src/settings/settings.ts: -------------------------------------------------------------------------------- 1 | import { Template } from '../templateManager'; 2 | import { Background } from '../backgroundManager'; 3 | 4 | interface MPSettings { 5 | backgroundId: string; 6 | templateId: string; 7 | fontFamily: string; 8 | fontSize: number; 9 | templates: Template[]; 10 | customTemplates: Template[]; 11 | backgrounds: Background[]; 12 | customBackgrounds: Background[]; 13 | customFonts: { value: string; label: string; isPreset?: boolean }[]; 14 | } 15 | 16 | const DEFAULT_SETTINGS: MPSettings = { 17 | backgroundId: 'default', 18 | templateId: 'default', 19 | fontFamily: '-apple-system', 20 | fontSize: 16, 21 | templates: [], 22 | customTemplates: [], 23 | backgrounds: [], 24 | customBackgrounds: [], 25 | customFonts: [ 26 | { 27 | value: 'Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, "PingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif', 28 | label: '默认字体', 29 | isPreset: true 30 | }, 31 | { value: 'SimSun, "宋体", serif', label: '宋体', isPreset: true }, 32 | { value: 'SimHei, "黑体", sans-serif', label: '黑体', isPreset: true }, 33 | { value: 'KaiTi, "楷体", serif', label: '楷体', isPreset: true }, 34 | { value: '"Microsoft YaHei", "微软雅黑", sans-serif', label: '雅黑', isPreset: true } 35 | ], 36 | }; 37 | 38 | export class SettingsManager { 39 | private plugin: any; 40 | private settings: MPSettings; 41 | 42 | constructor(plugin: any) { 43 | this.plugin = plugin; 44 | this.settings = DEFAULT_SETTINGS; 45 | } 46 | 47 | async loadSettings() { 48 | let savedData = await this.plugin.loadData(); 49 | if (!savedData) { 50 | savedData = {}; 51 | } 52 | if (!savedData.templates || savedData.templates.length === 0) { 53 | const { templates } = await import('../templates'); 54 | savedData.templates = Object.values(templates).map(template => ({ 55 | ...template, 56 | isPreset: true, 57 | isVisible: true // 默认可见 58 | })); 59 | } 60 | if (!savedData.customTemplates) { 61 | savedData.customTemplates = []; 62 | } 63 | if (!savedData.customFonts) { 64 | savedData.customFonts = DEFAULT_SETTINGS.customFonts; 65 | } 66 | // 加载背景设置 67 | if (!savedData.backgrounds || savedData.backgrounds.length === 0) { 68 | const { backgrounds } = await import('../backgrounds'); 69 | savedData.backgrounds = backgrounds.backgrounds.map(background => ({ 70 | ...background, 71 | isPreset: true, 72 | isVisible: true 73 | })); 74 | } 75 | if (!savedData.customBackgrounds) { 76 | savedData.customBackgrounds = []; 77 | } 78 | if (!savedData.customFonts) { 79 | savedData.customFonts = DEFAULT_SETTINGS.customFonts; 80 | } 81 | this.settings = Object.assign({}, DEFAULT_SETTINGS, savedData); 82 | } 83 | 84 | getAllTemplates(): Template[] { 85 | return [...this.settings.templates, ...this.settings.customTemplates]; 86 | } 87 | 88 | getVisibleTemplates(): Template[] { 89 | return this.getAllTemplates().filter(template => template.isVisible !== false); 90 | } 91 | 92 | getTemplate(templateId: string): Template | undefined { 93 | return this.settings.templates.find(template => template.id === templateId) 94 | || this.settings.customTemplates.find(template => template.id === templateId); 95 | } 96 | 97 | async addCustomTemplate(template: Template) { 98 | template.isPreset = false; 99 | template.isVisible = true; // 默认可见 100 | this.settings.customTemplates.push(template); 101 | await this.saveSettings(); 102 | } 103 | 104 | async updateTemplate(templateId: string, updatedTemplate: Partial