diff --git a/images/xhs/note2mdtest_1.png b/images/xhs/note2mdtest_1.png index 8bdf8a1..1714bb0 100644 Binary files a/images/xhs/note2mdtest_1.png and b/images/xhs/note2mdtest_1.png differ diff --git a/images/xhs/note2mdtest_2.png b/images/xhs/note2mdtest_2.png index 689dade..40abc77 100644 Binary files a/images/xhs/note2mdtest_2.png and b/images/xhs/note2mdtest_2.png differ diff --git a/images/xhs/note2mdtest_3.png b/images/xhs/note2mdtest_3.png index 127b6d2..c702456 100644 Binary files a/images/xhs/note2mdtest_3.png and b/images/xhs/note2mdtest_3.png differ diff --git a/images/xhs/note2mdtest_5.png b/images/xhs/note2mdtest_5.png index fac806b..f3e8fdc 100644 Binary files a/images/xhs/note2mdtest_5.png and b/images/xhs/note2mdtest_5.png differ diff --git a/release.md b/release.md index 7457294..8fe7805 100644 --- a/release.md +++ b/release.md @@ -1,5 +1,7 @@ -# 版本信息(发布版本时填写,供脚本使用) +# 版本信息 +‼️ 发布版本时填写,供脚本~/pubsh/release.sh使用 + ## v1.3.4 ### 重构 #### 新的架构图 @@ -59,11 +61,4 @@ 现在分页依据真实渲染高度,预览窗口内不会再丢失底部内容。建议在小红书预览里多翻几页、调整字号后重新分页验证结果。 -## v1.3.9 -重新实现分页测量,清理多余日志。 -- 重新实现分页测量:构建隐藏的“测量页面”,与真实页面同样的宽度、内边距(40px)和 class,逐个把克隆元素追加进去,利用 scrollHeight 决定是否换页,保证 margin 折叠后计算准确 (src/xiaohongshu/paginator.ts:57;waitForLayout 新增)。 -- 当元素放不下当前页时,移除测量克隆并把已排内容写入分页,再以该元素开启新页;不可分割元素允许独占一页即便超高 (src/xiaohongshu/paginator.ts:101)。 -- 清理多余日志,同时共用 PAGE_PADDING 常量让 renderPage 和测量逻辑保持一致 (src/xiaohongshu/paginator.ts:182)。 - -现在分页依据真实渲染高度,预览窗口内不会再丢失底部内容。建议在小红书预览里多翻几页、调整字号后重新分页验证结果。 diff --git a/scripts/create_milestone.sh b/scripts/create_milestone.sh deleted file mode 100755 index aebf81a..0000000 --- a/scripts/create_milestone.sh +++ /dev/null @@ -1,195 +0,0 @@ -#!/bin/bash -# create_milestone.sh - 自动创建项目里程碑版本 -# 使用方法: ./create_milestone.sh v1.3.0 "里程碑版本描述" - -set -e # 遇到错误立即退出 - -VERSION=$1 -DESCRIPTION=${2:-"里程碑版本"} - -if [ -z "$VERSION" ]; then - echo "❌ 错误: 请提供版本号" - echo "使用方法: $0 [description]" - echo "示例: $0 v1.3.0 '批量发布系统完成'" - exit 1 -fi - -# 版本号格式验证 -if [[ ! $VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "❌ 版本号格式错误,应为 vX.X.X 格式 (如: v1.3.0)" - exit 1 -fi - -# 检查版本是否已存在 -if git tag | grep -q "^$VERSION$"; then - echo "❌ 版本 $VERSION 已存在" - exit 1 -fi - -echo "🚀 开始创建里程碑版本: $VERSION" - -# 1. 检查工作目录状态 -echo "📋 检查Git状态..." -if ! git diff-index --quiet HEAD --; then - echo "⚠️ 发现未提交的更改,正在自动提交..." - git add . - git commit -m "feat: $DESCRIPTION - -版本: $VERSION (里程碑版本)" -fi - -# 2. 创建Git标签 -echo "🏷️ 创建Git标签..." -git tag -a "$VERSION" -m "$DESCRIPTION - -此版本为稳定的里程碑版本,用于后续大规模修改前的对比和回滚基准。 - -创建时间: $(date '+%Y-%m-%d %H:%M:%S') -Git提交: $(git rev-parse HEAD)" - -# 3. 创建发布分支 -echo "🌿 创建发布分支..." -git checkout -b "release/$VERSION" - -# 4. 构建项目 -echo "🔨 构建项目..." -if [ -f package.json ]; then - npm run build -else - echo "⚠️ 未找到package.json,跳过构建步骤" -fi - -# 5. 创建归档目录 -echo "📁 创建归档目录..." -mkdir -p "archives/$VERSION" - -# 6. 复制关键文件 -echo "📋 复制构建文件..." -for file in main.js manifest.json styles.css package.json; do - if [ -f "$file" ]; then - cp "$file" "archives/$VERSION/" - echo " ✅ 复制 $file" - else - echo " ⚠️ $file 不存在,跳过" - fi -done - -echo "📄 复制文档文件..." -for file in README.md CHANGELOG.md detaildesign.md diagrams.md; do - if [ -f "$file" ]; then - cp "$file" "archives/$VERSION/" - echo " ✅ 复制 $file" - else - echo " ⚠️ $file 不存在,跳过" - fi -done - -# 7. 创建源码快照 -echo "📦 创建源码快照..." -PROJECT_NAME=$(basename "$(pwd)") -cd .. && tar -czf "$PROJECT_NAME/archives/$VERSION/source-snapshot-$VERSION.tar.gz" \ - --exclude='node_modules' \ - --exclude='.git' \ - --exclude='archives' \ - "$PROJECT_NAME/" -cd "$PROJECT_NAME" - -# 8. 创建版本信息文档 -echo "📋 创建版本信息文档..." -cat > "archives/$VERSION/VERSION_INFO.md" << EOF -# 里程碑版本 $VERSION - -## 版本信息 -- **版本号**: $VERSION -- **发布日期**: $(date +%Y年%m月%d日) -- **Git Tag**: $VERSION -- **Git Branch**: release/$VERSION -- **Git Commit**: $(git rev-parse HEAD) -- **描述**: $DESCRIPTION - -## 主要内容 -$(if [ -f "archives/$VERSION/main.js" ]; then echo "- 构建文件: main.js ($(du -h "archives/$VERSION/main.js" | cut -f1))"; fi) -$(if [ -f "archives/$VERSION/manifest.json" ]; then echo "- 插件清单: manifest.json"; fi) -$(if [ -f "archives/$VERSION/styles.css" ]; then echo "- 样式文件: styles.css"; fi) -$(if [ -f "archives/$VERSION/README.md" ]; then echo "- 项目文档: README.md"; fi) -$(if [ -f "archives/$VERSION/CHANGELOG.md" ]; then echo "- 变更日志: CHANGELOG.md"; fi) -$(if [ -f "archives/$VERSION/detaildesign.md" ]; then echo "- 设计文档: detaildesign.md"; fi) -$(if [ -f "archives/$VERSION/diagrams.md" ]; then echo "- 架构图表: diagrams.md"; fi) -- 源码快照: source-snapshot-$VERSION.tar.gz ($(du -h "archives/$VERSION/source-snapshot-$VERSION.tar.gz" | cut -f1)) - -## 回滚说明 -如需回滚到此版本: - -### 方法1: 使用Git Tag回滚 -\`\`\`bash -git checkout $VERSION -git checkout -b rollback-to-$VERSION -\`\`\` - -### 方法2: 使用发布分支 -\`\`\`bash -git checkout release/$VERSION -\`\`\` - -### 方法3: 使用源代码快照 -\`\`\`bash -tar -xzf archives/$VERSION/source-snapshot-$VERSION.tar.gz -\`\`\` - -### 方法4: 使用构建文件 -\`\`\`bash -$(if [ -f "archives/$VERSION/main.js" ]; then echo "cp archives/$VERSION/main.js ./"; fi) -$(if [ -f "archives/$VERSION/manifest.json" ]; then echo "cp archives/$VERSION/manifest.json ./"; fi) -$(if [ -f "archives/$VERSION/styles.css" ]; then echo "cp archives/$VERSION/styles.css ./"; fi) -\`\`\` - -## 版本对比 -此版本可作为后续重大修改的对比基准,主要用于: -- 功能回归测试 -- 性能对比分析 -- 代码架构变更评估 -- 稳定性基准对比 - ---- -*此版本为稳定的里程碑版本,建议在进行大规模代码修改前保存此状态。* -*创建脚本: scripts/create_milestone.sh* -EOF - -# 9. 切换回主分支 -echo "🔄 切换回主分支..." -git checkout main - -# 10. 推送到远程 -echo "☁️ 推送到远程仓库..." -if git remote | grep -q origin; then - echo " 推送主分支和标签..." - git push origin main --tags - echo " 推送发布分支..." - git push origin "release/$VERSION" - echo "✅ 已推送到远程仓库" -else - echo "⚠️ 无远程仓库配置,跳过推送" -fi - -# 11. 验证创建结果 -echo "" -echo "🔍 验证里程碑创建结果..." -echo "📁 归档目录内容:" -ls -la "archives/$VERSION/" | while read line; do echo " $line"; done - -echo "" -echo "🎯 里程碑版本 $VERSION 创建完成!" -echo "" -echo "📋 创建内容:" -echo " - Git标签: $VERSION" -echo " - 发布分支: release/$VERSION" -echo " - 归档目录: archives/$VERSION/" -echo " - 源码快照: source-snapshot-$VERSION.tar.gz" -echo " - 版本文档: VERSION_INFO.md" -echo "" -echo "🔄 快速回滚命令:" -echo " git checkout $VERSION # 使用标签" -echo " git checkout release/$VERSION # 使用分支" -echo " tar -xzf archives/$VERSION/source-snapshot-$VERSION.tar.gz # 使用快照" -echo "" -echo "📖 详细信息请查看: archives/$VERSION/VERSION_INFO.md" \ No newline at end of file diff --git a/src/preview-view-backup.ts b/src/preview-view-backup.ts deleted file mode 100644 index 682b4a1..0000000 --- a/src/preview-view-backup.ts +++ /dev/null @@ -1,895 +0,0 @@ -/** - * 文件:note-preview.ts - * 功能:侧边预览视图;支持多平台预览(公众号/小红书)与发布触发。 - * - 渲染 Markdown - * - 平台切换下拉 - * - 单篇发布入口 - * - 与批量发布/图片处理集成预留 - */ - -import { EventRef, ItemView, Workspace, WorkspaceLeaf, Notice, Platform, TFile, TFolder, TAbstractFile, Plugin } from 'obsidian'; -import { uevent, debounce, waitForLayoutReady } from './utils'; -import { NMPSettings } from './settings'; -import AssetsManager from './assets'; -import { MarkedParser } from './markdown/parser'; -import { LocalImageManager, LocalFile } from './markdown/local-file'; -import { CardDataManager } from './markdown/code'; -import { ArticleRender } from './article-render'; -// 平台选择组件 -import { PlatformChooser, PlatformType } from './platform-chooser'; -// 微信公众号功能模块 -import { WechatPreview } from './wechat/wechat-preview'; -// 小红书功能模块 -import { XiaohongshuContentAdapter } from './xiaohongshu/adapter'; -import { XiaohongshuImageManager } from './xiaohongshu/image'; -import { XiaohongshuAPIManager } from './xiaohongshu/api'; -import { XiaohongshuPost } from './xiaohongshu/types'; -import { XiaohongshuPreview } from './xiaohongshu/xhs-preview'; -// 切图功能 -import { sliceArticleImage } from './slice-image'; - - -export const VIEW_TYPE_NOTE_PREVIEW = 'note-preview'; - -export class NotePreview extends ItemView { - workspace: Workspace; - plugin: Plugin; - mainDiv: HTMLDivElement; - toolbar: HTMLDivElement; - renderDiv: HTMLDivElement; - articleDiv: HTMLDivElement; - styleEl: HTMLElement; - coverEl: HTMLInputElement; - useDefaultCover: HTMLInputElement; - useLocalCover: HTMLInputElement; - msgView: HTMLDivElement; - wechatSelect: HTMLSelectElement; - platformSelect: HTMLSelectElement; // 新增:平台选择器 - themeSelect: HTMLSelectElement; - highlightSelect: HTMLSelectElement; - listeners?: EventRef[]; - container: Element; - settings: NMPSettings; - assetsManager: AssetsManager; - articleHTML: string; - title: string; - currentFile?: TFile; - currentTheme: string; - currentHighlight: string; - currentAppId: string; - currentPlatform: string = 'wechat'; // 新增:当前选择的平台,默认微信 - markedParser: MarkedParser; - cachedElements: Map = new Map(); - _articleRender: ArticleRender | null = null; - _xiaohongshuPreview: XiaohongshuPreview | null = null; - _wechatPreview: WechatPreview | null = null; - _platformChooser: PlatformChooser | null = null; - isCancelUpload: boolean = false; - isBatchRuning: boolean = false; - - - constructor(leaf: WorkspaceLeaf, plugin: Plugin) { - super(leaf); - this.workspace = this.app.workspace; - this.plugin = plugin; - this.settings = NMPSettings.getInstance(); - this.assetsManager = AssetsManager.getInstance(); - this.currentTheme = this.settings.defaultStyle; - this.currentHighlight = this.settings.defaultHighlight; - } - - getViewType() { - return VIEW_TYPE_NOTE_PREVIEW; - } - - getIcon() { - return 'clipboard-paste'; - } - - getDisplayText() { - return '笔记预览'; - } - - get render() { - if (!this._articleRender) { - this._articleRender = new ArticleRender(this.app, this, this.styleEl, this.articleDiv); - this._articleRender.currentTheme = this.currentTheme; - this._articleRender.currentHighlight = this.currentHighlight; - } - return this._articleRender; - } - - async onOpen() { - this.viewLoading(); - this.setup(); - uevent('open'); - } - - async setup() { - await waitForLayoutReady(this.app); - - if (!this.settings.isLoaded) { - const data = await this.plugin.loadData(); - NMPSettings.loadSettings(data); - } - if (!this.assetsManager.isLoaded) { - await this.assetsManager.loadAssets(); - } - - this.buildUI(); - this.listeners = [ - this.workspace.on('file-open', () => { - this.update(); - }), - this.app.vault.on("modify", (file) => { - if (this.currentFile?.path == file.path) { - this.renderMarkdown(); - } - } ) - ]; - - this.renderMarkdown(); - } - - async onClose() { - this.listeners?.forEach(listener => this.workspace.offref(listener)); - LocalFile.fileCache.clear(); - uevent('close'); - } - - onAppIdChanged() { - // 清理上传过的图片 - this.cleanArticleData(); - } - - async update() { - if (this.isBatchRuning) { - return; - } - this.cleanArticleData(); - this.renderMarkdown(); - } - - cleanArticleData() { - LocalImageManager.getInstance().cleanup(); - CardDataManager.getInstance().cleanup(); - } - - buildMsgView(parent: HTMLDivElement) { - this.msgView = parent.createDiv({ cls: 'msg-view' }); - const title = this.msgView.createDiv({ cls: 'msg-title' }); - title.id = 'msg-title'; - title.innerText = '加载中...'; - const okBtn = this.msgView.createEl('button', { cls: 'msg-ok-btn' }, async (button) => { - - }); - okBtn.id = 'msg-ok-btn'; - okBtn.innerText = '确定'; - okBtn.onclick = async () => { - this.msgView.setAttr('style', 'display: none;'); - } - const cancelBtn = this.msgView.createEl('button', { cls: 'msg-ok-btn' }, async (button) => { - }); - cancelBtn.id = 'msg-cancel-btn'; - cancelBtn.innerText = '取消'; - cancelBtn.onclick = async () => { - this.isCancelUpload = true; - this.msgView.setAttr('style', 'display: none;'); - } - } - - showLoading(msg: string, cancelable: boolean = false) { - const title = this.msgView.querySelector('#msg-title') as HTMLElement; - title!.innerText = msg; - const btn = this.msgView.querySelector('#msg-ok-btn') as HTMLElement; - btn.setAttr('style', 'display: none;'); - this.msgView.setAttr('style', 'display: flex;'); - const cancelBtn = this.msgView.querySelector('#msg-cancel-btn') as HTMLElement; - cancelBtn.setAttr('style', cancelable ? 'display: block;': 'display: none;'); - this.msgView.setAttr('style', 'display: flex;'); - } - - showMsg(msg: string) { - const title = this.msgView.querySelector('#msg-title') as HTMLElement; - title!.innerText = msg; - const btn = this.msgView.querySelector('#msg-ok-btn') as HTMLElement; - btn.setAttr('style', 'display: block;'); - this.msgView.setAttr('style', 'display: flex;'); - const cancelBtn = this.msgView.querySelector('#msg-cancel-btn') as HTMLElement; - cancelBtn.setAttr('style', 'display: none;'); - this.msgView.setAttr('style', 'display: flex;'); - } - - buildToolbar(parent: HTMLDivElement) { - this.toolbar = parent.createDiv({ cls: 'preview-toolbar' }); - let lineDiv; - - // 平台选择器(新增)- 始终显示 - lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line platform-selector-line' }); - - const platformLabel = lineDiv.createDiv({ cls: 'style-label' }); - platformLabel.innerText = '发布平台'; - - const platformSelect = lineDiv.createEl('select', { cls: 'platform-select' }); - - // 添加平台选项 - const wechatOption = platformSelect.createEl('option'); - wechatOption.value = 'wechat'; - wechatOption.text = '微信公众号'; - wechatOption.selected = true; - - const xiaohongshuOption = platformSelect.createEl('option'); - xiaohongshuOption.value = 'xiaohongshu'; - xiaohongshuOption.text = '小红书'; - - platformSelect.onchange = async () => { - this.currentPlatform = platformSelect.value; - await this.onPlatformChanged(); - }; - - this.platformSelect = platformSelect; - - // 公众号 - if (this.settings.wxInfo.length > 1 || Platform.isDesktop) { - lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line wechat-only' }); - - const wxLabel = lineDiv.createDiv({ cls: 'style-label' }); - wxLabel.innerText = '公众号'; - - const wxSelect = lineDiv.createEl('select', { cls: 'wechat-select' }); - wxSelect.onchange = async () => { - this.currentAppId = wxSelect.value; - this.onAppIdChanged(); - } - const defautlOp =wxSelect.createEl('option'); - defautlOp.value = ''; - defautlOp.text = '请在设置里配置公众号'; - for (let i = 0; i < this.settings.wxInfo.length; i++) { - const op = wxSelect.createEl('option'); - const wx = this.settings.wxInfo[i]; - op.value = wx.appid; - op.text = wx.name; - if (i== 0) { - op.selected = true - this.currentAppId = wx.appid; - } - } - this.wechatSelect = wxSelect; - - if (Platform.isDesktop) { - // 分隔线 - const separator = lineDiv.createDiv({ cls: 'toolbar-separator' }); - - const openBtn = lineDiv.createEl('button', { text: '🌐 去公众号后台', cls: 'toolbar-button purple-gradient' }); - - openBtn.onclick = async () => { - const { shell } = require('electron'); - shell.openExternal('https://mp.weixin.qq.com') - uevent('open-mp'); - } - } - } - else if (this.settings.wxInfo.length > 0) { - this.currentAppId = this.settings.wxInfo[0].appid; - } - - // 复制,刷新,带图片复制,发草稿箱 - lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line wechat-only flex-wrap' }); - - const refreshBtn = lineDiv.createEl('button', { text: '🔄 刷新', cls: 'toolbar-button purple-gradient' }); - - refreshBtn.onclick = async () => { - await this.assetsManager.loadCustomCSS(); - await this.assetsManager.loadExpertSettings(); - this.render.reloadStyle(); - await this.renderMarkdown(); - uevent('refresh'); - } - if (Platform.isDesktop) { - const copyBtn = lineDiv.createEl('button', { text: '📋 复制', cls: 'toolbar-button' }); - - copyBtn.onclick = async() => { - try { - await this.render.copyArticle(); - new Notice('复制成功,请到公众号编辑器粘贴。'); - uevent('copy'); - } catch (error) { - console.error(error); - new Notice('复制失败: ' + error); - } - } - } - - const uploadImgBtn = lineDiv.createEl('button', { text: '📤 上传图片', cls: 'toolbar-button' }); - - uploadImgBtn.onclick = async() => { - await this.uploadImages(); - uevent('upload'); - } - - const postBtn = lineDiv.createEl('button', { text: '📝 发草稿', cls: 'toolbar-button' }); - - postBtn.onclick = async() => { - await this.postArticle(); - uevent('pub'); - } - - const imagesBtn = lineDiv.createEl('button', { text: '🖼️ 图片/文字', cls: 'toolbar-button' }); - - imagesBtn.onclick = async() => { - await this.postImages(); - uevent('pub-images'); - } - - if (Platform.isDesktop && this.settings.isAuthKeyVaild()) { - const htmlBtn = lineDiv.createEl('button', { text: '💾 导出HTML', cls: 'toolbar-button' }); - - htmlBtn.onclick = async() => { - await this.exportHTML(); - uevent('export-html'); - } - } - - // 封面 - lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line wechat-only' }); - - const coverTitle = lineDiv.createDiv({ cls: 'style-label' }); - coverTitle.innerText = '封面'; - - this.useDefaultCover = lineDiv.createEl('input', { cls: 'input-style' }); - this.useDefaultCover.setAttr('type', 'radio'); - this.useDefaultCover.setAttr('name', 'cover'); - this.useDefaultCover.setAttr('value', 'default'); - this.useDefaultCover.setAttr('checked', true); - this.useDefaultCover.id = 'default-cover'; - this.useDefaultCover.onchange = async () => { - if (this.useDefaultCover.checked) { - this.coverEl.setAttr('style', 'visibility:hidden;width:0px;'); - } - else { - this.coverEl.setAttr('style', 'visibility:visible;width:180px;'); - } - } - const defaultLable = lineDiv.createEl('label'); - defaultLable.innerText = '默认'; - defaultLable.setAttr('for', 'default-cover'); - - this.useLocalCover = lineDiv.createEl('input', { cls: 'input-style' }); - this.useLocalCover.setAttr('type', 'radio'); - this.useLocalCover.setAttr('name', 'cover'); - this.useLocalCover.setAttr('value', 'local'); - this.useLocalCover.id = 'local-cover'; - this.useLocalCover.setAttr('style', 'margin-left:20px;'); - this.useLocalCover.onchange = async () => { - if (this.useLocalCover.checked) { - this.coverEl.setAttr('style', 'visibility:visible;width:180px;'); - } - else { - this.coverEl.setAttr('style', 'visibility:hidden;width:0px;'); - } - } - - const localLabel = lineDiv.createEl('label'); - localLabel.setAttr('for', 'local-cover'); - localLabel.innerText = '上传'; - - this.coverEl = lineDiv.createEl('input', { cls: 'upload-input' }); - this.coverEl.setAttr('type', 'file'); - this.coverEl.setAttr('placeholder', '封面图片'); - this.coverEl.setAttr('accept', '.png, .jpg, .jpeg'); - this.coverEl.setAttr('name', 'cover'); - this.coverEl.id = 'cover-input'; - - // 样式 - if (this.settings.showStyleUI) { - lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line wechat-only flex-wrap' }); - - const cssStyle = lineDiv.createDiv({ cls: 'style-label' }); - cssStyle.innerText = '样式'; - - const selectBtn = lineDiv.createEl('select', { cls: 'style-select' }); - - selectBtn.onchange = async () => { - this.currentTheme = selectBtn.value; - this.render.updateStyle(selectBtn.value); - } - - for (let s of this.assetsManager.themes) { - const op = selectBtn.createEl('option'); - op.value = s.className; - op.text = s.name; - op.selected = s.className == this.settings.defaultStyle; - } - - this.themeSelect = selectBtn; - - // 分隔线 - const separator = lineDiv.createDiv({ cls: 'toolbar-separator' }); - - const highlightStyle = lineDiv.createDiv({ cls: 'style-label' }); - highlightStyle.innerText = '代码高亮'; - - const highlightStyleBtn = lineDiv.createEl('select', { cls: 'style-select' }); - - highlightStyleBtn.onchange = async () => { - this.currentHighlight = highlightStyleBtn.value; - this.render.updateHighLight(highlightStyleBtn.value); - } - - for (let s of this.assetsManager.highlights) { - const op = highlightStyleBtn.createEl('option'); - op.value = s.name; - op.text = s.name; - op.selected = s.name == this.settings.defaultHighlight; - } - - this.highlightSelect = highlightStyleBtn; - } - - this.buildMsgView(this.toolbar); - } - - async buildUI() { - this.container = this.containerEl.children[1]; - this.container.empty(); - - this.mainDiv = this.container.createDiv({ cls: 'note-preview' }); - - this.buildToolbar(this.mainDiv); - - this.renderDiv = this.mainDiv.createDiv({cls: 'render-div'}); - this.renderDiv.id = 'render-div'; - this.styleEl = this.renderDiv.createEl('style'); - this.styleEl.setAttr('title', 'note-to-mp-style'); - this.articleDiv = this.renderDiv.createEl('div'); - } - - async viewLoading() { - const container = this.containerEl.children[1] - container.empty(); - const loading = container.createDiv({cls: 'loading-wrapper'}) - loading.createDiv({cls: 'loading-spinner'}) - } - - async renderMarkdown(af: TFile | null = null) { - if (!af) { - af = this.app.workspace.getActiveFile(); - } - if (!af || af.extension.toLocaleLowerCase() !== 'md') { - return; - } - this.currentFile = af; - // 如果关闭了样式 UI,则在渲染前强制使用全局默认样式/高亮(忽略 frontmatter 中的 theme/highlight) - if (!this.settings.showStyleUI) { - const globalStyle = this.settings.defaultStyle; - const globalHighlight = this.settings.defaultHighlight; - // 仅当变更时更新当前与 articleRender 中的值,避免不必要的刷新 - if (this.currentTheme !== globalStyle) { - this.currentTheme = globalStyle; - if (this._articleRender) { - this._articleRender.currentTheme = globalStyle; - } - } - if (this.currentHighlight !== globalHighlight) { - this.currentHighlight = globalHighlight; - if (this._articleRender) { - this._articleRender.currentHighlight = globalHighlight; - } - } - } - await this.render.renderMarkdown(af); - const metadata = this.render.getMetadata(); - if (metadata.appid) { - this.wechatSelect.value = metadata.appid; - } - else { - this.wechatSelect.value = this.currentAppId; - } - // 仅当 UI 开启时才允许 frontmatter 覆盖与下拉同步;关闭时忽略 frontmatter 的 theme/highlight - if (this.settings.showStyleUI) { - if (metadata.theme) { - this.assetsManager.themes.forEach(theme => { - if (theme.name === metadata.theme) { - this.currentTheme = theme.className; - if (this.themeSelect) this.themeSelect.value = theme.className; - if (this._articleRender) this._articleRender.currentTheme = theme.className; - } - }); - } else if (this.themeSelect) { - this.themeSelect.value = this.currentTheme; - } - - if (metadata.highlight) { - this.currentHighlight = metadata.highlight; - if (this.highlightSelect) this.highlightSelect.value = metadata.highlight; - if (this._articleRender) this._articleRender.currentHighlight = metadata.highlight; - } else if (this.highlightSelect) { - this.highlightSelect.value = this.currentHighlight; - } - } - - // 如果当前是小红书平台,更新小红书预览 - if (this.currentPlatform === 'xiaohongshu' && this._xiaohongshuPreview) { - this.articleHTML = this.render.articleHTML; - await this._xiaohongshuPreview.renderArticle(this.articleHTML, af); - } - } - - /** - * 平台切换处理 - * 当用户切换发布平台时调用 - */ - async onPlatformChanged() { - console.log(`[NotePreview] 平台切换至: ${this.currentPlatform}`); - - if (this.currentPlatform === 'xiaohongshu') { - // 切换到小红书预览模式 - await this.switchToXiaohongshuMode(); - } else { - // 切换到微信公众号模式 - this.switchToWechatMode(); - } - } - - /** - * 切换到小红书预览模式 - */ - private async switchToXiaohongshuMode() { - // 隐藏微信相关的工具栏行和平台选择器 - if (this.toolbar) { - const wechatLines = this.toolbar.querySelectorAll('.wechat-only'); - wechatLines.forEach((line: HTMLElement) => { - line.style.display = 'none'; - }); - - // 也隐藏平台选择器行 - // const platformLine = this.toolbar.querySelector('.platform-selector-line') as HTMLElement; - // if (platformLine) { - // platformLine.style.display = 'none'; - // } - } - - // 隐藏渲染区域 - if (this.renderDiv) this.renderDiv.style.display = 'none'; - - // 创建或显示小红书预览视图 - if (!this._xiaohongshuPreview) { - const xhsContainer = this.mainDiv.createDiv({ cls: 'xiaohongshu-preview-container' }); - this._xiaohongshuPreview = new XiaohongshuPreview(xhsContainer, this.app); - - // 设置回调函数 - this._xiaohongshuPreview.onRefreshCallback = async () => { - await this.onXiaohongshuRefresh(); - }; - this._xiaohongshuPreview.onPublishCallback = async () => { - await this.onXiaohongshuPublish(); - }; - this._xiaohongshuPreview.onPlatformChangeCallback = async (platform: string) => { - this.currentPlatform = platform; - if (platform === 'wechat') { - await this.onPlatformChanged(); - } - }; - - this._xiaohongshuPreview.build(); - } else { - const xhsContainer = this.mainDiv.querySelector('.xiaohongshu-preview-container') as HTMLElement; - if (xhsContainer) xhsContainer.style.display = 'flex'; - } - - // 如果有当前文件,渲染小红书预览 - if (this.currentFile) { - // 如果还没有生成 articleHTML,先生成它 - if (!this.articleHTML) { - await this.render.renderMarkdown(this.currentFile); - this.articleHTML = this.render.articleHTML; - } - - // 渲染到小红书预览 - if (this.articleHTML) { - await this._xiaohongshuPreview.renderArticle(this.articleHTML, this.currentFile); - } - } - } - - /** - * 切换到微信公众号模式 - */ - private switchToWechatMode() { - // 显示微信相关的工具栏行和平台选择器 - if (this.toolbar) { - const wechatLines = this.toolbar.querySelectorAll('.wechat-only'); - wechatLines.forEach((line: HTMLElement) => { - line.style.display = 'flex'; - }); - - // 也显示平台选择器行 - const platformLine = this.toolbar.querySelector('.platform-selector-line') as HTMLElement; - if (platformLine) { - platformLine.style.display = 'flex'; - } - } - - // 显示渲染区域 - if (this.renderDiv) this.renderDiv.style.display = 'block'; - - // 隐藏小红书预览视图 - const xhsContainer = this.mainDiv.querySelector('.xiaohongshu-preview-container') as HTMLElement; - if (xhsContainer) xhsContainer.style.display = 'none'; - } - - /** - * 更新按钮文本为微信公众号相关 - */ - private updateButtonsForWechat() { - const buttons = this.toolbar.querySelectorAll('button'); - buttons.forEach(button => { - const text = button.textContent; - if (text === '发布到小红书') { - button.textContent = '发草稿'; - } else if (text === '上传图片(小红书)') { - button.textContent = '上传图片'; - } - }); - } - - /** - * 更新按钮文本为小红书相关 - */ - private updateButtonsForXiaohongshu() { - const buttons = this.toolbar.querySelectorAll('button'); - buttons.forEach(button => { - const text = button.textContent; - if (text === '发草稿') { - button.textContent = '发布到小红书'; - } else if (text === '上传图片') { - button.textContent = '上传图片(小红书)'; - } - }); - } - - async uploadImages() { - if (this.currentPlatform === 'wechat') { - await this.uploadImagesToWechat(); - } else if (this.currentPlatform === 'xiaohongshu') { - await this.uploadImagesToXiaohongshu(); - } - } - - /** - * 上传图片到微信公众号 - */ - async uploadImagesToWechat() { - this.showLoading('图片上传中...'); - try { - await this.render.uploadImages(this.currentAppId); - this.showMsg('图片上传成功,并且文章内容已复制,请到公众号编辑器粘贴。'); - } catch (error) { - this.showMsg('图片上传失败: ' + error.message); - } - } - - /** - * 上传图片到小红书 - */ - async uploadImagesToXiaohongshu() { - this.showLoading('处理图片中...'); - try { - // 获取小红书适配器和图片处理器 - const adapter = new XiaohongshuContentAdapter(); - const imageHandler = XiaohongshuImageManager.getInstance(); - - // 获取当前文档的图片 - const imageManager = LocalImageManager.getInstance(); - const images = imageManager.getImageInfos(this.articleDiv); - - if (images.length === 0) { - this.showMsg('当前文档没有图片需要处理'); - return; - } - - // 处理图片:转换为PNG格式 - const imageBlobs: { name: string; blob: Blob }[] = []; - for (const img of images) { - // 从filePath获取文件 - const file = this.app.vault.getAbstractFileByPath(img.filePath); - if (file && file instanceof TFile) { - const fileData = await this.app.vault.readBinary(file); - imageBlobs.push({ - name: file.name, - blob: new Blob([fileData]) - }); - } - } - - const processedImages = await imageHandler.processImages(imageBlobs); - - this.showMsg(`成功处理 ${processedImages.length} 张图片,已转换为PNG格式`); - } catch (error) { - this.showMsg('图片处理失败: ' + error.message); - } - } - - async postArticle() { - if (this.currentPlatform === 'wechat') { - await this.postToWechat(); - } else if (this.currentPlatform === 'xiaohongshu') { - await this.postToXiaohongshu(); - } - } - - /** - * 发布到微信公众号草稿 - */ - async postToWechat() { - let localCover = null; - if (this.useLocalCover.checked) { - const fileInput = this.coverEl; - if (!fileInput.files || fileInput.files.length === 0) { - this.showMsg('请选择封面文件'); - return; - } - localCover = fileInput.files[0]; - if (!localCover) { - this.showMsg('请选择封面文件'); - return; - } - } - this.showLoading('发布中...'); - try { - await this.render.postArticle(this.currentAppId, localCover); - this.showMsg('发布成功'); - } - catch (error) { - this.showMsg('发布失败: ' + error.message); - } - } - - /** - * 发布到小红书 - */ - async postToXiaohongshu() { - this.showLoading('发布到小红书中...'); - try { - if (!this.currentFile) { - this.showMsg('没有可发布的文件'); - return; - } - - // 读取文件内容 - const fileContent = await this.app.vault.read(this.currentFile); - - // 使用小红书适配器转换内容 - const adapter = new XiaohongshuContentAdapter(); - const xiaohongshuPost = adapter.adaptMarkdownToXiaohongshu(fileContent, { - addStyle: true, - generateTitle: true - }); - - // 验证内容 - const validation = adapter.validatePost(xiaohongshuPost); - if (!validation.valid) { - this.showMsg('内容验证失败: ' + validation.errors.join('; ')); - return; - } - - // 获取小红书API实例 - const api = XiaohongshuAPIManager.getInstance(false); // 暂时使用false - - // 检查登录状态 - const isLoggedIn = await api.checkLoginStatus(); - if (!isLoggedIn) { - this.showMsg('请先登录小红书,或检查登录状态'); - return; - } - - // 发布内容 - const result = await api.createPost(xiaohongshuPost); - - if (result.success) { - this.showMsg('发布到小红书成功!'); - } else { - this.showMsg('发布失败: ' + result.message); - } - } - catch (error) { - this.showMsg('发布失败: ' + error.message); - } - } - - /** - * 小红书预览的刷新回调 - */ - async onXiaohongshuRefresh() { - await this.assetsManager.loadCustomCSS(); - await this.assetsManager.loadExpertSettings(); - // 更新小红书预览的样式 - if (this._xiaohongshuPreview) { - this._xiaohongshuPreview.assetsManager = this.assetsManager; - } - await this.renderMarkdown(); - new Notice('刷新成功'); - } - - /** - * 小红书预览的发布回调 - */ - async onXiaohongshuPublish() { - await this.postToXiaohongshu(); - } - - async postImages() { - this.showLoading('发布图片中...'); - try { - await this.render.postImages(this.currentAppId); - this.showMsg('图片发布成功'); - } catch (error) { - this.showMsg('图片发布失败: ' + error.message); - } - } - - async exportHTML() { - this.showLoading('导出HTML中...'); - try { - await this.render.exportHTML(); - this.showMsg('HTML导出成功'); - } catch (error) { - this.showMsg('HTML导出失败: ' + error.message); - } - } - - async sliceArticleImage() { - if (!this.currentFile) { - new Notice('请先打开一个笔记文件'); - return; - } - this.showLoading('切图处理中...'); - try { - const articleSection = this.render.getArticleSection(); - if (!articleSection) { - throw new Error('未找到预览区域'); - } - await sliceArticleImage(articleSection, this.currentFile, this.app); - this.showMsg('切图完成'); - } catch (error) { - console.error('切图失败:', error); - this.showMsg('切图失败: ' + error.message); - } - } - - async batchPost(folder: TFolder) { - const files = folder.children.filter((child: TAbstractFile) => child.path.toLocaleLowerCase().endsWith('.md')); - if (!files) { - new Notice('没有可渲染的笔记或文件不支持渲染'); - return; - } - - this.isCancelUpload = false; - this.isBatchRuning = true; - - try { - for (let file of files) { - this.showLoading(`即将发布: ${file.name}`, true); - await sleep(5000); - if (this.isCancelUpload) { - break; - } - this.cleanArticleData(); - await this.renderMarkdown(file as TFile); - await this.postArticle(); - } - - if (!this.isCancelUpload) { - this.showMsg(`批量发布完成:成功发布 ${files.length} 篇笔记`); - } - } - catch (e) { - console.error(e); - new Notice('批量发布失败: ' + e.message); - } - finally { - this.isBatchRuning = false; - this.isCancelUpload = false; - } - } -} \ No newline at end of file diff --git a/src/slice-image.ts b/src/slice-image.ts deleted file mode 100644 index c0d1f08..0000000 --- a/src/slice-image.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* 文件:slice-image.ts — 预览页面切图功能:将渲染完的 HTML 页面转为长图,再按比例裁剪为多张 PNG 图片。 */ - -import { toPng } from 'html-to-image'; -import { Notice, TFile } from 'obsidian'; -import { NMPSettings } from './settings'; -import * as fs from 'fs'; -import * as path from 'path'; - -/** - * 解析横竖比例字符串(如 "3:4")为数值 - */ -function parseAspectRatio(ratio: string): { width: number; height: number } { - const parts = ratio.split(':').map(p => parseFloat(p.trim())); - if (parts.length === 2 && parts[0] > 0 && parts[1] > 0) { - return { width: parts[0], height: parts[1] }; - } - // 默认 3:4 - return { width: 3, height: 4 }; -} - -/** - * 从 frontmatter 获取 slug,若不存在则使用文件名(去除扩展名) - */ -function getSlugFromFile(file: TFile, app: any): string { - const cache = app.metadataCache.getFileCache(file); - if (cache?.frontmatter?.slug) { - return String(cache.frontmatter.slug).trim(); - } - return file.basename; -} - -/** - * 确保目录存在 - */ -function ensureDir(dirPath: string) { - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - } -} - -/** - * 将 base64 dataURL 转为 Buffer - */ -function dataURLToBuffer(dataURL: string): Buffer { - const base64 = dataURL.split(',')[1]; - return Buffer.from(base64, 'base64'); -} - -/** - * 切图主函数 - * @param articleElement 预览文章的 HTML 元素(#article-section) - * @param file 当前文件 - * @param app Obsidian App 实例 - */ -export async function sliceArticleImage(articleElement: HTMLElement, file: TFile, app: any) { - const settings = NMPSettings.getInstance(); - const { sliceImageSavePath, sliceImageWidth, sliceImageAspectRatio } = settings; - - // 解析比例 - const ratio = parseAspectRatio(sliceImageAspectRatio); - const sliceHeight = Math.round((sliceImageWidth * ratio.height) / ratio.width); - - // 获取 slug - const slug = getSlugFromFile(file, app); - - new Notice(`开始切图:${slug},宽度=${sliceImageWidth},比例=${sliceImageAspectRatio}`); - - try { - // 1. 保存原始样式 - const originalWidth = articleElement.style.width; - const originalMaxWidth = articleElement.style.maxWidth; - const originalMinWidth = articleElement.style.minWidth; - - // 2. 临时设置为目标宽度进行渲染 - articleElement.style.width = `${sliceImageWidth}px`; - articleElement.style.maxWidth = `${sliceImageWidth}px`; - articleElement.style.minWidth = `${sliceImageWidth}px`; - - // 等待样式生效和重排 - await new Promise(resolve => setTimeout(resolve, 100)); - - new Notice(`设置渲染宽度: ${sliceImageWidth}px`); - - // 3. 生成长图 - 使用实际渲染宽度 - new Notice('正在生成长图...'); - const longImageDataURL = await toPng(articleElement, { - width: sliceImageWidth, - pixelRatio: 1, - cacheBust: true, - }); - - // 4. 恢复原始样式 - articleElement.style.width = originalWidth; - articleElement.style.maxWidth = originalMaxWidth; - articleElement.style.minWidth = originalMinWidth; - - // 5. 创建临时 Image 对象以获取长图实际高度 - const img = new Image(); - await new Promise((resolve, reject) => { - img.onload = () => resolve(); - img.onerror = reject; - img.src = longImageDataURL; - }); - - const fullHeight = img.height; - const fullWidth = img.width; - - new Notice(`长图生成完成:${fullWidth}x${fullHeight}px`); - - // 3. 保存完整长图 - ensureDir(sliceImageSavePath); - const longImagePath = path.join(sliceImageSavePath, `${slug}.png`); - const longImageBuffer = dataURLToBuffer(longImageDataURL); - fs.writeFileSync(longImagePath, new Uint8Array(longImageBuffer)); - new Notice(`长图已保存:${longImagePath}`); - - // 4. 计算需要切多少片 - const sliceCount = Math.ceil(fullHeight / sliceHeight); - new Notice(`开始切图:共 ${sliceCount} 张,每张 ${sliceImageWidth}x${sliceHeight}px`); - - // 5. 使用 Canvas 裁剪 - const canvas = document.createElement('canvas'); - canvas.width = sliceImageWidth; - canvas.height = sliceHeight; - const ctx = canvas.getContext('2d'); - - if (!ctx) { - throw new Error('无法创建 Canvas 上下文'); - } - - for (let i = 0; i < sliceCount; i++) { - const yOffset = i * sliceHeight; - const actualHeight = Math.min(sliceHeight, fullHeight - yOffset); - - // 清空画布(处理最后一张可能不足高度的情况,用白色填充) - ctx.fillStyle = '#ffffff'; - ctx.fillRect(0, 0, sliceImageWidth, sliceHeight); - - // 绘制裁剪区域 - ctx.drawImage( - img, - 0, yOffset, fullWidth, actualHeight, // 源区域 - 0, 0, sliceImageWidth, actualHeight // 目标区域 - ); - - // 导出为 PNG - const sliceDataURL = canvas.toDataURL('image/png'); - const sliceBuffer = dataURLToBuffer(sliceDataURL); - const sliceFilename = `${slug}_${i + 1}.png`; - const slicePath = path.join(sliceImageSavePath, sliceFilename); - fs.writeFileSync(slicePath, new Uint8Array(sliceBuffer)); - - new Notice(`已保存:${sliceFilename}`); - } - - new Notice(`✅ 切图完成!共 ${sliceCount} 张图片,保存在:${sliceImageSavePath}`); - } catch (error) { - console.error('切图失败:', error); - new Notice(`❌ 切图失败:${error.message}`); - } -} diff --git a/src/xiaohongshu/xhs-preview.ts b/src/xiaohongshu/xhs-preview.ts index 7585866..fe54290 100644 --- a/src/xiaohongshu/xhs-preview.ts +++ b/src/xiaohongshu/xhs-preview.ts @@ -17,6 +17,9 @@ import { sliceCurrentPage, sliceAllPages } from './slice'; const XHS_PREVIEW_DEFAULT_WIDTH = 540; const XHS_PREVIEW_WIDTH_OPTIONS = [1080, 720, 540, 360]; +const XHS_FONT_SIZE_MIN = 18; +const XHS_FONT_SIZE_MAX = 45; +const XHS_FONT_SIZE_DEFAULT = 36; /** * 小红书预览视图类 @@ -45,7 +48,7 @@ export class XiaohongshuPreview { // 分页数据 pages: PageInfo[] = []; currentPageIndex: number = 0; - currentFontSize: number = 16; + currentFontSize: number = XHS_FONT_SIZE_DEFAULT; articleHTML: string = ''; // 回调函数 @@ -147,9 +150,14 @@ export class XiaohongshuPreview { const decreaseBtn = fontSizeGroup.createEl('button', { text: '−', cls: 'font-size-btn' }); decreaseBtn.onclick = () => this.changeFontSize(-1); - this.fontSizeInput = fontSizeGroup.createEl('input', { + this.fontSizeInput = fontSizeGroup.createEl('input', { cls: 'font-size-input', - attr: { type: 'number', min: '12', max: '36', value: '16' } + attr: { + type: 'number', + min: String(XHS_FONT_SIZE_MIN), + max: String(XHS_FONT_SIZE_MAX), + value: String(XHS_FONT_SIZE_DEFAULT) + } }); this.fontSizeInput.style.width = '50px'; this.fontSizeInput.style.textAlign = 'center'; @@ -363,7 +371,7 @@ export class XiaohongshuPreview { * 切换字号(± 按钮) */ private async changeFontSize(delta: number): Promise { - this.currentFontSize = Math.max(12, Math.min(36, this.currentFontSize + delta)); + this.currentFontSize = Math.max(XHS_FONT_SIZE_MIN, Math.min(XHS_FONT_SIZE_MAX, this.currentFontSize + delta)); this.fontSizeInput.value = String(this.currentFontSize); await this.repaginateAndRender(); } @@ -373,9 +381,9 @@ export class XiaohongshuPreview { */ private async onFontSizeInputChanged(): Promise { const val = parseInt(this.fontSizeInput.value, 10); - if (isNaN(val) || val < 12 || val > 36) { + if (isNaN(val) || val < XHS_FONT_SIZE_MIN || val > XHS_FONT_SIZE_MAX) { this.fontSizeInput.value = String(this.currentFontSize); - new Notice('字号范围: 12-36'); + new Notice(`字号范围: ${XHS_FONT_SIZE_MIN}-${XHS_FONT_SIZE_MAX}`); return; } this.currentFontSize = val;