/** * 文件:setting-tab.ts * 作用:Obsidian 设置面板集成,提供界面化配置入口。 */ import { App, TextAreaComponent, PluginSettingTab, Setting, Notice, sanitizeHTMLToDom } from 'obsidian'; import Note2AnyPlugin 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 Note2AnySettingTab extends PluginSettingTab { plugin: Note2AnyPlugin; wxInfo: string; wxTextArea: TextAreaComponent|null; settings: NMPSettings; constructor(app: App, plugin: Note2AnyPlugin) { 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 tabs = [ { id: 'style', label: '样式', render: (panel: HTMLElement) => this.renderStyleTab(panel) }, { id: 'shortcode', label: '短代码', render: (panel: HTMLElement) => this.renderShortcodeTab(panel) }, { id: 'theme', label: '主题', render: (panel: HTMLElement) => this.renderThemeTab(panel) }, { id: 'image', label: '图片', render: (panel: HTMLElement) => this.renderImageTab(panel) }, { id: 'user', label: '用户信息', render: (panel: HTMLElement) => this.renderUserTab(panel) }, ]; const tabBar = containerEl.createDiv({ cls: 'nmp-settings-tabs' }); const panelsWrapper = containerEl.createDiv({ cls: 'nmp-settings-panels' }); const panelMap = new Map(); const buttonMap = new Map(); const activate = (id: string) => { buttonMap.forEach((btn, key) => btn.toggleClass('is-active', key === id)); panelMap.forEach((panel, key) => panel.toggleClass('is-active', key === id)); }; tabs.forEach((tab) => { const button = tabBar.createEl('button', { text: tab.label, cls: 'nmp-settings-tab-button' }); button.onclick = () => activate(tab.id); buttonMap.set(tab.id, button); const panel = panelsWrapper.createDiv({ cls: 'nmp-settings-panel', attr: { 'data-tab': tab.id } }); panelMap.set(tab.id, panel); tab.render(panel); }); if (tabs.length > 0) { activate(tabs[0].id); } } private renderStyleTab(panel: HTMLElement): void { new Setting(panel) .setName('默认样式') .addDropdown(dropdown => { const styles = this.plugin.assetsManager.themes; for (const 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(panel) .setName('代码高亮') .addDropdown(dropdown => { const styles = this.plugin.assetsManager.highlights; for (const 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(panel) .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(panel) .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(panel) .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(panel) .setName('显示代码行号') .addToggle(toggle => { toggle.setValue(this.settings.lineNumber); toggle.onChange(async (value) => { this.settings.lineNumber = value; await this.plugin.saveSettings(); }); }); new Setting(panel) .setName('启用空行渲染') .addToggle(toggle => { toggle.setValue(this.settings.enableEmptyLine); toggle.onChange(async (value) => { this.settings.enableEmptyLine = value; await this.plugin.saveSettings(); }); }); new Setting(panel) .setName('默认开启评论') .setDesc('发布到公众号时默认开启评论,可在 frontmatter 使用 need_open_comment 关闭') .addToggle(toggle => { toggle.setValue(this.settings.needOpenComment); toggle.onChange(async (value) => { this.settings.needOpenComment = value; await this.plugin.saveSettings(); }); }); } private renderShortcodeTab(panel: HTMLElement): void { new Setting(panel) .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(panel) .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(panel) .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(panel) .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;'); }); } private renderThemeTab(panel: HTMLElement): void { new Setting(panel) .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(panel) .setName('清空主题') .addButton(button => { button.setButtonText('清空'); button.onClick(async () => { await this.plugin.assetsManager.removeThemes(); this.settings.resetStyelAndHighlight(); await this.plugin.saveSettings(); }); }); new Setting(panel) .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(panel) .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(panel) .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;'); }); } private renderImageTab(panel: HTMLElement): void { new Setting(panel) .setName('切图保存路径') .setDesc('切图文件的保存目录,默认:/Users/gavin/note2any/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(panel) .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(panel) .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(panel) .setName('渲染图片标题') .addToggle(toggle => { toggle.setValue(this.settings.useFigcaption); toggle.onChange(async (value) => { this.settings.useFigcaption = value; await this.plugin.saveSettings(); }); }); new Setting(panel) .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(panel) .setName('水印图片') .setDesc('可填写文件名或 URL') .addText(text => { text.setPlaceholder('例如 watermark.png') .setValue(this.settings.watermark) .onChange(async (value) => { this.settings.watermark = value.trim(); await this.plugin.saveSettings(); }) .inputEl.setAttr('style', 'width: 320px;'); }); } private renderUserTab(panel: HTMLElement): void { 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(panel) .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(panel) .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;'); }) .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('测试公众号'); }); }); // const helpEl = panel.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' } }); } }