/**
* 文件: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('测试公众号');
})
})
}
}