583 lines
18 KiB
TypeScript
583 lines
18 KiB
TypeScript
/**
|
||
* 文件: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地址不在白名单中,请将如下地址添加到白名单:<br>59.110.112.211<br>154.8.198.218<br>详细步骤请参考下方文档';
|
||
}
|
||
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<string, HTMLElement>();
|
||
const buttonMap = new Map<string, HTMLButtonElement>();
|
||
|
||
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('用于 {{<gallery dir="..."/>}} 短代码解析;需指向本地图片根目录')
|
||
.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 = '使用指南:<a href="https://sunboshi.tech/customcss">https://sunboshi.tech/customcss</a>';
|
||
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 = '使用指南:<a href="https://sunboshi.tech/expert">https://sunboshi.tech/expert</a>';
|
||
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/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(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 = '详情说明:<a href="https://sunboshi.tech/subscribe">https://sunboshi.tech/subscribe</a>';
|
||
if (this.settings.isVip) {
|
||
descHtml = '<span style="color:rgb(245, 70, 85);font-weight: bold;">👑永久会员</span><br/>' + descHtml;
|
||
}
|
||
else if (this.settings.expireat) {
|
||
const timestr = this.settings.expireat.toLocaleString();
|
||
descHtml = `有效期至:${timestr} <br/>${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' } });
|
||
}
|
||
}
|