Files
note2any/src/setting-tab.ts
2025-10-10 21:54:05 +08:00

583 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 文件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' } });
}
}