/** * 文件:setting-tab.ts * 作用:Obsidian 设置面板集成,提供界面化配置入口。 */ import { App, TextAreaComponent, PluginSettingTab, Setting, Notice, sanitizeHTMLToDom } from 'obsidian'; import NoteToMpPlugin from './main'; import { wxGetToken, wxEncrypt } from './wechat/weixin-api'; import { cleanMathCache } from './markdown/math'; import { NMPSettings } from './settings'; import { DocModal } from './doc-modal'; export class NoteToMpSettingTab extends PluginSettingTab { plugin: NoteToMpPlugin; wxInfo: string; wxTextArea: TextAreaComponent|null; settings: NMPSettings; constructor(app: App, plugin: NoteToMpPlugin) { super(app, plugin); this.plugin = plugin; this.settings = NMPSettings.getInstance(); this.wxInfo = this.parseWXInfo(); } displayWXInfo(txt:string) { this.wxTextArea?.setValue(txt); } parseWXInfo() { const wxInfo = this.settings.wxInfo; if (wxInfo.length == 0) { return ''; } let res = ''; for (let wx of wxInfo) { res += `${wx.name}|${wx.appid}|********\n`; } return res; } async testWXInfo() { const authKey = this.settings.authKey; if (authKey.length == 0) { new Notice('请先设置authKey'); return; } const wxInfo = this.settings.wxInfo; if (wxInfo.length == 0) { new Notice('请先设置公众号信息'); return; } try { const docUrl = 'https://mp.weixin.qq.com/s/rk5CTPGr5ftly8PtYgSjCQ'; for (let wx of wxInfo) { const res = await wxGetToken(authKey, wx.appid, wx.secret.replace('SECRET', '')); if (res.status != 200) { const data = res.json; const { code, message } = data; let content = message; if (code === 50002) { content = '用户受限,可能是您的公众号被冻结或注销,请联系微信客服处理'; } else if (code === 40125) { content = 'AppSecret错误,请检查或者重置,详细操作步骤请参考下方文档'; } else if (code === 40164) { content = 'IP地址不在白名单中,请将如下地址添加到白名单:
59.110.112.211
154.8.198.218
详细步骤请参考下方文档'; } const modal = new DocModal(this.app, `${wx.name} 测试失败`, content, docUrl); modal.open(); break } const data = res.json; if (data.token.length == 0) { new Notice(`${wx.name}|${wx.appid} 测试失败`); break } new Notice(`${wx.name} 测试通过`); } } catch (error) { new Notice(`测试失败:${error}`); } } async encrypt() { if (this.wxInfo.length == 0) { new Notice('请输入内容'); return false; } if (this.settings.wxInfo.length > 0) { new Notice('已经保存过了,请先清除!'); return false; } const wechat = []; const lines = this.wxInfo.split('\n'); for (let line of lines) { line = line.trim(); if (line.length == 0) { continue; } const items = line.split('|'); if (items.length != 3) { new Notice('格式错误,请检查'); return false; } const name = items[0]; const appid = items[1].trim(); const secret = items[2].trim(); wechat.push({name, appid, secret}); } if (wechat.length == 0) { return false; } try { const res = await wxEncrypt(this.settings.authKey, wechat); if (res.status != 200) { const data = res.json; new Notice(`${data.message}`); return false; } const data = res.json; for (let wx of wechat) { wx.secret = data[wx.appid]; } this.settings.wxInfo = wechat; await this.plugin.saveSettings(); this.wxInfo = this.parseWXInfo(); this.displayWXInfo(this.wxInfo); new Notice('保存成功'); return true; } catch (error) { new Notice(`保存失败:${error}`); console.error(error); } return false; } async clear() { this.settings.wxInfo = []; await this.plugin.saveSettings(); this.wxInfo = ''; this.displayWXInfo('') } display() { const {containerEl} = this; containerEl.empty(); this.wxInfo = this.parseWXInfo(); const helpEl = containerEl.createEl('div', { cls: 'setting-help-section' }); helpEl.createEl('h2', {text: '帮助文档', cls: 'setting-help-title'}); helpEl.createEl('a', {text: 'https://sunboshi.tech/doc', attr: {href: 'https://sunboshi.tech/doc'}}); containerEl.createEl('h2', {text: '插件设置'}); new Setting(containerEl) .setName('默认样式') .addDropdown(dropdown => { const styles = this.plugin.assetsManager.themes; for (let s of styles) { dropdown.addOption(s.className, s.name); } dropdown.setValue(this.settings.defaultStyle); dropdown.onChange(async (value) => { this.settings.defaultStyle = value; await this.plugin.saveSettings(); }); }); new Setting(containerEl) .setName('代码高亮') .addDropdown(dropdown => { const styles = this.plugin.assetsManager.highlights; for (let s of styles) { dropdown.addOption(s.name, s.name); } dropdown.setValue(this.settings.defaultHighlight); dropdown.onChange(async (value) => { this.settings.defaultHighlight = value; await this.plugin.saveSettings(); }); }); new Setting(containerEl) .setName('在工具栏展示样式选择') .setDesc('建议在移动端关闭,可以增大文章预览区域') .addToggle(toggle => { toggle.setValue(this.settings.showStyleUI); toggle.onChange(async (value) => { this.settings.showStyleUI = value; await this.plugin.saveSettings(); }); }); new Setting(containerEl) .setName('链接展示样式') .addDropdown(dropdown => { dropdown.addOption('inline', '内嵌'); dropdown.addOption('footnote', '脚注'); dropdown.setValue(this.settings.linkStyle); dropdown.onChange(async (value) => { this.settings.linkStyle = value; await this.plugin.saveSettings(); }); }); new Setting(containerEl) .setName('文件嵌入展示样式') .addDropdown(dropdown => { dropdown.addOption('quote', '引用'); dropdown.addOption('content', '正文'); dropdown.setValue(this.settings.embedStyle); dropdown.onChange(async (value) => { this.settings.embedStyle = value; await this.plugin.saveSettings(); }); }); new Setting(containerEl) .setName('数学公式语法') .addDropdown(dropdown => { dropdown.addOption('latex', 'latex'); dropdown.addOption('asciimath', 'asciimath'); dropdown.setValue(this.settings.math); dropdown.onChange(async (value) => { this.settings.math = value; cleanMathCache(); await this.plugin.saveSettings(); }); }); new Setting(containerEl) .setName('显示代码行号') .addToggle(toggle => { toggle.setValue(this.settings.lineNumber); toggle.onChange(async (value) => { this.settings.lineNumber = value; await this.plugin.saveSettings(); }); }) new Setting(containerEl) .setName('Gallery 根路径') .setDesc('用于 {{}} 短代码解析;需指向本地图片根目录') .addText(text => { text.setPlaceholder('例如 /Users/xxx/site/static 或 相对路径') .setValue(this.settings.galleryPrePath || '') .onChange(async (value) => { this.settings.galleryPrePath = value.trim(); await this.plugin.saveSettings(); }); text.inputEl.setAttr('style', 'width: 360px;'); }); new Setting(containerEl) .setName('Gallery 选取图片数') .setDesc('每个 gallery 短代码最多替换为前 N 张图片') .addText(text => { text.setPlaceholder('数字 >=1') .setValue(String(this.settings.galleryNumPic || 2)) .onChange(async (value) => { const n = parseInt(value, 10); if (Number.isFinite(n) && n >= 1) { this.settings.galleryNumPic = n; await this.plugin.saveSettings(); } }); text.inputEl.setAttr('style', 'width: 120px;'); }); new Setting(containerEl) .setName('默认封面图片') .setDesc('当文章无任何图片/短代码时使用;可填 wikilink 文件名或 http(s) URL') .addText(text => { text.setPlaceholder('例如 cover.png 或 https://...') .setValue(this.settings.defaultCoverPic || '') .onChange(async (value) => { this.settings.defaultCoverPic = value.trim(); await this.plugin.saveSettings(); }); text.inputEl.setAttr('style', 'width: 360px;'); }); new Setting(containerEl) .setName('忽略 frontmatter 封面') .setDesc('开启后不使用 frontmatter 中 cover/image 字段,封面将按正文首图→gallery→默认封面回退') .addToggle(toggle => { toggle.setValue(this.settings.ignoreFrontmatterImage); toggle.onChange(async (value) => { this.settings.ignoreFrontmatterImage = value; await this.plugin.saveSettings(); }); }); new Setting(containerEl) .setName('启用空行渲染') .addToggle(toggle => { toggle.setValue(this.settings.enableEmptyLine); toggle.onChange(async (value) => { this.settings.enableEmptyLine = value; await this.plugin.saveSettings(); }); }) // 切图配置区块 containerEl.createEl('h2', {text: '切图配置'}); new Setting(containerEl) .setName('切图保存路径') .setDesc('切图文件的保存目录,默认:/Users/gavin/note2mp/images/xhs') .addText(text => { text.setPlaceholder('例如 /Users/xxx/images/xhs') .setValue(this.settings.sliceImageSavePath || '') .onChange(async (value) => { this.settings.sliceImageSavePath = value.trim(); await this.plugin.saveSettings(); }); text.inputEl.setAttr('style', 'width: 360px;'); }); new Setting(containerEl) .setName('切图宽度') .setDesc('长图及切图的宽度(像素),默认:1080') .addText(text => { text.setPlaceholder('数字 >=100') .setValue(String(this.settings.sliceImageWidth || 1080)) .onChange(async (value) => { const n = parseInt(value, 10); if (Number.isFinite(n) && n >= 100) { this.settings.sliceImageWidth = n; await this.plugin.saveSettings(); } }); text.inputEl.setAttr('style', 'width: 120px;'); }); new Setting(containerEl) .setName('切图横竖比例') .setDesc('格式:宽:高,例如 3:4 表示竖图,16:9 表示横图') .addText(text => { text.setPlaceholder('例如 3:4') .setValue(this.settings.sliceImageAspectRatio || '3:4') .onChange(async (value) => { this.settings.sliceImageAspectRatio = value.trim(); await this.plugin.saveSettings(); }); text.inputEl.setAttr('style', 'width: 120px;'); }); new Setting(containerEl) .setName('渲染图片标题') .addToggle(toggle => { toggle.setValue(this.settings.useFigcaption); toggle.onChange(async (value) => { this.settings.useFigcaption = value; await this.plugin.saveSettings(); }); }) new Setting(containerEl) .setName('Excalidraw 渲染为 PNG 图片') .setDesc('开启:将 Excalidraw 笔记/嵌入转换为位图 PNG 插入;关闭:保持原始 SVG/矢量渲染(更清晰,体积更小)。') .addToggle(toggle => { toggle.setValue(this.settings.excalidrawToPNG); toggle.onChange(async (value) => { this.settings.excalidrawToPNG = value; await this.plugin.saveSettings(); }); }) new Setting(containerEl) .setName('水印图片') .addText(text => { text.setPlaceholder('请输入图片名称') .setValue(this.settings.watermark) .onChange(async (value) => { this.settings.watermark = value.trim(); await this.plugin.saveSettings(); }) .inputEl.setAttr('style', 'width: 320px;') }) new Setting(containerEl) .setName('获取更多主题') .addButton(button => { button.setButtonText('下载'); button.onClick(async () => { button.setButtonText('下载中...'); await this.plugin.assetsManager.downloadThemes(); button.setButtonText('下载完成'); }); }) .addButton(button => { button.setIcon('folder-open'); button.onClick(async () => { await this.plugin.assetsManager.openAssets(); }); }); new Setting(containerEl) .setName('清空主题') .addButton(button => { button.setButtonText('清空'); button.onClick(async () => { await this.plugin.assetsManager.removeThemes(); this.settings.resetStyelAndHighlight(); await this.plugin.saveSettings(); }); }) new Setting(containerEl) .setName('全局CSS属性') .setDesc('只能填写CSS属性,不能写选择器') .addTextArea(text => { this.wxTextArea = text; text.setPlaceholder('请输入CSS属性,如:background: #fff;padding: 10px;') .setValue(this.settings.baseCSS) .onChange(async (value) => { this.settings.baseCSS = value; await this.plugin.saveSettings(); }) .inputEl.setAttr('style', 'width: 520px; height: 60px;'); }) const customCSSDoc = '使用指南:https://sunboshi.tech/customcss'; new Setting(containerEl) .setName('自定义CSS笔记') .setDesc(sanitizeHTMLToDom(customCSSDoc)) .addText(text => { text.setPlaceholder('请输入自定义CSS笔记标题') .setValue(this.settings.customCSSNote) .onChange(async (value) => { this.settings.customCSSNote = value.trim(); await this.plugin.saveSettings(); await this.plugin.assetsManager.loadCustomCSS(); }) .inputEl.setAttr('style', 'width: 320px;') }); const expertDoc = '使用指南:https://sunboshi.tech/expert'; new Setting(containerEl) .setName('专家设置笔记') .setDesc(sanitizeHTMLToDom(expertDoc)) .addText(text => { text.setPlaceholder('请输入专家设置笔记标题') .setValue(this.settings.expertSettingsNote) .onChange(async (value) => { this.settings.expertSettingsNote = value.trim(); await this.plugin.saveSettings(); await this.plugin.assetsManager.loadExpertSettings(); }) .inputEl.setAttr('style', 'width: 320px;') }); let descHtml = '详情说明:https://sunboshi.tech/subscribe'; if (this.settings.isVip) { descHtml = '👑永久会员
' + descHtml; } else if (this.settings.expireat) { const timestr = this.settings.expireat.toLocaleString(); descHtml = `有效期至:${timestr}
${descHtml}` } new Setting(containerEl) .setName('注册码(AuthKey)') .setDesc(sanitizeHTMLToDom(descHtml)) .addText(text => { text.setPlaceholder('请输入注册码') .setValue(this.settings.authKey) .onChange(async (value) => { this.settings.authKey = value.trim(); this.settings.getExpiredDate(); await this.plugin.saveSettings(); }) .inputEl.setAttr('style', 'width: 320px;') }).descEl.setAttr('style', '-webkit-user-select: text; user-select: text;') let isClear = this.settings.wxInfo.length > 0; let isRealClear = false; const buttonText = isClear ? '清空公众号信息' : '保存公众号信息'; new Setting(containerEl) .setName('公众号信息') .addTextArea(text => { this.wxTextArea = text; text.setPlaceholder('请输入公众号信息\n格式:公众号名称|公众号AppID|公众号AppSecret\n多个公众号请换行输入\n输入完成后点击加密按钮') .setValue(this.wxInfo) .onChange(value => { this.wxInfo = value; }) .inputEl.setAttr('style', 'width: 520px; height: 120px;'); }) new Setting(containerEl).addButton(button => { button.setButtonText(buttonText); button.onClick(async () => { if (isClear) { isRealClear = true; isClear = false; button.setButtonText('确认清空?'); } else if (isRealClear) { isRealClear = false; isClear = false; this.clear(); button.setButtonText('保存公众号信息'); } else { button.setButtonText('保存中...'); if (await this.encrypt()) { isClear = true; isRealClear = false; button.setButtonText('清空公众号信息'); } else { button.setButtonText('保存公众号信息'); } } }); }) .addButton(button => { button.setButtonText('测试公众号'); button.onClick(async () => { button.setButtonText('测试中...'); await this.testWXInfo(); button.setButtonText('测试公众号'); }) }) } }