diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml index 22b1735f..45116c49 100644 --- a/.github/workflows/build-check.yml +++ b/.github/workflows/build-check.yml @@ -187,14 +187,14 @@ jobs: - name: Target id: target shell: bash - run: echo "target=${{ matrix.platform }}-${{ matrix.arch }}" >> $GITHUB_OUTPUT + run: echo "TARGET=${{ matrix.platform }}-${{ matrix.arch }}" >> $GITHUB_OUTPUT - name: Package run: | - npx @vscode/vsce package --pre-release --target ${{ steps.target.outputs.target }} --no-update-package-json --no-git-tag-version ${{ steps.version.outputs.VERSION }} + npx @vscode/vsce package --pre-release --target ${{ steps.target.outputs.TARGET }} --no-update-package-json --no-git-tag-version ${{ steps.version.outputs.VERSION }} - name: Upload artifact uses: actions/upload-artifact@v3 with: - name: ${{ steps.target.outputs.target }} + name: ${{ steps.target.outputs.TARGET }} path: '*.vsix' diff --git a/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml index 2558dccb..3a1e6043 100644 --- a/.github/workflows/marketplace-publish.yml +++ b/.github/workflows/marketplace-publish.yml @@ -56,7 +56,8 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - name: Check out + uses: actions/checkout@v3 - name: Setup node.js uses: actions/setup-node@v3 @@ -97,16 +98,16 @@ jobs: - name: Target id: target shell: bash - run: echo "target=${{ matrix.platform }}-${{ matrix.arch }}" >> $GITHUB_OUTPUT + run: echo "TARGET=${{ matrix.platform }}-${{ matrix.arch }}" >> $GITHUB_OUTPUT - name: Package run: | - npx @vscode/vsce package --target ${{ steps.target.outputs.target }} --no-git-tag-version --no-update-package-json ${{ steps.version.outputs.VERSION }} + npx @vscode/vsce package --target ${{ steps.target.outputs.TARGET }} --no-git-tag-version --no-update-package-json ${{ steps.version.outputs.VERSION }} - name: Upload artifact uses: actions/upload-artifact@v3 with: - name: ${{ steps.target.outputs.target }} + name: ${{ steps.target.outputs.TARGET }} path: '*.vsix' publish: diff --git a/package.json b/package.json index cfe7b7c2..40549d74 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ "onView:cnblogs-account", "onView:cnblogs-post-list", "onView:vscode-cnb-workspace", - "onCommand:vscode-cnb.open-workspace", - "onCommand:vscode-cnb.webLogin", + "onCommand:vscode-cnb.workspace.code-open", + "onCommand:vscode-cnb.login.web", "onCommand:vscode-cnb.logout", "onCommand:vscode-cnb.ing-publish-select" ], @@ -125,7 +125,7 @@ { "title": "登录到博客园", "enablement": "!vscode-cnb.isAuthorized", - "command": "vscode-cnb.webLogin" + "command": "vscode-cnb.login.web" }, { "command": "vscode-cnb.logout", @@ -135,214 +135,221 @@ "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.post-list.prev", + "command": "vscode-cnb.post.list-view.prev", "title": "上一页", "icon": "$(chevron-left)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post-list.refreshing && vscode-cnb.post-list.hasPrev", + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view.refreshing && vscode-cnb.post.list-view.hasPrev", "category": "Cnblogs Post List" }, { - "command": "vscode-cnb.post-list.seek", + "command": "vscode-cnb.post.list-view.seek", "title": "跳页", "icon": "$(vscode-cnb-icon-seek)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post-list.refreshing && vscode-cnb.post-list.pageCount > 0", + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view.refreshing && vscode-cnb.post.list-view.pageCount > 0", "category": "Cnblogs Post List" }, { - "command": "vscode-cnb.post-list.next", + "command": "vscode-cnb.post.list-view.next", "title": "下一页", "icon": "$(chevron-right)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post-list.refreshing && vscode-cnb.post-list.hasNext", + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view.refreshing && vscode-cnb.post.list-view.hasNext", "category": "Cnblogs Post List" }, { - "command": "vscode-cnb.refresh-post-list", + "command": "vscode-cnb.post.list-view.refresh", "title": "刷新", "icon": "$(refresh)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post-list.refreshing", + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view.refreshing", "category": "Cnblogs Post List" }, { - "command": "vscode-cnb.del-post", + "command": "vscode-cnb.post.pull-all", + "title": "下载全部随笔", + "icon": "$(cloud-download)", + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view.refreshing && vscode-cnb.post.list-view.pageCount > 0", + "category": "Cnblogs Post List" + }, + { + "command": "vscode-cnb.post.del", "title": "删除随笔(支持多选)", "icon": "$(trash)", "category": "Cnblogs Post List", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.create-local-draft", + "command": "vscode-cnb.post.create-local-draft", "title": "新建本地草稿", "icon": "$(new-file)", "category": "Cnblogs Local Draft" }, { - "command": "vscode-cnb.modify-post-setting", + "command": "vscode-cnb.post.modify-setting", "title": "博文设置", "icon": "$(gear)", "category": "Cnblogs Post List", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.upload-post", + "command": "vscode-cnb.post.upload", "title": "上传博文", "icon": "$(cloud-upload)", "category": "Cnblogs Post List", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.upload-post-file", + "command": "vscode-cnb.post.upload-file", "title": "上传到博客园", "icon": "$(vscode-cnb-cloud-upload)", "enablement": "vscode-cnb.isAuthorized", "category": "Cnblogs" }, { - "command": "vscode-cnb.upload-post-no-confirm", + "command": "vscode-cnb.post.upload-no-confirm", "title": "上传博文", "icon": "$(cloud-upload)", "category": "Cnblogs Post List", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.upload-post-file-no-confirm", + "command": "vscode-cnb.post.upload-file-no-confirm", "title": "上传到博客园", "icon": "$(vscode-cnb-cloud-upload)", "enablement": "vscode-cnb.isAuthorized", "category": "Cnblogs" }, { - "command": "vscode-cnb.upload-img", + "command": "vscode-cnb.img.upload", "title": "上传图片到博客园", "category": "Cnblogs", "icon": "$(vscode-cnb-image-upload)", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.upload-img-clipboard", + "command": "vscode-cnb.img.upload-clipboard", "title": "上传剪贴板图片到博客园", "category": "Cnblogs", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.upload-img-fs", + "command": "vscode-cnb.img.upload-fs", "title": "上传本地图片到博客园", "category": "Cnblogs", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.reveal-local-post-file-in-os", + "command": "vscode-cnb.post.os-open-local-file", "title": "在文件资源管理器中打开", "category": "Cnblogs" }, { - "command": "vscode-cnb.pull-remote-post", + "command": "vscode-cnb.post.pull", "title": "拉取博文", "category": "Cnblogs", "enablement": "vscode-cnb.isAuthorized", "icon": "$(cloud-download)" }, { - "command": "vscode-cnb.show-post-to-local-file-info", + "command": "vscode-cnb.post.show-local-file-info", "title": "博客园关联博文", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.new-post-category", + "command": "vscode-cnb.post-category.new", "title": "新建博文分类", "icon": "$(add)", "category": "Cnblogs Post Categories Management", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.del-selected-post-category", + "command": "vscode-cnb.post-category.del-select", "title": "删除", "icon": "$(trash)", "category": "Cnblogs Post Categories Management", "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.postCategoriesList.isRefreshing" }, { - "command": "vscode-cnb.refresh-post-category-list", + "command": "vscode-cnb.post-category.refresh", "title": "刷新", "icon": "$(refresh)", "category": "Cnblogs Post Categories Management", "enablement": "!vscode-cnb.postCategoriesList.isRefreshing" }, { - "command": "vscode-cnb.update-post-category", + "command": "vscode-cnb.post-category.update", "title": "编辑", "icon": "$(edit)", "category": "Cnblogs Post Categories Management", "enablement": "!vscode-cnb.postCategoriesList.isRefreshing" }, { - "command": "vscode-cnb.del-post-to-local-file-map", + "command": "vscode-cnb.post.del-local-map", "title": "取消关联本地文件(支持多选)", "category": "Cnblogs Post List" }, { - "command": "vscode-cnb.rename-post", + "command": "vscode-cnb.post.rename", "title": "重命名博文", "category": "Cnblogs Post List", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.open-post-in-blog-admin", + "command": "vscode-cnb.post.open-in-blog-admin", "title": "在博客后台中编辑", "category": "Cnblogs", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.open-workspace", + "command": "vscode-cnb.workspace.code-open", "title": "打开工作空间", "category": "Cnblogs" }, { - "command": "vscode-cnb.view-post-online", + "command": "vscode-cnb.post.view-in-browser", "title": "在线查看博文", "category": "Cnblogs" }, { - "command": "vscode-cnb.export-post-to-pdf", + "command": "vscode-cnb.post.export-to-pdf", "title": "导出 PDF", "category": "Cnblogs", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.extract-img", + "command": "vscode-cnb.img.extract", "title": "提取图片", "category": "Cnblogs", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.search-post", + "command": "vscode-cnb.post.search", "title": "搜索博文", "category": "Cnblogs", "icon": "$(vscode-cnb-post-list-search)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post-list.refreshing" + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view.refreshing" }, { - "command": "vscode-cnb.clear-post-search-results", + "command": "vscode-cnb.post.list-view.search.clear", "title": "清除随笔搜索结果", "category": "Cnblogs", "icon": "$(clear-all)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post-list.refreshing" + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view.refreshing" }, { - "command": "vscode-cnb.refresh-post-search-results", + "command": "vscode-cnb.post.list-view.search.refresh", "title": "刷新随笔搜索结果", "category": "Cnblogs", "icon": "$(refresh)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post-list-refreshing" + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view-refreshing" }, { - "command": "vscode-cnb.ing.publish", + "command": "vscode-cnb.ing.pub", "title": "发闪存", "category": "Cnblogs", "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.ingList.isRefreshing", "icon": "$(add)" }, { - "command": "vscode-cnb.ing.publish-select", + "command": "vscode-cnb.ing.pub-select", "title": "将选中内容发到闪存", "category": "Cnblogs", "enablement": "vscode-cnb.isAuthorized && editorHasSelection == true && isInDiffEditor == false && isInEmbeddedEditor == false" @@ -389,58 +396,58 @@ "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.copy-post-link", + "command": "vscode-cnb.post.copy-link", "title": "复制博文链接", "icon": "$(link)", "enablement": "vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.blog-export.refresh-record", + "command": "vscode-cnb.backup.refresh-record", "title": "刷新博客备份记录", "category": "Cnblogs", "icon": "$(refresh)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.blog-export.records.isRefreshing" + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.backup.records.isRefreshing" }, { - "command": "vscode-cnb.blog-export.open-local-export", + "command": "vscode-cnb.backup.open-local", "title": "打开本地已下载博客备份文件", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.blog-export.records.isRefreshing", + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.backup.records.isRefreshing", "icon": "$(file-add)" }, { - "command": "vscode-cnb.blog-export.edit", + "command": "vscode-cnb.backup.edit", "title": "编辑博文", "category": "Cnblogs", "enablement": "vscode-cnb.isAuthorized", "icon": "$(pencil)" }, { - "command": "vscode-cnb.blog-export.create", + "command": "vscode-cnb.backup.create", "title": "新建博客备份", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.blog-export.records.isRefreshing", + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.backup.records.isRefreshing", "icon": "$(add)" }, { - "command": "vscode-cnb.blog-export.download", + "command": "vscode-cnb.backup.download", "title": "下载备份", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.blog-export.records.isRefreshing && !vscode-cnb.blog-export.downloading", + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.backup.records.isRefreshing && !vscode-cnb.backup.downloading", "icon": "$(cloud-download)" }, { - "command": "vscode-cnb.blog-export.view-post", + "command": "vscode-cnb.backup.view-post", "title": "查看备份博文", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.blog-export.records.isRefreshing", + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.backup.records.isRefreshing", "icon": "$(eye)" }, { - "command": "vscode-cnb.blog-export.delete", + "command": "vscode-cnb.backup.delete", "title": "delete", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.blog-export.records.isRefreshing && !vscode-cnb.blog-export.downloading", + "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.backup.records.isRefreshing && !vscode-cnb.backup.downloading", "icon": "$(trash)" } ], @@ -588,52 +595,52 @@ "type": "object", "additionalProperties": false, "default": { - "upload-post-file-no-confirm": true, - "pull-remote-post": true, - "modify-post-setting": true, - "show-post-to-local-file-info": true, - "open-post-in-blog-admin": true, - "export-post-to-pdf": true, - "copy-post-link": true + "post-upload-file-no-confirm": true, + "post-pull": true, + "post-modify-setting": true, + "post-show-local-file-info": true, + "post-open-in-blog-admin": true, + "post-export-to-pdf": true, + "post-copy-link": true }, "properties": { - "upload-post-file-no-confirm": { + "post-upload-file-no-confirm": { "order": 0, "description": "上传到博客园", "type": "boolean", "default": true }, - "pull-remote-post": { + "post-pull": { "order": 1, "description": "拉取博文", "type": "boolean", "default": true }, - "modify-post-setting": { + "post-modify-setting": { "order": 2, "description": "博文设置", "type": "boolean", "default": true }, - "show-post-to-local-file-info": { + "post-show-local-file-info": { "order": 3, "description": "博客园关联博文", "type": "boolean", "default": true }, - "open-post-in-blog-admin": { + "post-open-in-blog-admin": { "order": 4, "description": "在博客后台中编辑", "type": "boolean", "default": true }, - "export-post-to-pdf": { + "post-export-to-pdf": { "order": 5, "description": "导出 PDF", "type": "boolean", "default": true }, - "copy-post-link": { + "post-copy-link": { "order": 6, "description": "复制博文链接", "type": "boolean", @@ -648,71 +655,71 @@ "scope": "application", "additionalProperties": false, "default": { - "upload-post-file-no-confirm": true, - "pull-remote-post": true, - "modify-post-setting": true, - "show-post-to-local-file-info": true, - "open-post-in-blog-admin": true, - "export-post-to-pdf": true, - "copy-post-link": true, - "upload-img-clipboard": true, - "upload-img-fs": true, - "extract-img": true, - "ing:publish-select": false + "post-upload-file-no-confirm": true, + "post-pull": true, + "post-modify-setting": true, + "post-show-local-file-info": true, + "post-open-in-blog-admin": true, + "post-export-to-pdf": true, + "post-copy-link": true, + "img-upload-clipboard": true, + "img-upload-fs": true, + "img-extract": true, + "ing-pub-select": false }, "properties": { - "upload-post-file-no-confirm": { + "post-upload-file-no-confirm": { "order": 0, "description": "上传到博客园", "type": "boolean" }, - "pull-remote-post": { + "post-pull": { "order": 1, "description": "拉取博文", "type": "boolean" }, - "modify-post-setting": { + "post-modify-setting": { "order": 2, "description": "博文设置", "type": "boolean" }, - "show-post-to-local-file-info": { + "post-show-local-file-info": { "order": 3, "description": "博客园关联博文", "type": "boolean" }, - "open-post-in-blog-admin": { + "post-open-in-blog-admin": { "order": 4, "description": "在博客后台中编辑", "type": "boolean", "default": true }, - "export-post-to-pdf": { + "post-export-to-pdf": { "order": 5, "description": "导出 PDF", "type": "boolean" }, - "copy-post-link": { + "post-copy-link": { "order": 6, "description": "复制博文链接", "type": "boolean" }, - "upload-img-clipboard": { + "img-upload-clipboard": { "order": 7, "description": "上传剪贴板图片到博客园", "type": "boolean" }, - "upload-img-fs": { + "img-upload-fs": { "order": 8, "description": "上传本地图片到博客园", "type": "boolean" }, - "extract-img": { + "img-extract": { "order": 9, "description": "提取图片", "type": "boolean" }, - "ing:publish-select": { + "ing-pub-select": { "order": 10, "description": "将选中内容发到闪存", "type": "boolean" @@ -818,42 +825,47 @@ "group": "navigation" }, { - "command": "vscode-cnb.post-list.prev", + "command": "vscode-cnb.post.list-view.prev", "when": "view == cnblogs-post-list || view == cnblogs-post-list-another", "group": "navigation@1" }, { - "command": "vscode-cnb.post-list.next", + "command": "vscode-cnb.post.list-view.next", "when": "view == cnblogs-post-list || view == cnblogs-post-list-another", "group": "navigation@2" }, { - "command": "vscode-cnb.post-list.seek", + "command": "vscode-cnb.post.list-view.seek", "when": "view == cnblogs-post-list || view == cnblogs-post-list-another", "group": "navigation@3" }, { - "command": "vscode-cnb.refresh-post-list", + "command": "vscode-cnb.post.list-view.refresh", "when": "view == cnblogs-post-list || view == cnblogs-post-list-another", "group": "navigation@4" }, { - "command": "vscode-cnb.search-post", + "command": "vscode-cnb.post.search", "when": "view == cnblogs-post-list || view == cnblogs-post-list-another", "group": "navigation@5" }, { - "command": "vscode-cnb.create-local-draft", - "group": "navigation@6", - "when": "view == cnblogs-post-list || view == cnblogs-post-list-another" + "command": "vscode-cnb.post.pull-all", + "when": "view == cnblogs-post-list || view == cnblogs-post-list-another", + "group": "navigation@6" + }, + { + "command": "vscode-cnb.post.create-local-draft", + "when": "view == cnblogs-post-list || view == cnblogs-post-list-another", + "group": "navigation@7" }, { - "command": "vscode-cnb.new-post-category", + "command": "vscode-cnb.post-category.new", "group": "navigation@1", "when": "view == cnblogs-post-category-list" }, { - "command": "vscode-cnb.refresh-post-category-list", + "command": "vscode-cnb.post-category.refresh", "when": "view == cnblogs-post-category-list", "group": "navigation@2" }, @@ -888,40 +900,40 @@ "group": "navigation@5" }, { - "command": "vscode-cnb.ing.publish", + "command": "vscode-cnb.ing.pub", "when": "view == vscode-cnb.ing-list-webview", "group": "navigation@6" }, { - "command": "vscode-cnb.blog-export.create", + "command": "vscode-cnb.backup.create", "when": "view == vscode-cnb.blog-export", "group": "navigation@1" }, { - "command": "vscode-cnb.blog-export.refresh-record", + "command": "vscode-cnb.backup.refresh-record", "when": "view == vscode-cnb.blog-export", "group": "navigation@2" } ], "commandPalette": [ { - "command": "vscode-cnb.reveal-local-post-file-in-os", + "command": "vscode-cnb.post.os-open-local-file", "when": "false" }, { - "command": "vscode-cnb.upload-post", + "command": "vscode-cnb.post.upload", "when": "false" }, { - "command": "vscode-cnb.modify-post-setting", + "command": "vscode-cnb.post.modify-setting", "when": "false" }, { - "command": "vscode-cnb.del-post", + "command": "vscode-cnb.post.del", "when": "false" }, { - "command": "vscode-cnb.upload-post-file", + "command": "vscode-cnb.post.upload-file", "when": "true" }, { @@ -929,59 +941,59 @@ "when": "false" }, { - "command": "vscode-cnb.show-post-to-local-file-info", + "command": "vscode-cnb.post.show-local-file-info", "when": "false" }, { - "command": "vscode-cnb.new-post-category", + "command": "vscode-cnb.post-category.new", "when": "false" }, { - "command": "vscode-cnb.del-selected-post-category", + "command": "vscode-cnb.post-category.del-select", "when": "false" }, { - "command": "vscode-cnb.refresh-post-category-list", + "command": "vscode-cnb.post-category.refresh", "when": "false" }, { - "command": "vscode-cnb.update-post-category", + "command": "vscode-cnb.post-category.update", "when": "false" }, { - "command": "vscode-cnb.post-list.next", + "command": "vscode-cnb.post.list-view.next", "when": "false" }, { - "command": "vscode-cnb.post-list.seek", + "command": "vscode-cnb.post.list-view.seek", "when": "false" }, { - "command": "vscode-cnb.del-post-to-local-file-map", + "command": "vscode-cnb.post.del-local-map", "when": "false" }, { - "command": "vscode-cnb.rename-post", + "command": "vscode-cnb.post.rename", "when": "false" }, { - "command": "vscode-cnb.open-post-in-blog-admin", + "command": "vscode-cnb.post.open-in-blog-admin", "when": "false" }, { - "command": "vscode-cnb.webLogin", + "command": "vscode-cnb.login.web", "when": "false" }, { - "command": "vscode-cnb.view-post-online", + "command": "vscode-cnb.post.view-in-browser", "when": "true" }, { - "command": "vscode-cnb.export-post-to-pdf", + "command": "vscode-cnb.post.export-to-pdf", "when": "false" }, { - "command": "vscode-cnb.ing.publish-select", + "command": "vscode-cnb.ing.pub-select", "when": "false" }, { @@ -1005,286 +1017,267 @@ "when": "false" }, { - "command": "vscode-cnb.copy-post-link", + "command": "vscode-cnb.post.copy-link", "when": "false" }, { - "command": "vscode-cnb.blog-export.refresh-record", + "command": "vscode-cnb.backup.refresh-record", "when": "false" }, { - "command": "vscode-cnb.blog-export.edit", + "command": "vscode-cnb.backup.edit", "when": "false" }, { - "command": "vscode-cnb.blog-export.view-post", + "command": "vscode-cnb.backup.view-post", "when": "false" }, { - "command": "vscode-cnb.blog-export.delete", + "command": "vscode-cnb.backup.delete", "when": "false" } ], "view/item/context": [ { - "command": "vscode-cnb.upload-post", + "command": "vscode-cnb.post.upload", "group": "inline@1", "when": "viewItem == cnb-post-cached" }, { - "command": "vscode-cnb.pull-remote-post", + "command": "vscode-cnb.post.pull", "group": "inline@2", "when": "viewItem == cnb-post-cached" }, { - "command": "vscode-cnb.modify-post-setting", + "command": "vscode-cnb.post.modify-setting", "group": "inline@3", "when": "viewItem =~ /^cnb-post/ && viewItem != cnb-post-category" }, { - "command": "vscode-cnb.del-post", + "command": "vscode-cnb.post.del", "group": "inline@4", "when": "viewItem =~ /^cnb-post/ && viewItem != cnb-post-category" }, { - "command": "vscode-cnb.create-local-draft", + "command": "vscode-cnb.post.create-local-draft", "when": "viewItem == cnb-local-drafts-folder" }, { - "command": "vscode-cnb.rename-post", + "command": "vscode-cnb.post.rename", "when": "viewItem =~ /^cnb-post/ && viewItem != cnb-post-category", "group": "1@2" }, { - "command": "vscode-cnb.modify-post-setting", + "command": "vscode-cnb.post.modify-setting", "when": "viewItem =~ /^cnb-post/ && viewItem != cnb-post-category", "group": "1@1" }, { - "command": "vscode-cnb.del-post", + "command": "vscode-cnb.post.del", "when": "viewItem =~ /^cnb-post/ && viewItem != cnb-post-category", "group": "delete@1" }, { - "command": "vscode-cnb.del-post-to-local-file-map", + "command": "vscode-cnb.post.del-local-map", "when": "viewItem =~ /^cnb-post-cached/", "group": "delete@2" }, { - "command": "vscode-cnb.upload-post-no-confirm", + "command": "vscode-cnb.post.upload-no-confirm", "group": "0@1", "when": "viewItem == cnb-post-cached" }, { - "command": "vscode-cnb.pull-remote-post", + "command": "vscode-cnb.post.pull", "group": "0@2", "when": "viewItem == cnb-post-cached" }, { - "command": "vscode-cnb.reveal-local-post-file-in-os", + "command": "vscode-cnb.post.os-open-local-file", "when": "viewItem =~ /^cnb-post-cached/", "group": "0@3" }, { - "command": "vscode-cnb.view-post-online", + "command": "vscode-cnb.post.view-in-browser", "when": "viewItem =~ /^cnb-post/ && viewItem != cnb-post-category", "group": "0@4" }, { - "command": "vscode-cnb.export-post-to-pdf", + "command": "vscode-cnb.post.export-to-pdf", "when": "viewItem =~ /^cnb-post/ && viewItem != cnb-post-category", "group": "0@5" }, { - "command": "vscode-cnb.copy-post-link", + "command": "vscode-cnb.post.copy-link", "when": "viewItem =~ /^cnb-post/ && viewItem != cnb-post-category", "group": "0@6" }, { - "command": "vscode-cnb.del-selected-post-category", + "command": "vscode-cnb.post-category.del-select", "when": "viewItem == cnb-post-category" }, { - "command": "vscode-cnb.update-post-category", + "command": "vscode-cnb.post-category.update", "when": "viewItem == cnb-post-category", "group": "inline@1" }, { - "command": "vscode-cnb.del-selected-post-category", + "command": "vscode-cnb.post-category.del-select", "when": "viewItem == cnb-post-category", "group": "inline@2" }, { - "command": "vscode-cnb.update-post-category", + "command": "vscode-cnb.post-category.update", "when": "viewItem == cnb-post-category" }, { - "command": "vscode-cnb.refresh-post-search-results", + "command": "vscode-cnb.post.list-view.search.refresh", "when": "viewItem == cnblogs-post-search-results-entry", "group": "inline@1" }, { - "command": "vscode-cnb.clear-post-search-results", + "command": "vscode-cnb.post.list-view.search.clear", "when": "viewItem == cnblogs-post-search-results-entry", "group": "inline@2" }, { - "command": "vscode-cnb.refresh-post-search-results", + "command": "vscode-cnb.post.list-view.search.refresh", "when": "viewItem == cnblogs-post-search-results-entry", "group": "cnblogs-post-search-results-entry@1" }, { - "command": "vscode-cnb.clear-post-search-results", + "command": "vscode-cnb.post.list-view.search.clear", "when": "viewItem == cnblogs-post-search-results-entry", "group": "cnblogs-post-search-results-entry@2" }, { - "command": "vscode-cnb.blog-export.open-local-export", + "command": "vscode-cnb.backup.open-local", "when": "viewItem == cnblogs-export-downloaded-entry", "group": "inline@1" }, { - "command": "vscode-cnb.blog-export.delete", + "command": "vscode-cnb.backup.delete", "when": "viewItem == cnblogs-export-downloaded", "group": "inline@1" }, { - "command": "vscode-cnb.blog-export.view-post", + "command": "vscode-cnb.backup.view-post", "when": "viewItem == cnblogs-export-post", "group": "inline@1" }, { - "command": "vscode-cnb.blog-export.delete", + "command": "vscode-cnb.backup.delete", "when": "viewItem == cnblogs-export-record-done", "group": "inline@1" }, { - "command": "vscode-cnb.blog-export.download", + "command": "vscode-cnb.backup.download", "when": "viewItem == cnblogs-export-record-done", "group": "inline@2" } ], "editor/context": [ { - "command": "vscode-cnb.webLogin", - "when": "!vscode-cnb.isAuthorized", - "group": "cnblogs@0" + "command": "vscode-cnb.login.web", + "when": "!vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.show-post-to-local-file-info", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.show-post-to-local-file-info", - "group": "cnblogs@1" + "command": "vscode-cnb.post.show-local-file-info", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.post-show-local-file-info" }, { - "command": "vscode-cnb.upload-post-file-no-confirm", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.upload-post-file-no-confirm", - "group": "cnblogs@2" + "command": "vscode-cnb.post.upload-file-no-confirm", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.post-upload-file-no-confirm" }, { - "command": "vscode-cnb.pull-remote-post", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.pull-remote-post", - "group": "cnblogs@3" + "command": "vscode-cnb.post.pull", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.post-pull" }, { - "command": "vscode-cnb.modify-post-setting", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.modify-post-setting", - "group": "cnblogs@4" + "command": "vscode-cnb.post.modify-setting", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.post-modify-setting" }, { - "command": "vscode-cnb.open-post-in-blog-admin", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.open-post-in-blog-admin", - "group": "cnblogs@5" + "command": "vscode-cnb.post.open-in-blog-admin", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.post-open-in-blog-admin" }, { - "command": "vscode-cnb.upload-img-clipboard", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.upload-img-clipboard", - "group": "cnblogs@6" + "command": "vscode-cnb.post.export-to-pdf", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.post-export-to-pdf" }, { - "command": "vscode-cnb.upload-img-fs", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.upload-img-fs", - "group": "cnblogs@7" + "command": "vscode-cnb.img.upload-clipboard", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.img-upload-clipboard" }, { - "command": "vscode-cnb.export-post-to-pdf", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.export-post-to-pdf", - "group": "cnblogs@8" + "command": "vscode-cnb.img.upload-fs", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.img-upload-fs" }, { - "command": "vscode-cnb.extract-img", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.extract-img", - "group": "cnblogs@9" + "command": "vscode-cnb.img.extract", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.editor.img-extract" }, { - "command": "vscode-cnb.ing.publish-select", - "group": "cnblogs@10", - "when": "config.cnblogsClient.menus.context.editor.ing:publish-select" + "command": "vscode-cnb.ing.pub-select", + "when": "config.cnblogsClient.menus.context.editor.ing-pub-select" } ], "editor/title": [ { - "command": "vscode-cnb.upload-img", + "command": "vscode-cnb.img.upload", "when": "resourceLangId == markdown", "group": "navigation" }, { - "command": "vscode-cnb.upload-post-file", + "command": "vscode-cnb.post.upload-file", "when": "resourceLangId == markdown", "group": "navigation" } ], "explorer/context": [ { - "command": "vscode-cnb.webLogin", - "when": "!vscode-cnb.isAuthorized", - "group": "cnblogs@1" + "command": "vscode-cnb.login.web", + "when": "!vscode-cnb.isAuthorized" }, { - "command": "vscode-cnb.upload-post-file", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.upload-post-file", - "group": "cnblogs@2" + "command": "vscode-cnb.post.upload-file", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.post-upload-file" }, { - "command": "vscode-cnb.pull-remote-post", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.pull-remote-post", - "group": "cnblogs@3" + "command": "vscode-cnb.post.pull", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.post-pull" }, { - "command": "vscode-cnb.modify-post-setting", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.modify-post-setting", - "group": "cnblogs@4" + "command": "vscode-cnb.post.modify-setting", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.post-modify-setting" }, { - "command": "vscode-cnb.show-post-to-local-file-info", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.show-post-to-local-file-info", - "group": "cnblogs@5" + "command": "vscode-cnb.post.show-local-file-info", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.post-show-local-file-info" }, { - "command": "vscode-cnb.open-post-in-blog-admin", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.open-post-in-blog-admin", - "group": "cnblogs@6" + "command": "vscode-cnb.post.open-in-blog-admin", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.post-open-in-blog-admin" }, { - "command": "vscode-cnb.export-post-to-pdf", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.export-post-to-pdf", - "group": "cnblogs@7" + "command": "vscode-cnb.post.export-to-pdf", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.post-export-to-pdf" }, { - "command": "vscode-cnb.copy-post-link", - "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.copy-post-link", - "group": "cnblogs@8" + "command": "vscode-cnb.post.copy-link", + "when": "resourceLangId == markdown && config.cnblogsClient.menus.context.explorer.post-copy-link" } ] }, "keybindings": [ { - "command": "vscode-cnb.upload-img-clipboard", + "command": "vscode-cnb.img.upload-clipboard", "key": "ctrl+alt+u", "mac": "cmd+alt+u", "when": "editorTextFocus && resourceLangId == markdown" }, { - "command": "vscode-cnb.upload-img-fs", + "command": "vscode-cnb.img.upload-fs", "key": "ctrl+alt+f", "mac": "cmd+alt+f", "when": "editorTextFocus && resourceLangId == markdown" @@ -1306,26 +1299,26 @@ }, { "view": "cnblogs-authorize", - "contents": "[从浏览器登录](command:vscode-cnb.webLogin) \n[个人访问令牌登录](command:vscode-cnb.patLogin)" + "contents": "[从浏览器登录](command:vscode-cnb.login.web) \n[个人访问令牌登录](command:vscode-cnb.login.pat)" }, { "view": "vscode-cnb-workspace", - "contents": "[在 Code 中打开工作空间](command:vscode-cnb.open-workspace)", + "contents": "[在 Code 中打开工作空间](command:vscode-cnb.workspace.code-open)", "when": "!vscode-cnb.isTargetWorkspace" }, { "view": "vscode-cnb-workspace", - "contents": "[在访达中打开工作空间](command:vscode-cnb.reveal-workspace-in-os)", + "contents": "[在访达中打开工作空间](command:vscode-cnb.workspace.os-open)", "when": "isMac" }, { "view": "vscode-cnb-workspace", - "contents": "[在文件资源器中打开工作空间](command:vscode-cnb.reveal-workspace-in-os)", + "contents": "[在文件资源器中打开工作空间](command:vscode-cnb.workspace.os-open)", "when": "isWindows" }, { "view": "vscode-cnb-workspace", - "contents": "[设置工作空间](command:vscode-cnb.set-workspace)" + "contents": "[设置工作空间](command:vscode-cnb.workspace.set)" } ] }, diff --git a/rs/Cargo.lock b/rs/Cargo.lock index 6ec8d295..00038553 100644 --- a/rs/Cargo.lock +++ b/rs/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.72" @@ -104,6 +119,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "winapi", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -130,6 +158,50 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +dependencies = [ + "serde", +] + [[package]] name = "encoding_rs" version = "0.8.32" @@ -139,6 +211,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.1" @@ -266,7 +344,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -279,6 +357,18 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "0.2.9" @@ -350,6 +440,35 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.4.0" @@ -367,7 +486,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", + "serde", ] [[package]] @@ -465,6 +596,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.31.1" @@ -693,6 +833,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "serde_qs", + "serde_with", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -820,6 +961,35 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.0.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "slab" version = "0.4.8" @@ -839,6 +1009,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "2.0.27" @@ -883,6 +1059,34 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1121,6 +1325,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/rs/Cargo.toml b/rs/Cargo.toml index 48458049..7adb9c45 100644 --- a/rs/Cargo.toml +++ b/rs/Cargo.toml @@ -27,5 +27,6 @@ serde = { version = "1.0.177", features = ["derive"] } serde_qs = "0.12.0" serde_json = { version = "1.0", default-features = false, features = ["alloc"] } serde-wasm-bindgen = "0.5.0" +serde_with = "3.1.0" reqwest = { version = "0.11.16", features = ["json"] } diff --git a/rs/src/cnb/ing/comment.rs b/rs/src/cnb/ing/comment.rs new file mode 100644 index 00000000..1b3f47fc --- /dev/null +++ b/rs/src/cnb/ing/comment.rs @@ -0,0 +1,62 @@ +use crate::cnb::ing::{IngReq, ING_API_BASE_URL}; +use crate::infra::http::{setup_auth, APPLICATION_JSON}; +use crate::infra::result::IntoResult; +use crate::panic_hook; +use alloc::format; +use alloc::string::{String, ToString}; +use anyhow::{anyhow, Result}; +use core::ops::Not; +use reqwest::header::CONTENT_TYPE; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[allow(non_camel_case_types)] +#[serde_with::skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, Default)] +struct Body { + #[serde(rename(serialize = "replyTo"))] + reply_to: Option, + #[serde(rename(serialize = "parentCommentId"))] + parent_comment_id: Option, + content: String, +} + +#[wasm_bindgen(js_class = IngReq)] +impl IngReq { + #[wasm_bindgen(js_name = comment)] + pub async fn export_comment( + &self, + ing_id: usize, + content: String, + reply_to: Option, + parent_comment_id: Option, + ) -> Result<(), String> { + panic_hook!(); + let url = format!("{ING_API_BASE_URL}/{ing_id}/comments"); + + let client = reqwest::Client::new().post(url); + + let body = Body { + reply_to, + parent_comment_id, + content, + }; + let req = setup_auth(client, &self.token, self.is_pat_token) + .header(CONTENT_TYPE, APPLICATION_JSON); + + let result: Result<()> = try { + let body = serde_json::to_string_pretty(&body)?; + let req = req.body(body); + + let resp = req.send().await?; + let code = resp.status(); + + if code.is_success().not() { + let text = resp.text().await?; + anyhow!("{}: {}", code, text).into_err()? + } + }; + + result.map_err(|e| e.to_string()) + } +} diff --git a/rs/src/cnb/ing/get_comment.rs b/rs/src/cnb/ing/get_comment.rs new file mode 100644 index 00000000..adaa0f8c --- /dev/null +++ b/rs/src/cnb/ing/get_comment.rs @@ -0,0 +1,35 @@ +use crate::cnb::ing::{IngReq, ING_API_BASE_URL}; +use crate::infra::http::setup_auth; +use crate::infra::result::{homo_result_string, HomoResult, IntoResult}; +use crate::panic_hook; +use alloc::format; +use alloc::string::String; +use anyhow::{anyhow, Result}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = IngReq)] +impl IngReq { + #[wasm_bindgen(js_name = getComment)] + pub async fn export_get_comment(&self, ing_id: usize) -> HomoResult { + panic_hook!(); + let url = format!("{ING_API_BASE_URL}/{ing_id}/comments"); + + let client = reqwest::Client::new().get(url); + + let req = setup_auth(client, &self.token, self.is_pat_token); + + let result: Result = try { + let resp = req.send().await?; + let code = resp.status(); + + if code.is_success() { + resp.text().await? + } else { + let text = resp.text().await?; + anyhow!("{}: {}", code, text).into_err()? + } + }; + + homo_result_string(result) + } +} diff --git a/rs/src/cnb/ing/get_list.rs b/rs/src/cnb/ing/get_list.rs new file mode 100644 index 00000000..27e5a70b --- /dev/null +++ b/rs/src/cnb/ing/get_list.rs @@ -0,0 +1,41 @@ +use crate::cnb::ing::{IngReq, ING_API_BASE_URL}; +use crate::infra::http::setup_auth; +use crate::infra::result::{homo_result_string, HomoResult, IntoResult}; +use crate::panic_hook; +use alloc::string::String; +use alloc::{format, vec}; +use anyhow::{anyhow, Result}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = IngReq)] +impl IngReq { + #[wasm_bindgen(js_name = getList)] + pub async fn export_get_list( + &self, + page_index: usize, + page_size: usize, + ing_type: usize, + ) -> HomoResult { + panic_hook!(); + let url = format!("{ING_API_BASE_URL}/@{ing_type}"); + + let client = reqwest::Client::new().get(url); + + let queries = vec![("pageIndex", page_index), ("pageSize", page_size)]; + let req = setup_auth(client, &self.token, self.is_pat_token).query(&queries); + + let result: Result = try { + let resp = req.send().await?; + let code = resp.status(); + + if code.is_success() { + resp.text().await? + } else { + let text = resp.text().await?; + anyhow!("{}: {}", code, text).into_err()? + } + }; + + homo_result_string(result) + } +} diff --git a/rs/src/cnb/ing/mod.rs b/rs/src/cnb/ing/mod.rs new file mode 100644 index 00000000..aa041b0d --- /dev/null +++ b/rs/src/cnb/ing/mod.rs @@ -0,0 +1,28 @@ +mod comment; +mod get_comment; +mod get_list; +mod r#pub; + +use crate::panic_hook; +use alloc::string::{String, ToString}; +use wasm_bindgen::prelude::*; + +const ING_API_BASE_URL: &str = "https://api.cnblogs.com/api/statuses"; + +#[wasm_bindgen(js_name = IngReq)] +pub struct IngReq { + token: String, + is_pat_token: bool, +} + +#[wasm_bindgen(js_class = IngReq)] +impl IngReq { + #[wasm_bindgen(constructor)] + pub fn new(token: &str, is_pat_token: bool) -> IngReq { + panic_hook!(); + IngReq { + token: token.to_string(), + is_pat_token, + } + } +} diff --git a/rs/src/cnb/ing/pub.rs b/rs/src/cnb/ing/pub.rs new file mode 100644 index 00000000..bd2252fe --- /dev/null +++ b/rs/src/cnb/ing/pub.rs @@ -0,0 +1,43 @@ +use crate::cnb::ing::{IngReq, ING_API_BASE_URL}; +use crate::infra::http::{setup_auth, APPLICATION_JSON}; +use crate::infra::result::IntoResult; +use crate::panic_hook; +use alloc::string::{String, ToString}; +use anyhow::{anyhow, Result}; +use core::ops::Not; +use reqwest::header::CONTENT_TYPE; +use serde_json::json; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = IngReq)] +impl IngReq { + #[wasm_bindgen(js_name = pub)] + pub async fn export_pub(&self, content: &str, is_private: bool) -> Result<(), String> { + panic_hook!(); + let url = ING_API_BASE_URL; + + let body = json!({ + "content": content, + "isPrivate": is_private, + }) + .to_string(); + + let client = reqwest::Client::new().post(url); + + let req = setup_auth(client, &self.token, self.is_pat_token) + .header(CONTENT_TYPE, APPLICATION_JSON) + .body(body); + + let result: Result<()> = try { + let resp = req.send().await?; + let code = resp.status(); + + if code.is_success().not() { + let text = resp.text().await?; + anyhow!("{}: {}", code, text).into_err()? + } + }; + + result.map_err(|e| e.to_string()) + } +} diff --git a/rs/src/cnb/mod.rs b/rs/src/cnb/mod.rs new file mode 100644 index 00000000..b3b10d9f --- /dev/null +++ b/rs/src/cnb/mod.rs @@ -0,0 +1,3 @@ +pub mod ing; +pub mod oauth; +pub mod user; diff --git a/rs/src/cnb/oauth/get_token.rs b/rs/src/cnb/oauth/get_token.rs new file mode 100644 index 00000000..561cade5 --- /dev/null +++ b/rs/src/cnb/oauth/get_token.rs @@ -0,0 +1,53 @@ +use crate::cnb::oauth::OauthReq; +use crate::cnb::oauth::OAUTH_API_BASE_URL; +use crate::infra::http::{cons_query_string, APPLICATION_X3WFU}; +use crate::infra::result::{homo_result_string, HomoResult, IntoResult}; +use crate::panic_hook; +use alloc::string::String; +use alloc::{format, vec}; +use anyhow::{anyhow, Result}; +use reqwest::header::CONTENT_TYPE; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = OauthReq)] +impl OauthReq { + #[wasm_bindgen(js_name = getToken)] + pub async fn export_get_token( + &self, + auth_code: &str, + verify_code: &str, + callback_url: &str, + ) -> HomoResult { + panic_hook!(); + let url = format!("{OAUTH_API_BASE_URL}/connect/token"); + + let client = reqwest::Client::new().post(url); + + let queries = vec![ + ("code", auth_code), + ("code_verifier", verify_code), + ("grant_type", "authorization_code"), + ("client_id", &self.client_id), + ("client_secret", &self.client_secret), + ("redirect_uri", callback_url), + ]; + + let req = client + .header(CONTENT_TYPE, APPLICATION_X3WFU) + .body(cons_query_string(queries)); + + let result: Result = try { + let resp = req.send().await?; + let code = resp.status(); + + if code.is_success() { + resp.text().await? + } else { + let text = resp.text().await?; + anyhow!("{}: {}", code, text).into_err()? + } + }; + + homo_result_string(result) + } +} diff --git a/rs/src/cnb/oauth/mod.rs b/rs/src/cnb/oauth/mod.rs new file mode 100644 index 00000000..a203e436 --- /dev/null +++ b/rs/src/cnb/oauth/mod.rs @@ -0,0 +1,26 @@ +mod get_token; +mod revoke_token; + +use crate::panic_hook; +use alloc::string::String; +use wasm_bindgen::prelude::*; + +const OAUTH_API_BASE_URL: &str = "https://oauth.cnblogs.com"; + +#[wasm_bindgen(js_name = OauthReq)] +pub struct OauthReq { + client_id: String, + client_secret: String, +} + +#[wasm_bindgen(js_class = OauthReq)] +impl OauthReq { + #[wasm_bindgen(constructor)] + pub fn new(client_id: String, client_secret: String) -> OauthReq { + panic_hook!(); + OauthReq { + client_id, + client_secret, + } + } +} diff --git a/rs/src/cnb/oauth/revoke_token.rs b/rs/src/cnb/oauth/revoke_token.rs new file mode 100644 index 00000000..ecd8062e --- /dev/null +++ b/rs/src/cnb/oauth/revoke_token.rs @@ -0,0 +1,49 @@ +use crate::cnb::oauth::OauthReq; +use crate::cnb::oauth::OAUTH_API_BASE_URL; +use crate::infra::http::{cons_query_string, APPLICATION_X3WFU}; +use crate::infra::result::IntoResult; +use crate::{basic, panic_hook}; +use alloc::string::{String, ToString}; +use alloc::{format, vec}; +use anyhow::{anyhow, Result}; +use base64::engine::general_purpose; +use base64::Engine; +use core::ops::Not; +use reqwest::header::{AUTHORIZATION, CONTENT_TYPE}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = OauthReq)] +impl OauthReq { + #[wasm_bindgen(js_name = revokeToken)] + pub async fn export_revoke_token(&self, token: &str) -> Result<(), String> { + panic_hook!(); + let credentials = format!("{}:{}", self.client_id, self.client_secret); + let credentials = general_purpose::STANDARD.encode(credentials); + let url = format!("{OAUTH_API_BASE_URL}/connect/revocation"); + + let client = reqwest::Client::new().post(url); + + let queries = vec![ + ("client_id", self.client_id.as_str()), + ("token", token), + ("token_type_hint", "refresh_token"), + ]; + + let req = client + .header(CONTENT_TYPE, APPLICATION_X3WFU) + .header(AUTHORIZATION, basic!(credentials)) + .body(cons_query_string(queries)); + + let result: Result<()> = try { + let resp = req.send().await?; + let code = resp.status(); + + if code.is_success().not() { + let text = resp.text().await?; + anyhow!("{}: {}", code, text).into_err()? + } + }; + + result.map_err(|e| e.to_string()) + } +} diff --git a/rs/src/cnb/user/get_info.rs b/rs/src/cnb/user/get_info.rs new file mode 100644 index 00000000..b1d87cd8 --- /dev/null +++ b/rs/src/cnb/user/get_info.rs @@ -0,0 +1,35 @@ +use crate::cnb::user::{UserReq, API_BASE_URL}; +use crate::infra::http::setup_auth; +use crate::infra::result::{homo_result_string, HomoResult, IntoResult}; +use crate::panic_hook; +use alloc::format; +use alloc::string::String; +use anyhow::{anyhow, Result}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = UserReq)] +impl UserReq { + #[wasm_bindgen(js_name = getInfo)] + pub async fn export_get_info(&self) -> HomoResult { + panic_hook!(); + let url = format!("{API_BASE_URL}/users"); + + let client = reqwest::Client::new().get(url); + + let req = setup_auth(client, &self.token, self.is_pat_token); + + let result: Result = try { + let resp = req.send().await?; + let code = resp.status(); + + if code.is_success() { + resp.text().await? + } else { + let text = resp.text().await?; + anyhow!("{}: {}", code, text).into_err()? + } + }; + + homo_result_string(result) + } +} diff --git a/rs/src/cnb/user/mod.rs b/rs/src/cnb/user/mod.rs new file mode 100644 index 00000000..f61c7727 --- /dev/null +++ b/rs/src/cnb/user/mod.rs @@ -0,0 +1,25 @@ +mod get_info; + +use crate::panic_hook; +use alloc::string::{String, ToString}; +use wasm_bindgen::prelude::*; + +const API_BASE_URL: &str = "https://api.cnblogs.com/api"; + +#[wasm_bindgen(js_name = UserReq)] +pub struct UserReq { + token: String, + is_pat_token: bool, +} + +#[wasm_bindgen(js_class = UserReq)] +impl UserReq { + #[wasm_bindgen(constructor)] + pub fn new(token: &str, is_pat_token: bool) -> UserReq { + panic_hook!(); + UserReq { + token: token.to_string(), + is_pat_token, + } + } +} diff --git a/rs/src/http.rs b/rs/src/http/mod.rs similarity index 64% rename from rs/src/http.rs rename to rs/src/http/mod.rs index 2405315f..8d5f8b17 100644 --- a/rs/src/http.rs +++ b/rs/src/http/mod.rs @@ -5,9 +5,10 @@ pub mod put; use crate::infra::result::IntoResult; use crate::panic_hook; -use alloc::string::String; -use anyhow::{bail, Result}; +use alloc::string::{String, ToString}; +use anyhow::{anyhow, bail, Result}; use core::convert::TryFrom; +use core::ops::Not; use core::str::FromStr; use reqwest::header::HeaderMap; use reqwest::Response; @@ -27,7 +28,19 @@ fn header_json_to_header_map(header_json: &str) -> Result { header_map.into_ok() } -async fn body_or_err(resp: Response) -> Result { +pub async fn unit_or_err(resp: Response) -> Result<(), String> { + let code = resp.status(); + let result: Result<()> = try { + let body = resp.text().await?; + + if code.is_success().not() { + anyhow!("{}: {}", code, body).into_err()? + } + }; + result.map_err(|e| e.to_string()) +} + +pub async fn body_or_err(resp: Response) -> Result { let code = resp.status(); let body = resp.text().await?; diff --git a/rs/src/infra/http.rs b/rs/src/infra/http.rs new file mode 100644 index 00000000..f8534464 --- /dev/null +++ b/rs/src/infra/http.rs @@ -0,0 +1,43 @@ +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use reqwest::header::AUTHORIZATION; +use reqwest::RequestBuilder; + +pub const APPLICATION_JSON: &str = "application/json"; +pub const APPLICATION_X3WFU: &str = "application/x-www-form-urlencoded"; +pub const AUTHORIZATION_TYPE: &str = "Authorization-Type"; +pub const PAT: &str = "pat"; + +#[macro_export] +macro_rules! bearer { + ($token:expr) => {{ + use alloc::format; + format!("Bearer {}", $token) + }}; +} + +#[macro_export] +macro_rules! basic { + ($token:expr) => {{ + use alloc::format; + format!("Basic {}", $token) + }}; +} + +pub fn setup_auth(builder: RequestBuilder, token: &str, is_pat_token: bool) -> RequestBuilder { + let builder = builder.header(AUTHORIZATION, bearer!(token)); + + if is_pat_token { + builder.header(AUTHORIZATION_TYPE, PAT) + } else { + builder + } +} + +pub fn cons_query_string(queries: Vec<(&str, &str)>) -> String { + queries + .into_iter() + .map(|(k, v)| format!("{k}={v}")) + .fold("".to_string(), |acc, q| format!("{acc}&{q}")) +} diff --git a/rs/src/infra.rs b/rs/src/infra/mod.rs similarity index 78% rename from rs/src/infra.rs rename to rs/src/infra/mod.rs index f82fe523..d2d40234 100644 --- a/rs/src/infra.rs +++ b/rs/src/infra/mod.rs @@ -1,3 +1,4 @@ +pub mod http; pub mod option; pub mod panic_hook; pub mod result; diff --git a/rs/src/lib.rs b/rs/src/lib.rs index 8dbd61f9..25be9605 100644 --- a/rs/src/lib.rs +++ b/rs/src/lib.rs @@ -1,7 +1,9 @@ #![no_std] +#![feature(try_blocks)] extern crate alloc; pub mod base64; +pub mod cnb; pub mod http; pub mod infra; pub mod rand; diff --git a/src/auth/access-token.ts b/src/auth/access-token.ts deleted file mode 100644 index 4cfda70f..00000000 --- a/src/auth/access-token.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type AccessToken = { - exp: number -} diff --git a/src/auth/account-info.ts b/src/auth/account-info.ts index 3223762e..38d4062e 100644 --- a/src/auth/account-info.ts +++ b/src/auth/account-info.ts @@ -1,49 +1,49 @@ -import { UserInfoSpec } from '@/auth/oauth' -import { trim } from 'lodash-es' -import { AuthenticationSessionAccountInformation } from 'vscode' -import { AuthProvider } from './auth-provider' - -export class AccountInfo implements AuthenticationSessionAccountInformation { - readonly label: string - readonly id: string - - private _blogApp: string | null = null +import { AuthenticationSessionAccountInformation as ASAI } from 'vscode' +import { UserReq } from '@/wasm' +import { Alert } from '@/infra/alert' + +function getAuthedUserReq(token: string) { + // TODO: need better solution + const isPatToken = token.length === 64 + return new UserReq(token, isPatToken) +} - private constructor( - public readonly name: string, - public readonly avatar: string, - public readonly website: string, //The user blog home page url - public readonly blogId: number, - public readonly sub: string, //UserId(data type is Guid) - public readonly accountId: number //SpaceUserId - ) { - this.id = `${this.accountId}-${AuthProvider.providerId}` - this.label = name - } +/* eslint-disable @typescript-eslint/naming-convention */ +export type UserInfo = { + UserId: string + SpaceUserID: number + BlogId: number + DisplayName: string + Face: string + Avatar: string + Seniority: string + BlogApp: string + FollowingCount: number + FollowerCount: number + IsVip: boolean +} - get userId() { - return this.sub - } +export class AccountInfo implements ASAI { + readonly id: string + readonly label: string - get blogApp(): string | null { - this._blogApp ??= this.parseBlogApp() - return this._blogApp + constructor(public readonly userInfo: UserInfo) { + this.id = `${userInfo.SpaceUserID}-cnblogs` + this.label = userInfo.DisplayName } +} - static newAnonymous = () => new AccountInfo('anonymous', '', '', -1, '', -1) +export namespace AccountInfo { + export async function get(token: string) { + const req = getAuthedUserReq(token) - static from = (userInfo: UserInfoSpec) => - new AccountInfo( - userInfo.name, - userInfo.picture, - userInfo.website, - parseInt(userInfo.blog_id, 10), - userInfo.sub, - parseInt(userInfo.account_id, 10) - ) + try { + const resp = await req.getInfo() + const userInfo = JSON.parse(resp) - private parseBlogApp = () => - trim(this.website ?? '', '/') - .split('/') - .pop() ?? null + return new AccountInfo(userInfo) + } catch (e) { + void Alert.err(`获取用户信息失败: ${e}`) + } + } } diff --git a/src/auth/account-manager.ts b/src/auth/account-manager.ts index 795bd3bb..de7d8e53 100644 --- a/src/auth/account-manager.ts +++ b/src/auth/account-manager.ts @@ -1,6 +1,5 @@ -import { AccountInfo } from './account-info' import { globalCtx } from '@/ctx/global-ctx' -import { window, authentication, AuthenticationGetSessionOptions, Disposable, InputBoxOptions } from 'vscode' +import { window, authentication, AuthenticationGetSessionOptions, Disposable } from 'vscode' import { accountViewDataProvider } from '@/tree-view/provider/account-view-data-provider' import { postDataProvider } from '@/tree-view/provider/post-data-provider' import { postCategoryDataProvider } from '@/tree-view/provider/post-category-tree-data-provider' @@ -20,14 +19,18 @@ let authSession: AuthSession | null = null export namespace AccountManagerNg { export async function ensureSession(opt?: AuthenticationGetSessionOptions) { - const session = await authentication.getSession(authProvider.providerId, [], opt).then( - session => (session ? AuthSession.from(session) : null), - e => { - void Alert.err(`创建/获取 Session 失败: ${e}`) - } - ) - - if (session != null && session.account.accountId < 0) { + let session + try { + const result = await authentication.getSession(authProvider.providerId, [], opt) + if (result === undefined) session = null + // TODO: need better impl + else session = result + } catch (e) { + void Alert.err(`创建/获取 Session 失败: ${e}`) + session = null + } + + if (session != null && session.account.userInfo.SpaceUserID < 0) { authSession = null await authProvider.removeSession(session.id) } else { @@ -46,13 +49,17 @@ export namespace AccountManagerNg { title: '请输入您的个人访问令牌 (PAT)', prompt: '您可以从账户设置中获取个人访问令牌:\nhttps://account.cnblogs.com/settings/account/personal-access-token', password: true, - } as InputBoxOptions + } const pat = await window.showInputBox(opt) if (pat === undefined) return - await authProvider.onAccessTokenGranted(pat) - await ensureSession() - await AccountManagerNg.updateAuthStatus() + try { + await authProvider.onAccessTokenGranted(pat) + await ensureSession() + await AccountManagerNg.updateAuthStatus() + } catch (e) { + void Alert.err(`授权失败: ${e}`) + } } export async function logout() { @@ -88,12 +95,12 @@ export namespace AccountManagerNg { await execCmd('setContext', `${globalCtx.extName}.${isAuthorizedStorageKey}`, accountManager.isAuthorized) - if (accountManager.isAuthorized) { - await execCmd('setContext', `${globalCtx.extName}.user`, { - name: accountManager.currentUser.name, - avatar: accountManager.currentUser.avatar, - }) - } + if (!accountManager.isAuthorized) return + + await execCmd('setContext', `${globalCtx.extName}.user`, { + name: accountManager.currentUser?.userInfo.DisplayName, + avatar: accountManager.currentUser?.userInfo.Avatar, + }) } } @@ -123,8 +130,8 @@ class AccountManager extends Disposable { return authSession !== null } - get currentUser(): AccountInfo { - return authSession?.account ?? AccountInfo.newAnonymous() + get currentUser() { + return authSession?.account } } diff --git a/src/auth/auth-provider.ts b/src/auth/auth-provider.ts index dee9bafd..9dfb1db3 100644 --- a/src/auth/auth-provider.ts +++ b/src/auth/auth-provider.ts @@ -1,11 +1,9 @@ import { AuthSession } from '@/auth/auth-session' import { genVerifyChallengePair } from '@/service/code-challenge' -import { isArray, isUndefined } from 'lodash-es' import { authentication, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent as VscAuthProviderAuthSessionChEv, - CancellationToken, CancellationTokenSource, Disposable, env, @@ -48,13 +46,10 @@ async function browserSignIn(challengeCode: string, scopes: string[]) { } export class AuthProvider implements AuthenticationProvider, Disposable { - static readonly providerId = 'cnblogs' - static readonly providerName = '博客园Cnblogs' + readonly providerId = 'cnblogs' + readonly providerName = '博客园Cnblogs' - readonly providerId = AuthProvider.providerId - readonly providerName = AuthProvider.providerName - - protected readonly sessionStorageKey = `${AuthProvider.providerId}.sessions` + protected readonly sessionStorageKey = `${this.providerId}.sessions` protected readonly allScopes = globalCtx.config.oauth.scope.split(' ') private _allSessions?: AuthSession[] | null @@ -62,7 +57,7 @@ export class AuthProvider implements AuthenticationProvider, Disposable { private readonly _sessionChangeEmitter = new EventEmitter() private readonly _disposable = Disposable.from( this._sessionChangeEmitter, - authentication.registerAuthenticationProvider(AuthProvider.providerId, AuthProvider.providerName, this, { + authentication.registerAuthenticationProvider(this.providerId, this.providerName, this, { supportsMultipleAccounts: false, }), this.onDidChangeSessions(() => { @@ -78,9 +73,7 @@ export class AuthProvider implements AuthenticationProvider, Disposable { const sessions = await this.getAllSessions() const parsedScopes = this.ensureScopes(scopes) - return sessions - .map(x => AuthSession.from(x)) - .filter(({ scopes: sessionScopes }) => parsedScopes.every(x => sessionScopes.includes(x))) + return sessions.filter(({ scopes: sessionScopes }) => parsedScopes.every(x => sessionScopes.includes(x))) } createSession(scopes: string[]): Thenable { @@ -127,9 +120,8 @@ export class AuthProvider implements AuthenticationProvider, Disposable { } try { - const token = await Oauth.fetchToken(verifyCode, authCode) + const token = await Oauth.getToken(verifyCode, authCode) const authSession = await this.onAccessTokenGranted(token.accessToken, { - cancelToken: cancelTokenSrc.token, onStateChange(state) { progress.report({ message: state }) }, @@ -169,63 +161,35 @@ export class AuthProvider implements AuthenticationProvider, Disposable { async onAccessTokenGranted( accessToken: string, { - cancelToken, onStateChange, shouldFireSessionAddedEvent = true, }: { onStateChange?: (state: string) => void - cancelToken?: CancellationToken shouldFireSessionAddedEvent?: boolean } = {} ) { - const ifNotCancelledThen = (f: () => TR): TR | undefined => { - if (cancelToken?.isCancellationRequested) return - return f() - } - - let session: AuthSession | undefined - - try { - onStateChange?.('正在获取账户信息...') - - const spec = await Oauth.fetchUserInfo(accessToken) + onStateChange?.('正在获取账户信息...') - onStateChange?.('即将完成...') - - session = ifNotCancelledThen(() => { - if (isUndefined(spec)) return - - return AuthSession.from({ - accessToken, - account: AccountInfo.from(spec), - scopes: this.ensureScopes(null), - id: `${this.providerId}-${spec.account_id}`, - }) - }) + const accountInfo = await AccountInfo.get(accessToken) + if (accountInfo === undefined) throw Error('用户信息获取失败') - const hasStored = await ifNotCancelledThen(() => { - if (isUndefined(session)) return Promise.resolve(false) + onStateChange?.('即将完成...') - return globalCtx.secretsStorage.store(this.sessionStorageKey, JSON.stringify([session])).then( - () => true, - () => false - ) - }) - - ifNotCancelledThen(() => { - if (!hasStored || isUndefined(session) || !shouldFireSessionAddedEvent) return + const session = new AuthSession( + accountInfo, + `${this.providerId}-${accountInfo.userInfo.SpaceUserID}`, + accessToken, + this.ensureScopes(null) + ) + await globalCtx.secretsStorage.store(this.sessionStorageKey, JSON.stringify([session])) - return this._sessionChangeEmitter.fire({ - added: [session], - removed: undefined, - changed: undefined, - }) - }) - } finally { - if (session != null && cancelToken?.isCancellationRequested) await this.removeSession(session.id) - } + if (!shouldFireSessionAddedEvent) return session - if (session == null) throw new Error('Failed to create session') + this._sessionChangeEmitter.fire({ + added: [session], + removed: undefined, + changed: undefined, + }) return session } @@ -236,16 +200,17 @@ export class AuthProvider implements AuthenticationProvider, Disposable { protected async getAllSessions(): Promise { const legacyToken = LegacyTokenStore.getAccessToken() - if (legacyToken != null) { + if (legacyToken !== undefined) { await this.onAccessTokenGranted(legacyToken, { shouldFireSessionAddedEvent: false }) - .then(undefined, console.warn) - .finally(() => void LegacyTokenStore.remove()) + void LegacyTokenStore.remove() } if (this._allSessions == null || this._allSessions.length <= 0) { const storage = await globalCtx.secretsStorage.get(this.sessionStorageKey) const sessions = JSON.parse(storage ?? '[]') as AuthSession[] | null | undefined - this._allSessions = isArray(sessions) ? sessions.map(x => AuthSession.from(x)) : [] + + if (Array.isArray(sessions)) this._allSessions = sessions + else this._allSessions = [] } return this._allSessions diff --git a/src/auth/auth-session.ts b/src/auth/auth-session.ts index f06a0977..0bdafbcd 100644 --- a/src/auth/auth-session.ts +++ b/src/auth/auth-session.ts @@ -1,36 +1,20 @@ -import { AccessToken } from '@/auth/access-token' import { AccountInfo } from '@/auth/account-info' -import { keys, merge, pick } from 'lodash-es' -import { AuthenticationSession } from 'vscode' +import { AuthenticationSession as CodeAuthSession } from 'vscode' -export class AuthSession implements AuthenticationSession { - private _parsedAccessToken: AccessToken | null = null - - private constructor( +export class AuthSession implements CodeAuthSession { + constructor( public readonly account: AccountInfo, - public readonly id = '', - public readonly accessToken = '', - public readonly scopes: readonly string[] = [] + public readonly id: string, + public readonly accessToken: string, + public readonly scopes: string[] ) {} get isExpired() { // TODO: need better solution if (this.accessToken.length === 64) return false - if (this._parsedAccessToken == null) { - const buf = Buffer.from(this.accessToken.split('.')[1], 'base64') - this._parsedAccessToken ??= JSON.parse(buf.toString()) - } - - if (this._parsedAccessToken == null) return true - return this._parsedAccessToken.exp * 1000 <= Date.now() - } - - static from>(t?: T) { - const session = new AuthSession(AccountInfo.newAnonymous()) - - merge(session, pick(t, keys(session))) - - return session + const accessTokenPart2 = this.accessToken.split('.')[1] + const buf = Buffer.from(accessTokenPart2, 'base64') + return (<{ exp: number }>JSON.parse(buf.toString())).exp } } diff --git a/src/auth/oauth.ts b/src/auth/oauth.ts index 684baf01..08e0a2a1 100644 --- a/src/auth/oauth.ts +++ b/src/auth/oauth.ts @@ -1,38 +1,21 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { TokenInfo } from '@/model/token-info' -import { AccountInfo } from '@/auth/account-info' import { globalCtx } from '@/ctx/global-ctx' -import { consHeader, ReqHeaderKey } from '@/infra/http/infra/header' -import { Req } from '@/infra/http/req' import { Alert } from '@/infra/alert' -import { consUrlPara } from '@/infra/http/infra/url-para' -import { basic } from '@/infra/http/infra/auth-type' -import { RsBase64 } from '@/wasm' +import { OauthReq } from '@/wasm' -export type UserInfoSpec = Pick & { - readonly blog_id: string - readonly account_id: string - readonly picture: string +function getAuthedOauthReq() { + const clientId = globalCtx.config.oauth.clientId + const clientSec = globalCtx.config.oauth.clientSecret + return new OauthReq(clientId, clientSec) } export namespace Oauth { - import ContentType = ReqHeaderKey.ContentType - - export async function fetchToken(verifyCode: string, authCode: string) { - const url = `${globalCtx.config.oauth.authority}${globalCtx.config.oauth.tokenRoute}` - const header = consHeader([ReqHeaderKey.CONTENT_TYPE, ContentType.appX3wfu]) - - const body = consUrlPara( - ['code', authCode], - ['code_verifier', verifyCode], - ['grant_type', 'authorization_code'], - ['client_id', globalCtx.config.oauth.clientId], - ['client_secret', globalCtx.config.oauth.clientSecret], - ['redirect_uri', globalCtx.extensionUrl] - ) - + export async function getToken(verifyCode: string, authCode: string) { + const req = getAuthedOauthReq() + const callback_url = globalCtx.extensionUrl try { - const resp = await Req.post(url, header, body) + const resp = await req.getToken(authCode, verifyCode, callback_url) return TokenInfo.fromResp(resp) } catch (e) { void Alert.err(`获取 Token 失败: ${e}`) @@ -40,36 +23,10 @@ export namespace Oauth { } } - export async function fetchUserInfo(token: string) { - const { authority, userInfoRoute } = globalCtx.config.oauth - - const url = `${authority}${userInfoRoute}` - const header = consHeader([ReqHeaderKey.AUTHORIZATION, `Bearer ${token}`]) - - try { - const resp = await Req.get(url, header) - return JSON.parse(resp) as UserInfoSpec - } catch (e) { - void Alert.err(`获取用户信息失败: ${e}`) - } - } - export async function revokeToken(token: string) { - // FIX: revoke url is deprecated - const { clientId, clientSecret, revokeRoute, authority } = globalCtx.config.oauth - - const url = `${authority}${revokeRoute}` - - const credentials = RsBase64.encode(`${clientId}:${clientSecret}`) - const header = consHeader( - [ReqHeaderKey.AUTHORIZATION, basic(credentials)], - [ReqHeaderKey.CONTENT_TYPE, ContentType.appX3wfu] - ) - - const body = consUrlPara(['client_id', clientId], ['token', token], ['token_type_hint', 'refresh_token']) - + const req = getAuthedOauthReq() try { - await Req.post(url, header, body) + await req.revokeToken(token) return true } catch (e) { void Alert.err(`撤销 Token 失败: ${e}`) diff --git a/src/cmd/blog-export/download.ts b/src/cmd/blog-export/download.ts index 2e4f3986..4b5ce91c 100644 --- a/src/cmd/blog-export/download.ts +++ b/src/cmd/blog-export/download.ts @@ -99,5 +99,5 @@ export async function downloadBlogExport(input: unknown) { } function setIsDownloading(value: boolean) { - return execCmd('setContext', `${globalCtx.extName}.blog-export.downloading`, value || undefined) + return execCmd('setContext', `${globalCtx.extName}.backup.downloading`, value || undefined) } diff --git a/src/cmd/blog-export/refresh.ts b/src/cmd/blog-export/refresh.ts index 03f32e1e..5dcdee04 100644 --- a/src/cmd/blog-export/refresh.ts +++ b/src/cmd/blog-export/refresh.ts @@ -11,5 +11,5 @@ export async function refreshExportRecord() { } function setIsRefreshing(value: boolean) { - return execCmd('setContext', `${globalCtx.extName}.blog-export.records.isRefreshing`, value || undefined) + return execCmd('setContext', `${globalCtx.extName}.backup.records.isRefreshing`, value || undefined) } diff --git a/src/cmd/blog-export/view-post.ts b/src/cmd/blog-export/view-post.ts index ebe97964..9ac77dd7 100644 --- a/src/cmd/blog-export/view-post.ts +++ b/src/cmd/blog-export/view-post.ts @@ -4,7 +4,7 @@ import { ExportPostTreeItem } from '@/tree-view/model/blog-export/post' import { URLSearchParams } from 'url' import { languages, TextDocumentContentProvider, Uri, window, workspace } from 'vscode' -const schema = 'vscode-cnb.blog-export.post' +const schema = 'vscode-cnb.backup.post' function parseInput(input: unknown): ExportPostTreeItem | null | undefined { return input instanceof ExportPostTreeItem ? input : null diff --git a/src/cmd/browser.ts b/src/cmd/browser.ts new file mode 100644 index 00000000..37053110 --- /dev/null +++ b/src/cmd/browser.ts @@ -0,0 +1,35 @@ +import { Uri } from 'vscode' +import { accountManager } from '@/auth/account-manager' +import { execCmd } from '@/infra/cmd' + +export namespace Browser.Open { + export function open(url: string) { + return execCmd('vscode.open', Uri.parse(url)) + } +} + +export namespace Browser.Open.Cnb { + export const home = () => open('https://www.cnblogs.com') + export const news = () => open('https://news.cnblogs.com') + export const ing = () => open('https://ing.cnblogs.com') + export const q = () => open('https://q.cnblogs.com') +} + +export namespace Browser.Open.User { + export const accountSetting = () => open('https://account.cnblogs.com/settings/account') + + export const blog = () => { + const blogApp = accountManager.currentUser?.userInfo.BlogApp + if (blogApp !== undefined) void open(`https://www.cnblogs.com/${blogApp}`) + } + + export const blogConsole = () => open('https://i.cnblogs.com') + + export const home = () => { + const accountId = accountManager.currentUser?.userInfo.SpaceUserID + if (accountId === undefined || accountId <= 0) return + + const url = `https://home.cnblogs.com/u/${accountId}` + return open(url) + } +} diff --git a/src/cmd/ing/comment-ing.ts b/src/cmd/ing/comment-ing.ts index ef3a6096..118a251c 100644 --- a/src/cmd/ing/comment-ing.ts +++ b/src/cmd/ing/comment-ing.ts @@ -28,13 +28,15 @@ export class CommentIngCmdHandler implements CmdHandler { const atContent = atUserAlias ? `@${atUserAlias} ` : '' if (this._content) { - return window.withProgress({ location: ProgressLocation.Notification, title: '正在请求...' }, p => { + return window.withProgress({ location: ProgressLocation.Notification, title: '正在请求...' }, async p => { p.report({ increment: 30 }) - return IngApi.comment(this._ingId, { - replyTo: atUserId, - content: atContent + this._content, - parentCommentId: this._parentCommentId ?? 0, - }).then(hasCommented => (hasCommented ? this.onCommented() : undefined)) + const isSuccess = await IngApi.comment( + this._ingId, + atContent + this._content, + atUserId, + this._parentCommentId ?? 0 + ) + if (isSuccess) await this.onCommented() }) } } diff --git a/src/cmd/ing/ing-page-list.ts b/src/cmd/ing/ing-page-list.ts new file mode 100644 index 00000000..55c5c665 --- /dev/null +++ b/src/cmd/ing/ing-page-list.ts @@ -0,0 +1,63 @@ +import { getIngListWebviewProvider } from '@/service/ing/ing-list-webview-provider' +import { QuickPickItem, window } from 'vscode' +import { IngType, IngTypesMetadata } from '@/model/ing' + +export namespace Ing.ListView { + export const refresh = () => getIngListWebviewProvider().refreshingList() + + export function goNext() { + const provider = getIngListWebviewProvider() + const { pageIndex } = provider + return provider.refreshingList({ pageIndex: pageIndex + 1 }) + } + + export function goPrev() { + const provider = getIngListWebviewProvider() + const { pageIndex } = provider + if (pageIndex > 1) return provider.refreshingList({ pageIndex: pageIndex - 1 }) + return Promise.resolve() + } + + export function goFirst(): Promise { + return getIngListWebviewProvider().refreshingList({ pageIndex: 1 }) + } + + export function switchType() { + const options: (QuickPickItem & { ingType: IngType })[] = IngTypesMetadata.map( + ([ingType, { displayName, description }]) => ({ + label: displayName, + ingType: ingType, + description: description, + picked: ingType === getIngListWebviewProvider().ingType, + }) + ) + const quickPick = window.createQuickPick<(typeof options)[0]>() + + quickPick.title = '选择闪存类型' + quickPick.items = options + quickPick.canSelectMany = false + quickPick.activeItems = options.filter(x => x.picked) + quickPick.selectedItems = quickPick.activeItems + quickPick.ignoreFocusOut = false + + const disposables = [quickPick] + + quickPick.onDidChangeSelection( + ([selectedItem]) => { + if (selectedItem) { + quickPick.hide() + return getIngListWebviewProvider().refreshingList({ + pageIndex: 1, + ingType: selectedItem.ingType, + }) + } + }, + undefined, + disposables + ) + quickPick.onDidHide(() => disposables.forEach(d => d.dispose()), undefined, disposables) + quickPick.show() + + return Promise.resolve() + } +} diff --git a/src/cmd/ing/pub-ing-with-input.ts b/src/cmd/ing/pub-ing-with-input.ts new file mode 100644 index 00000000..5fe2ae3f --- /dev/null +++ b/src/cmd/ing/pub-ing-with-input.ts @@ -0,0 +1,111 @@ +import { Alert } from '@/infra/alert' +import { window } from 'vscode' +import { pubIng } from '@/cmd/ing/pub-ing' + +async function setupContent(value: string) { + const validateInput = (value: string) => { + if (value === '') return '闪存内容不能为空' + if (value.length > 2000) return '内容不能超过2000个字符' + } + const input = await window.showInputBox({ + title: '编辑闪存 - 内容(1/3)', + placeHolder: '你在做什么? 你在想什么?', + value, + validateInput, + }) + + if (input === undefined) throw Error() + + return input +} + +async function setupAccess() { + const items = [ + { label: '公开', value: false }, + { label: '仅自己', value: true }, + ] + + const selected = await window.showQuickPick(items, { + title: '编辑闪存 - 访问权限(2/3)', + canPickMany: false, + }) + + if (selected === undefined) throw Error() + + return selected.value +} + +async function setupTag(value: string) { + const input = await window.showInputBox({ + title: '编辑闪存 - 标签(可选)', + prompt: '输入以 "," 分隔的标签, 例如: "Tag1,Tag2"', + value, + }) + + if (input === undefined) throw Error() + + return input + .split(',') + .map(x => x.trim()) + .filter(x => x !== '') +} + +function fmtIng(content: string, tags: string[]) { + const tagPrefix = tags.map(x => `[${x}]`).join('') + return `${tagPrefix}${content}` +} + +class InteractiveState { + private ingTags: string[] = [] + private ingContent = '' + private ingIsPrivate = false + + async withContent(value: string) { + try { + this.ingContent = await setupContent(value) + this.ingIsPrivate = await setupAccess() + this.ingTags = await setupTag('') + + const isConfirm = await this.showConfirm() + if (isConfirm) { + const content = fmtIng(this.ingContent, this.ingTags) + pubIng(content, this.ingIsPrivate) + } + } catch (_) { + // pub canceled, do nothing + } + } + + private async showConfirm() { + // eslint-disable-next-line no-constant-condition + while (true) { + const items = [ + ['确定', () => Promise.resolve(true)], + ['编辑内容', async () => (this.ingContent = await setupContent(this.ingContent))], + ['编辑访问权限', async () => (this.ingIsPrivate = await setupAccess())], + ['编辑标签', async () => (this.ingTags = await setupTag(this.ingTags.join(',')))], + ] as const + + const detail = `📝${fmtIng(this.ingContent, this.ingTags)}${this.ingIsPrivate ? '\n🔒仅自己可见' : ''}` + + const selected = await Alert.info( + '确定要发布闪存吗?', + { + modal: true, + detail, + }, + ...items.map(([title]) => title) + ) + + if (selected === '确定') return true + if (selected === '编辑内容') this.ingContent = await setupContent(this.ingContent) + if (selected === '编辑访问权限') this.ingIsPrivate = await setupAccess() + if (selected === '编辑标签') this.ingTags = await setupTag(this.ingTags.join(',')) + if (selected === undefined) return false + } + } +} + +export function pubIngWithInput(value: string) { + void new InteractiveState().withContent(value) +} diff --git a/src/cmd/ing/pub-ing-with-select.ts b/src/cmd/ing/pub-ing-with-select.ts new file mode 100644 index 00000000..6d934d10 --- /dev/null +++ b/src/cmd/ing/pub-ing-with-select.ts @@ -0,0 +1,13 @@ +import { window } from 'vscode' +import { Alert } from '@/infra/alert' +import { pubIngWithInput } from '@/cmd/ing/pub-ing-with-input' + +export function pubIngWithSelect() { + const text = window.activeTextEditor?.document.getText(window.activeTextEditor?.selection) + if (text === undefined) { + void Alert.warn(`当前没有选中任何内容`) + return + } + + pubIngWithInput(text) +} diff --git a/src/cmd/ing/pub-ing.ts b/src/cmd/ing/pub-ing.ts new file mode 100644 index 00000000..3c98cc11 --- /dev/null +++ b/src/cmd/ing/pub-ing.ts @@ -0,0 +1,42 @@ +import { execCmd } from '@/infra/cmd' +import { IngType } from '@/model/ing' +import { Alert } from '@/infra/alert' +import { globalCtx } from '@/ctx/global-ctx' +import { IngApi } from '@/service/ing/ing-api' +import { getIngListWebviewProvider } from '@/service/ing/ing-list-webview-provider' +import { ProgressLocation, Uri, window } from 'vscode' + +async function afterPub(ingIsPrivate: boolean) { + await getIngListWebviewProvider().refreshingList({ + ingType: ingIsPrivate ? IngType.my : IngType.all, + pageIndex: 1, + }) + + const codeOpen = (uri: string) => execCmd('vscode.open', Uri.parse(uri)) + + const options = [ + ['打开闪存', () => codeOpen(globalCtx.config.ingSite)], + ['我的闪存', () => codeOpen(`${globalCtx.config.ingSite}/#my`)], + ['新回应', () => codeOpen(`${globalCtx.config.ingSite}/#recentcomment`)], + ['提到我', () => codeOpen(`${globalCtx.config.ingSite}/#mention`)], + ] as const + + const selected = await Alert.info('闪存已发布, 快去看看吧', ...options.map(v => ({ title: v[0], id: v[0] }))) + + if (selected) return options.find(x => x[0] === selected.id)?.[1]() +} + +export function pubIng(content: string, isPrivate: boolean) { + const opt = { + location: ProgressLocation.Notification, + title: '正在发布...', + } + + void window.withProgress(opt, async p => { + p.report({ increment: 40 }) + const isSuccess = await IngApi.pub(content, isPrivate) + p.report({ increment: 100 }) + if (isSuccess) void afterPub(isPrivate) + p.report({ increment: 100 }) + }) +} diff --git a/src/cmd/ing/publish-ing.ts b/src/cmd/ing/publish-ing.ts deleted file mode 100644 index d4bd4997..00000000 --- a/src/cmd/ing/publish-ing.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { CmdHandler } from '@/cmd/cmd-handler' -import { execCmd } from '@/infra/cmd' -import { IngPublishModel, IngType } from '@/model/ing' -import { Alert } from '@/infra/alert' -import { globalCtx } from '@/ctx/global-ctx' -import { IngApi } from '@/service/ing/ing-api' -import { getIngListWebviewProvider } from '@/service/ing/ing-list-webview-provider' -import { InputStep, MultiStepInput, QuickPickParameters } from '@/service/multi-step-input' -import { MessageOptions, ProgressLocation, QuickPickItem, Uri, window } from 'vscode' - -export class PublishIngCmdHandler implements CmdHandler { - readonly operation = '发布闪存' - readonly editingText = '编辑闪存' - readonly inputStep: Record<'content' | 'access' | 'tags', InputStep> = { - content: async input => { - this.currentStep = 1 - this.inputContent = await input.showInputBox({ - title: this.editingText + ' - 内容', - value: this.inputContent, - prompt: '你在做什么? 你在想什么?', - totalSteps: Object.keys(this.inputStep).length, - step: this.currentStep, - ignoreFocusOut: true, - validateInput: v => Promise.resolve(v.length > 2000 ? '最多输入2000个字符' : undefined), - shouldResume: () => Promise.resolve(false), - }) - }, - access: async input => { - this.currentStep = 2 - const items = [ - { label: '公开', value: false }, - { label: '仅自己', value: true }, - ] - const activeItem = items.filter(x => x.value === this.inputIsPrivate) - const result = await input.showQuickPick< - QuickPickItem & { value: boolean }, - QuickPickParameters - >({ - title: this.editingText + ' - 访问权限', - placeholder: '', - step: this.currentStep, - totalSteps: Object.keys(this.inputStep).length, - items: items, - activeItems: activeItem, - canSelectMany: false, - ignoreFocusOut: true, - shouldResume: () => Promise.resolve(false), - }) - if (result && result.value != null) this.inputIsPrivate = result.value - }, - tags: async input => { - this.currentStep = 3 - const value = await input.showInputBox({ - title: this.editingText + '标签(非必填)', - step: this.currentStep, - totalSteps: Object.keys(this.inputStep).length, - placeHolder: '在此输入标签', - shouldResume: () => Promise.resolve(false), - prompt: '输入标签, 以 "," 分隔', - validateInput: () => Promise.resolve(undefined), - value: this.inputTags.join(', '), - ignoreFocusOut: true, - }) - this.inputTags = value - .split(/, ?/) - .map(x => x.trim()) - .filter(x => !!x) - }, - } - inputTags: string[] = [] - inputContent = '' - inputIsPrivate = false - currentStep = 0 - - constructor(public readonly contentSource: 'select' | 'input' = 'select') {} - - private get formattedIngContent() { - return `${this.inputTags.map(x => `[${x}]`).join('')}${this.inputContent}` - } - - async handle(): Promise { - const content = await this.getContent() - return content ? this.publish(content) : Promise.resolve() - } - - private async publish(model: IngPublishModel): Promise { - return this.onPublished( - await window.withProgress({ location: ProgressLocation.Notification, title: '正在发布...' }, p => { - p.report({ increment: 30 }) - return IngApi.publishIng(model).then(isPublished => { - p.report({ increment: 70 }) - return isPublished - }) - }) - ) - } - - private getContent(): Promise { - switch (this.contentSource) { - case 'select': - return this.getContentFromSelection() - case 'input': - return this.acquireInputContent() - } - } - - private getContentFromSelection(): Promise { - const text = window.activeTextEditor?.document.getText(window.activeTextEditor?.selection) - if (!text) { - this.warnNoSelection() - return Promise.resolve(false) - } - this.inputContent = text - return this.acquireInputContent() - } - - private async acquireInputContent(step = this.inputStep.content): Promise { - await MultiStepInput.run(step) - return this.inputContent && (await this.confirmPublish()) - ? { - content: this.formattedIngContent, - isPrivate: this.inputIsPrivate, - } - : false - } - - private async confirmPublish(): Promise { - const items = [ - ['确定', () => Promise.resolve(true)], - ['编辑内容', async () => (await this.acquireInputContent(this.inputStep.content)) !== false], - ['编辑访问权限', async () => (await this.acquireInputContent(this.inputStep.access)) !== false], - ['编辑标签', async () => (await this.acquireInputContent(this.inputStep.tags)) !== false], - ] as const - const selected = await Alert.info( - '确定要发布闪存吗?', - { - modal: true, - detail: '📝' + this.formattedIngContent + (this.inputIsPrivate ? '\n\n🔒仅自己可见' : ''), - } as MessageOptions, - ...items.map(([title]) => title) - ) - return (await items.find(x => x[0] === selected)?.[1].call(null)) ?? false - } - - private warnNoSelection() { - void Alert.warn(`无法${this.operation}, 当前没有选中的内容`) - } - - private async onPublished(isPublished: boolean): Promise { - if (isPublished) { - await getIngListWebviewProvider().refreshingList({ - ingType: this.inputIsPrivate ? IngType.my : IngType.all, - pageIndex: 1, - }) - - const options = [ - ['打开闪存', (): Thenable => execCmd('vscode.open', Uri.parse(globalCtx.config.ingSite))], - [ - '我的闪存', - (): Thenable => execCmd('vscode.open', Uri.parse(globalCtx.config.ingSite + '/#my')), - ], - [ - '新回应', - (): Thenable => - execCmd('vscode.open', Uri.parse(globalCtx.config.ingSite + '/#recentcomment')), - ], - [ - '提到我', - (): Thenable => execCmd('vscode.open', Uri.parse(globalCtx.config.ingSite + '/#mention')), - ], - ] as const - const option = await Alert.info( - '闪存已发布, 快去看看吧', - { modal: false }, - ...options.map(v => ({ title: v[0], id: v[0] })) - ) - if (option) return options.find(x => x[0] === option.id)?.[1].call(null) - } - } -} diff --git a/src/cmd/ing/refresh-ing-list.ts b/src/cmd/ing/refresh-ing-list.ts deleted file mode 100644 index fc531178..00000000 --- a/src/cmd/ing/refresh-ing-list.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { getIngListWebviewProvider } from '@/service/ing/ing-list-webview-provider' - -export const refreshIngList = () => getIngListWebviewProvider().refreshingList() diff --git a/src/cmd/ing/select-ing-type.ts b/src/cmd/ing/select-ing-type.ts deleted file mode 100644 index 40afc693..00000000 --- a/src/cmd/ing/select-ing-type.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IngType, IngTypesMetadata } from '@/model/ing' -import { getIngListWebviewProvider } from '@/service/ing/ing-list-webview-provider' -import { QuickPickItem, window } from 'vscode' - -export function switchIngType() { - const options: (QuickPickItem & { ingType: IngType })[] = IngTypesMetadata.map( - ([ingType, { displayName, description }]) => ({ - label: displayName, - ingType: ingType, - description: description, - picked: ingType === getIngListWebviewProvider().ingType, - }) - ) - const quickPick = window.createQuickPick<(typeof options)[0]>() - - quickPick.title = '选择闪存类型' - quickPick.items = options - quickPick.canSelectMany = false - quickPick.activeItems = options.filter(x => x.picked) - quickPick.selectedItems = quickPick.activeItems - quickPick.ignoreFocusOut = false - - const disposables = [quickPick] - - quickPick.onDidChangeSelection( - ([selectedItem]) => { - if (selectedItem) { - quickPick.hide() - return getIngListWebviewProvider().refreshingList({ - pageIndex: 1, - ingType: selectedItem.ingType, - }) - } - }, - undefined, - disposables - ) - quickPick.onDidHide(() => disposables.forEach(d => d.dispose()), undefined, disposables) - quickPick.show() - - return Promise.resolve() -} diff --git a/src/cmd/ing/switch-ing-list-page.ts b/src/cmd/ing/switch-ing-list-page.ts deleted file mode 100644 index 59bb0384..00000000 --- a/src/cmd/ing/switch-ing-list-page.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getIngListWebviewProvider } from '@/service/ing/ing-list-webview-provider' - -export function goIngListNextPage() { - const provider = getIngListWebviewProvider() - const { pageIndex } = provider - return provider.refreshingList({ pageIndex: pageIndex + 1 }) -} - -export function goIngListPrevPage() { - const provider = getIngListWebviewProvider() - const { pageIndex } = provider - if (pageIndex > 1) return provider.refreshingList({ pageIndex: pageIndex - 1 }) - return Promise.resolve() -} - -export function goIngList1stPage(): Promise { - return getIngListWebviewProvider().refreshingList({ pageIndex: 1 }) -} diff --git a/src/cmd/open/open-cnb-home.ts b/src/cmd/open/open-cnb-home.ts deleted file mode 100644 index dc028cbc..00000000 --- a/src/cmd/open/open-cnb-home.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { execCmd } from '@/infra/cmd' -import { Uri } from 'vscode' - -export const openCnbHome = () => execCmd('vscode.open', Uri.parse('https://www.cnblogs.com')) diff --git a/src/cmd/open/open-cnb-ing.ts b/src/cmd/open/open-cnb-ing.ts deleted file mode 100644 index a8a04bba..00000000 --- a/src/cmd/open/open-cnb-ing.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Uri } from 'vscode' -import { execCmd } from '@/infra/cmd' - -export const openCnbIng = () => execCmd('vscode.open', Uri.parse('https://ing.cnblogs.com')) diff --git a/src/cmd/open/open-cnb-news.ts b/src/cmd/open/open-cnb-news.ts deleted file mode 100644 index 14baf098..00000000 --- a/src/cmd/open/open-cnb-news.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { execCmd } from '@/infra/cmd' -import { Uri } from 'vscode' - -export const openCnbNews = () => execCmd('vscode.open', Uri.parse('https://news.cnblogs.com')) diff --git a/src/cmd/open/open-cnb-q.ts b/src/cmd/open/open-cnb-q.ts deleted file mode 100644 index 3f2f8646..00000000 --- a/src/cmd/open/open-cnb-q.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { execCmd } from '@/infra/cmd' -import { Uri } from 'vscode' - -export const openCnbQ = () => execCmd('vscode.open', Uri.parse('https://q.cnblogs.com')) diff --git a/src/cmd/open/open-my-account-setting.ts b/src/cmd/open/open-my-account-setting.ts deleted file mode 100644 index 29a64aa0..00000000 --- a/src/cmd/open/open-my-account-setting.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Uri } from 'vscode' -import { execCmd } from '@/infra/cmd' - -export const openMyAccountSetting = () => - execCmd('vscode.open', Uri.parse('https://account.cnblogs.com/settings/account')) diff --git a/src/cmd/open/open-my-blog-console.ts b/src/cmd/open/open-my-blog-console.ts deleted file mode 100644 index 9dfdda08..00000000 --- a/src/cmd/open/open-my-blog-console.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Uri } from 'vscode' -import { execCmd } from '@/infra/cmd' - -export const openMyWebBlogConsole = () => execCmd('vscode.open', Uri.parse('https://i.cnblogs.com')) diff --git a/src/cmd/open/open-my-blog.ts b/src/cmd/open/open-my-blog.ts deleted file mode 100644 index d3ac0e1c..00000000 --- a/src/cmd/open/open-my-blog.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { accountManager } from '@/auth/account-manager' -import { execCmd } from '@/infra/cmd' -import { Uri } from 'vscode' - -export const openMyBlog = () => { - const userBlogUrl = accountManager.currentUser?.website - if (userBlogUrl) return execCmd('vscode.open', Uri.parse(userBlogUrl)) -} diff --git a/src/cmd/open/open-my-home-page.ts b/src/cmd/open/open-my-home-page.ts deleted file mode 100644 index 9fdc3a57..00000000 --- a/src/cmd/open/open-my-home-page.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { accountManager } from '@/auth/account-manager' -import { execCmd } from '@/infra/cmd' -import { Uri } from 'vscode' - -export const openMyHomePage = () => { - const { accountId } = accountManager.currentUser - if (!accountId || accountId <= 0) return - - const userHomePageUrl = `https://home.cnblogs.com/u/${accountId}` - if (userHomePageUrl) void execCmd('vscode.open', Uri.parse(userHomePageUrl)) -} diff --git a/src/cmd/open/open-workspace.ts b/src/cmd/open/open-workspace.ts deleted file mode 100644 index 6ca777bc..00000000 --- a/src/cmd/open/open-workspace.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MessageOptions } from 'vscode' -import { execCmd } from '@/infra/cmd' -import { Alert } from '@/infra/alert' -import { WorkspaceCfg } from '@/ctx/cfg/workspace' - -export const openWorkspace = async () => { - const uri = WorkspaceCfg.getWorkspaceUri() - const options = ['在当前窗口中打开', '在新窗口中打开'] - const msg = `即将打开 ${uri.fsPath}` - const input = await Alert.info(msg, { modal: true } as MessageOptions, ...options) - if (input === undefined) return - - const shouldOpenInNewWindow = input === options[1] - - await execCmd('vscode.openFolder', uri, shouldOpenInNewWindow) -} diff --git a/src/cmd/open/os-open-workspace.ts b/src/cmd/open/os-open-workspace.ts deleted file mode 100644 index f1b11ed8..00000000 --- a/src/cmd/open/os-open-workspace.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { execCmd } from '@/infra/cmd' -import { WorkspaceCfg } from '@/ctx/cfg/workspace' - -export const osOpenWorkspace = () => execCmd('revealFileInOS', WorkspaceCfg.getWorkspaceUri()) diff --git a/src/cmd/pdf/export-pdf.ts b/src/cmd/pdf/export-pdf.ts index 018e763e..baf0dcf9 100644 --- a/src/cmd/pdf/export-pdf.ts +++ b/src/cmd/pdf/export-pdf.ts @@ -181,13 +181,10 @@ export async function exportPostToPdf(input?: Post | PostTreeItem | Uri): Promis if (!(input instanceof Post) && !(input instanceof PostTreeItem) && !(input instanceof Uri)) return const chromiumPath = await retrieveChromiumPath() - if (!chromiumPath) return + if (chromiumPath === undefined) return - const { - currentUser: { blogApp }, - } = accountManager - - if (!blogApp) return void Alert.warn('无法获取到博客地址, 请检查登录状态') + const blogApp = accountManager.currentUser?.userInfo.BlogApp + if (blogApp === undefined) return void Alert.warn('无法获取博客地址, 请检查登录状态') reportErrors( await window.withProgress( diff --git a/src/cmd/pdf/post-pdf-template-builder.ts b/src/cmd/pdf/post-pdf-template-builder.ts index 0443925b..1cbbff9a 100644 --- a/src/cmd/pdf/post-pdf-template-builder.ts +++ b/src/cmd/pdf/post-pdf-template-builder.ts @@ -66,7 +66,7 @@ export namespace PostPdfTemplateBuilder { blogId, } = setting - const { userId } = accountManager.currentUser + const userId = accountManager.currentUser?.userInfo.UserId return ` diff --git a/src/cmd/post-list/create-local-draft.ts b/src/cmd/post-list/create-local-draft.ts index 54e07663..80626bd6 100644 --- a/src/cmd/post-list/create-local-draft.ts +++ b/src/cmd/post-list/create-local-draft.ts @@ -5,7 +5,7 @@ import { osOpenActiveFile } from '@/cmd/open/os-open-active-file' import { openPostFile } from './open-post-file' import { WorkspaceCfg } from '@/ctx/cfg/workspace' -export const createLocalDraft = async () => { +export async function createLocalDraft() { let title = await window.showInputBox({ placeHolder: '请输入标题', prompt: `文件将会保存到 ${WorkspaceCfg.getWorkspaceUri().fsPath.replace(homedir(), '~')} 目录下`, diff --git a/src/cmd/post-list/del-post.ts b/src/cmd/post-list/del-post.ts index d8ef7629..030dfbd0 100644 --- a/src/cmd/post-list/del-post.ts +++ b/src/cmd/post-list/del-post.ts @@ -1,12 +1,12 @@ import { MessageOptions, ProgressLocation, Uri, window, workspace } from 'vscode' -import { Post } from '@/model/post' import { Alert } from '@/infra/alert' import { PostService } from '@/service/post/post' import { PostFileMap, PostFileMapManager } from '@/service/post/post-file-map' import { extTreeViews } from '@/tree-view/tree-view-register' -import { refreshPostList } from './refresh-post-list' import { PostTreeItem } from '@/tree-view/model/post-tree-item' import { postCategoryDataProvider } from '@/tree-view/provider/post-category-tree-data-provider' +import { Post } from '@/model/post' +import { PostListView } from '@/cmd/post-list/post-list-view' let isDeleting = false @@ -79,7 +79,7 @@ export async function delSelectedPost(arg: unknown) { emitEvent: false, maps: selectedPost.map(p => [p.id, '']), }) - await refreshPostList().catch() + await PostListView.refresh() postCategoryDataProvider.onPostUpdated({ refreshPost: true, postIds: selectedPost.map(({ id }) => id), diff --git a/src/cmd/post-list/open-post-in-vscode.ts b/src/cmd/post-list/open-post-in-vscode.ts index 4a422006..fd00c85f 100644 --- a/src/cmd/post-list/open-post-in-vscode.ts +++ b/src/cmd/post-list/open-post-in-vscode.ts @@ -1,6 +1,6 @@ import fs from 'fs' import path from 'path' -import { FileSystemError, MessageOptions, Uri, workspace } from 'vscode' +import { Uri, workspace } from 'vscode' import { Post } from '@/model/post' import { Alert } from '@/infra/alert' import { PostService } from '@/service/post/post' @@ -12,35 +12,35 @@ import sanitizeFileName from 'sanitize-filename' import { WorkspaceCfg } from '@/ctx/cfg/workspace' import { PostCategoryCfg } from '@/ctx/cfg/post-category' -async function buildLocalPostFileUri(post: Post, includePostId = false): Promise { +export async function buildLocalPostFileUri(post: Post, includePostId = false): Promise { const workspaceUri = WorkspaceCfg.getWorkspaceUri() const shouldCreateLocalPostFileWithCategory = PostCategoryCfg.isCreateLocalPostFileWithCategory() const ext = `.${post.isMarkdown ? 'md' : 'html'}` const postIdSegment = includePostId ? `.${post.id}` : '' const { text: postTitle } = await PostTitleSanitizer.sanitize(post) - if (shouldCreateLocalPostFileWithCategory) { - const firstCategoryId = post.categoryIds?.[0] - const category = firstCategoryId ? await PostCategoryService.find(firstCategoryId) : null - let i: typeof category | undefined = category - let categoryTitle = '' - while (i != null) { - categoryTitle = path.join( - sanitizeFileName(i.title, { - replacement: invalidChar => (invalidChar === '/' ? '_' : ''), - }), - categoryTitle - ) - i = i.parent - } - return Uri.joinPath(workspaceUri, categoryTitle, `${postTitle}${postIdSegment}${ext}`) - } else { - return Uri.joinPath(workspaceUri, `${postTitle}${postIdSegment}${ext}`) + if (!shouldCreateLocalPostFileWithCategory) return Uri.joinPath(workspaceUri, `${postTitle}${postIdSegment}${ext}`) + + const firstCategoryId = post.categoryIds?.[0] ?? null + let i = firstCategoryId ? await PostCategoryService.find(firstCategoryId) : null + let categoryTitle = '' + while (i != null) { + categoryTitle = path.join( + sanitizeFileName(i.title, { + replacement: invalidChar => (invalidChar === '/' ? '_' : ''), + }), + categoryTitle + ) + i = i.parent ?? null } + + return Uri.joinPath(workspaceUri, categoryTitle, `${postTitle}${postIdSegment}${ext}`) } export async function openPostInVscode(postId: number, forceUpdateLocalPostFile = false): Promise { let mappedPostFilePath = PostFileMapManager.getFilePath(postId) + if (mappedPostFilePath === '') mappedPostFilePath = undefined + const isFileExist = !!mappedPostFilePath && fs.existsSync(mappedPostFilePath) if (mappedPostFilePath && isFileExist && !forceUpdateLocalPostFile) { await openPostFile(mappedPostFilePath) @@ -52,44 +52,37 @@ export async function openPostInVscode(postId: number, forceUpdateLocalPostFile mappedPostFilePath = undefined } - const postEditDto = await PostService.fetchPostEditDto(postId) - if (!postEditDto) return false - - const post = postEditDto.post + const { post } = await PostService.fetchPostEditDto(postId) const workspaceUri = WorkspaceCfg.getWorkspaceUri() - await createDirectoryIfNotExist(workspaceUri) + await mkDirIfNotExist(workspaceUri) let fileUri = mappedPostFilePath ? Uri.file(mappedPostFilePath) : await buildLocalPostFileUri(post) // 博文尚未关联到本地文件的情况 - if (!mappedPostFilePath) { - // 本地存在和博文同名的文件, 询问用户是要覆盖还是同时保留两者 - if (fs.existsSync(fileUri.fsPath)) { - const opt = ['保留本地文件, 以博文 ID 为文件名新建另一个文件', '覆盖本地文件'] - const selected = await Alert.info( - `无法建立博文与本地文件的关联, 文件名冲突`, - { detail: `本地已存在名为 ${path.basename(fileUri.fsPath)} 的文件`, modal: true } as MessageOptions, - ...opt - ) + // 本地存在和博文同名的文件, 询问用户是要覆盖还是同时保留两者 + if (!mappedPostFilePath && fs.existsSync(fileUri.fsPath)) { + const opt = ['保留本地文件并以博文 ID 为文件名新建另一个文件', '覆盖本地文件'] + const selected = await Alert.info( + `无法建立博文与本地文件的关联, 文件名冲突`, + { detail: `本地已存在名为 ${path.basename(fileUri.fsPath)} 的文件`, modal: true }, + ...opt + ) - if (selected === opt[0]) fileUri = await buildLocalPostFileUri(post, true) - } + if (selected === opt[0]) fileUri = await buildLocalPostFileUri(post, true) } // 博文内容写入本地文件, 若文件不存在, 会自动创建对应的文件 - await workspace.fs.writeFile(fileUri, Buffer.from(postEditDto.post.postBody)) + await workspace.fs.writeFile(fileUri, Buffer.from(post.postBody)) await PostFileMapManager.updateOrCreate(postId, fileUri.fsPath) await openPostFile(post) return fileUri } -async function createDirectoryIfNotExist(uri: Uri) { +async function mkDirIfNotExist(uri: Uri) { try { await workspace.fs.readDirectory(uri) - } catch (err) { - if (err instanceof FileSystemError) await workspace.fs.createDirectory(uri) - - void Alert.err('Create workspace directory failed') - console.error(err) + } catch (e) { + void Alert.err(`创建目录失败: ${e}`) + throw e } } diff --git a/src/cmd/post-list/post-list-view.ts b/src/cmd/post-list/post-list-view.ts new file mode 100644 index 00000000..e528bce6 --- /dev/null +++ b/src/cmd/post-list/post-list-view.ts @@ -0,0 +1,143 @@ +import { globalCtx } from '@/ctx/global-ctx' +import { PostService } from '@/service/post/post' +import { window } from 'vscode' +import { postDataProvider } from '@/tree-view/provider/post-data-provider' +import { Alert } from '@/infra/alert' +import { PostListState } from '@/model/post-list-state' +import { extTreeViews } from '@/tree-view/tree-view-register' +import { execCmd } from '@/infra/cmd' +import { PageList } from '@/model/page' + +let refreshTask: Promise | null = null +let isRefreshing = false + +async function setRefreshing(value = false) { + const extName = globalCtx.extName + await execCmd('setContext', `${extName}.post.list-view.refreshing`, value).then(undefined, () => false) + isRefreshing = value +} + +async function setPostListContext(pageCount: number, hasPrev: boolean, hasNext: boolean) { + const extName = globalCtx.extName + await execCmd('setContext', `${extName}.post.list-view.hasPrev`, hasPrev) + await execCmd('setContext', `${extName}.post.list-view.hasNext`, hasNext) + await execCmd('setContext', `${extName}.post.list-view.pageCount`, pageCount) +} + +async function goPage(f: (currentIndex: number) => number) { + if (isRefreshing) return + + const state = PostService.getPostListState() + if (state === undefined) { + void Alert.warn('操作失败: 状态错误') + return + } + + const index = f(state.pageIndex) + if (!isPageIndexInRange(index, state)) { + void Alert.warn(`操作失败: 已达到最大页数`) + return + } + + await PostService.updatePostListStateNg(index, state.pageCap, state.pageItemCount, state.pageCount) + await PostListView.refresh() +} + +function isPageIndexInRange(pageIndex: number, state: PostListState) { + return pageIndex <= state.pageCount && pageIndex >= 1 +} + +function updatePostListViewTitle() { + const state = PostService.getPostListState() + if (state === undefined) return + + const views = [extTreeViews.postList, extTreeViews.anotherPostList] + for (const view of views) { + let title = view.title ?? '' + const idx = title.indexOf('(') + const pager = `${state.pageIndex}/${state.pageCount}` + title = idx >= 0 ? title.substring(0, idx) : title + view.title = `${title} (${pager})` + } +} + +export namespace PostListView { + export async function refresh({ queue = false } = {}): Promise { + if (isRefreshing && !queue) { + await refreshTask + return false + } else if (isRefreshing && refreshTask != null) { + await refreshTask + return refresh() + } + + const fut = async () => { + await setRefreshing(true) + const data = await postDataProvider.loadPost() + const pageIndex = data.page.index + const pageCount = data.pageCount + const pageCap = data.page.cap + const pageItemsCount = data.page.items.length + const hasPrev = PageList.hasPrev(pageIndex) + const hasNext = PageList.hasNext(pageIndex, pageCount) + + await setPostListContext(pageCount, hasPrev, hasNext) + await PostService.updatePostListStateNg(pageIndex, pageCap, pageItemsCount, pageCount) + updatePostListViewTitle() + await postDataProvider.refreshSearch() + await setRefreshing(false) + } + + refreshTask = fut() + .then(() => true) + .catch(e => { + void Alert.err(`刷新博文列表失败: ${e}`) + return false + }) + .finally(() => (refreshTask = null)) + + return refreshTask + } + + export const goNext = () => goPage(i => i + 1) + + export const goPrev = () => goPage(i => i - 1) + + export async function seek() { + const input = await window.showInputBox({ + placeHolder: '请输入页码', + validateInput: i => { + const n = Number.parseInt(i) + if (isNaN(n) || !n) return '请输入正确格式的页码' + + const state = PostService.getPostListState() + if (!state) return '博文列表尚未加载' + + if (isPageIndexInRange(n, state)) return undefined + + return `页码超出范围, 页码范围: 1-${state.pageCount}` + }, + }) + const pageIndex = Number.parseInt(input ?? '-1') + if (pageIndex > 0 && !isNaN(pageIndex)) await goPage(() => pageIndex) + } + + export namespace Search { + export async function search() { + const searchKey = await window.showInputBox({ + ignoreFocusOut: true, + title: '搜索博文', + prompt: '输入关键词搜索博文', + placeHolder: '在此输入关键词', + validateInput: value => (value.length <= 30 ? null : '最多输入30个字符'), + }) + if (searchKey === undefined) return + + await postDataProvider.search({ key: searchKey }) + } + + export const clear = () => postDataProvider.clearSearch() + + export const refresh = () => postDataProvider.refreshSearch() + } +} diff --git a/src/cmd/post-list/post-pull-all.ts b/src/cmd/post-list/post-pull-all.ts new file mode 100644 index 00000000..9d614336 --- /dev/null +++ b/src/cmd/post-list/post-pull-all.ts @@ -0,0 +1,93 @@ +import { PostService } from '@/service/post/post' +import { Alert } from '@/infra/alert' +import { PostFileMapManager } from '@/service/post/post-file-map' +import { existsSync } from 'fs' +import { basename } from 'path' +import { ProgressLocation, Uri, window, workspace } from 'vscode' +import { buildLocalPostFileUri } from '@/cmd/post-list/open-post-in-vscode' +import { accountManager } from '@/auth/account-manager' + +enum ConflictStrategy { + ask, + skip, + overwrite, +} + +async function overwriteFile(path: string, text: string) { + const uri = Uri.file(path) + const buf = Buffer.from(text) + await workspace.fs.delete(uri, { useTrash: true }) + await workspace.fs.writeFile(uri, buf) +} + +// 单次累计随笔请求上限 +const MAX_POST_LIMIT = 1000 +// 单次累计随笔字节上限 +const MAX_BYTE_LIMIT = MAX_POST_LIMIT * 10000 + +export async function postPullAll() { + const isVip = accountManager.currentUser?.userInfo.IsVip ?? false + if (!isVip) { + void Alert.info('下载随笔: 您是普通用户, 此功能目前仅面向 [VIP](https://cnblogs.vip/) 用户开放') + return + } + + let strategy = ConflictStrategy.ask + + const opt = { + title: '下载随笔', + location: ProgressLocation.Notification, + } + + let byteCount = 0 + let postCount = 0 + + await window.withProgress(opt, async p => { + for await (const post of PostService.allPostIter()) { + byteCount += Buffer.byteLength(post.postBody, 'utf-8') + postCount += 1 + if (postCount > MAX_POST_LIMIT || byteCount > MAX_BYTE_LIMIT) { + void Alert.info('下载随笔: 已达到单次请求限制, 无法下载更多随笔') + return + } + + p.report({ message: `${post.title}` }) + + const path = PostFileMapManager.getFilePath(post.id) + + // 本地没有博文或关联到的文件不存在 + if (path === undefined || !existsSync(path)) { + const uri = await buildLocalPostFileUri(post, false) + const buf = Buffer.from(post.postBody) + await workspace.fs.writeFile(uri, buf) + await PostFileMapManager.updateOrCreate(post.id, uri.path) + continue + } + + const fileName = basename(path) + + // 存在冲突 + if (strategy === ConflictStrategy.ask) { + const answer = await Alert.warn( + `拉取博文"${post.title}"时存在冲突: 已存在本地文件 ${fileName}`, + ...['跳过', '跳过所有冲突', '覆盖', '覆盖全部', '退出'] + ) + + if (answer === '覆盖') { + await overwriteFile(path, post.postBody) + } else if (answer === '退出') { + break + } else if (answer === '跳过所有冲突') { + strategy = ConflictStrategy.skip + } else if (answer === '覆盖全部') { + strategy = ConflictStrategy.overwrite + await overwriteFile(path, post.postBody) + } // answer eq undefined or '跳过', do nothing. + } else if (strategy === ConflictStrategy.overwrite) { + await overwriteFile(path, post.postBody) + } // strategy eq ConflictStrategy.skip, do nothing. + } + + void Alert.info('下载随笔: 操作完成') + }) +} diff --git a/src/cmd/post-list/post-pull.ts b/src/cmd/post-list/post-pull.ts new file mode 100644 index 00000000..47b03432 --- /dev/null +++ b/src/cmd/post-list/post-pull.ts @@ -0,0 +1,92 @@ +import { Uri, window, workspace } from 'vscode' +import { Post } from '@/model/post' +import { PostFileMapManager } from '@/service/post/post-file-map' +import { buildLocalPostFileUri } from '@/cmd/post-list/open-post-in-vscode' +import { existsSync } from 'fs' +import { PostService } from '@/service/post/post' +import { Alert } from '@/infra/alert' +import path from 'path' +import { revealPostListItem } from '@/service/post/post-list-view' +import { PostTreeItem } from '@/tree-view/model/post-tree-item' +import { MarkdownCfg } from '@/ctx/cfg/markdown' + +export async function postPull(input: Post | PostTreeItem | Uri | undefined | null) { + const ctxList: CmdCtx[] = [] + let uri: Uri | undefined + input = input instanceof PostTreeItem ? input.post : input + if (parsePostInput(input) && input.id > 0) await handlePostInput(input, ctxList) + else if ((uri = parseUriInput(input))) handleUriInput(uri, ctxList) + + const fileName = resolveFileNames(ctxList) + + if (MarkdownCfg.isShowConfirmMsgWhenPullPost()) { + const answer = await Alert.warn( + '确认要拉取远程博文吗?', + { + modal: true, + detail: `本地文件 ${fileName} 将被覆盖(可通过设置关闭对话框)`, + }, + '确认' + ) + if (answer !== '确认') return + } + + if (ctxList.length <= 0) return + + await update(ctxList) + + void Alert.info(`本地文件 ${resolveFileNames(ctxList)} 已更新`) +} + +type InputType = Post | Uri | undefined | null +type CmdCtx = { + postId: number + fileUri: Uri +} + +const parsePostInput = (input: InputType): input is Post => input instanceof Post + +async function handlePostInput(post: Post, contexts: CmdCtx[]) { + const path = PostFileMapManager.getFilePath(post.id) + // 本地没有博文或关联到的文件不存在 + if (path === undefined || !existsSync(path)) { + const uri = await buildLocalPostFileUri(post, false) + await workspace.fs.writeFile(uri, Buffer.from(post.postBody)) + await PostFileMapManager.updateOrCreate(post.id, uri.path) + return + } + + await revealPostListItem(post) + contexts.push({ postId: post.id, fileUri: Uri.file(path) }) +} + +function parseUriInput(input: InputType): Uri | undefined { + if (input instanceof Uri) return input + + const doc = window.activeTextEditor?.document + if (doc !== undefined && !doc.isUntitled) return doc.uri +} + +function handleUriInput(fileUri: Uri, contexts: CmdCtx[]) { + const postId = PostFileMapManager.getPostId(fileUri.fsPath) + if (postId === undefined) return Alert.fileNotLinkedToPost(fileUri) + + contexts.push({ postId, fileUri }) +} + +async function update(contexts: CmdCtx[]) { + for (const ctx of contexts) { + const { fileUri, postId } = ctx + + const { post } = await PostService.fetchPostEditDto(postId) + + const textEditors = window.visibleTextEditors.filter(x => x.document.uri.fsPath === fileUri.fsPath) + await Promise.all(textEditors.map(editor => editor.document.save())) + await workspace.fs.writeFile(fileUri, Buffer.from(post.postBody)) + } +} + +function resolveFileNames(ctxList: CmdCtx[]) { + const arr = ctxList.map(x => path.basename(x.fileUri.fsPath)) + return `"${arr.join('", ')}` +} diff --git a/src/cmd/post-list/refresh-post-list.ts b/src/cmd/post-list/refresh-post-list.ts deleted file mode 100644 index a21c45b8..00000000 --- a/src/cmd/post-list/refresh-post-list.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { globalCtx } from '@/ctx/global-ctx' -import { PostService } from '@/service/post/post' -import { window } from 'vscode' -import { postDataProvider } from '@/tree-view/provider/post-data-provider' -import { Alert } from '@/infra/alert' -import { PostListState } from '@/model/post-list-state' -import { extTreeViews } from '@/tree-view/tree-view-register' -import { execCmd } from '@/infra/cmd' -import { PageList } from '@/model/page' - -let refreshTask: Promise | null = null - -export async function refreshPostList({ queue = false } = {}): Promise { - if (isRefreshing && !queue) { - await refreshTask - return false - } else if (isRefreshing && refreshTask != null) { - await refreshTask - return refreshPostList() - } - - const fut = async () => { - await setRefreshing(true) - const data = await postDataProvider.loadPost() - const pageIndex = data.page.index - const pageCount = data.pageCount - const pageCap = data.page.cap - const pageItemsCount = data.page.items.length - const hasPrev = PageList.hasPrev(pageIndex) - const hasNext = PageList.hasNext(pageIndex, pageCount) - - await setPostListContext(pageCount, hasPrev, hasNext) - await PostService.updatePostListStateNg(pageIndex, pageCap, pageItemsCount, pageCount) - updatePostListViewTitle() - await postDataProvider.refreshSearch() - await setRefreshing(false) - } - - refreshTask = fut() - .then(() => true) - .catch(() => { - void Alert.err('刷新博文列表失败') - return false - }) - .finally(() => (refreshTask = null)) - - return refreshTask -} - -export const goNextPostList = () => goPage(i => i + 1) - -export const goPrevPostList = () => goPage(i => i - 1) - -export async function seekPostList() { - const input = await window.showInputBox({ - placeHolder: '请输入页码', - validateInput: i => { - const n = Number.parseInt(i) - if (isNaN(n) || !n) return '请输入正确格式的页码' - - const state = PostService.getPostListState() - if (!state) return '博文列表尚未加载' - - if (isPageIndexInRange(n, state)) return undefined - - return `页码超出范围, 页码范围: 1-${state.pageCount}` - }, - }) - const pageIndex = Number.parseInt(input ?? '-1') - if (pageIndex > 0 && !isNaN(pageIndex)) await goPage(() => pageIndex) -} - -let isRefreshing = false - -async function setRefreshing(value = false) { - const extName = globalCtx.extName - await execCmd('setContext', `${extName}.post-list.refreshing`, value).then(undefined, () => false) - isRefreshing = value -} - -async function setPostListContext(pageCount: number, hasPrev: boolean, hasNext: boolean) { - const extName = globalCtx.extName - await execCmd('setContext', `${extName}.post-list.hasPrev`, hasPrev) - await execCmd('setContext', `${extName}.post-list.hasNext`, hasNext) - await execCmd('setContext', `${extName}.post-list.pageCount`, pageCount) -} - -async function goPage(f: (currentIndex: number) => number) { - if (isRefreshing) return - - const state = PostService.getPostListState() - if (state === undefined) { - void Alert.warn('操作失败: 状态错误') - return - } - - const index = f(state.pageIndex) - if (!isPageIndexInRange(index, state)) { - void Alert.warn(`操作失败: 已达到最大页数`) - return - } - - await PostService.updatePostListStateNg(index, state.pageCap, state.pageItemCount, state.pageCount) - await refreshPostList() -} - -const isPageIndexInRange = (pageIndex: number, state: PostListState) => pageIndex <= state.pageCount && pageIndex >= 1 - -const updatePostListViewTitle = () => { - const state = PostService.getPostListState() - if (state === undefined) return - - const views = [extTreeViews.postList, extTreeViews.anotherPostList] - for (const view of views) { - let title = view.title ?? '' - const idx = title.indexOf('(') - const pager = `${state.pageIndex}/${state.pageCount}` - title = idx >= 0 ? title.substring(0, idx) : title - view.title = `${title} (${pager})` - } -} diff --git a/src/cmd/post-list/search.ts b/src/cmd/post-list/search.ts deleted file mode 100644 index f28fd766..00000000 --- a/src/cmd/post-list/search.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { window } from 'vscode' -import { postDataProvider } from '@/tree-view/provider/post-data-provider' - -export async function searchPost() { - const searchKey = await window.showInputBox({ - ignoreFocusOut: true, - title: '搜索博文', - prompt: '输入关键词搜索博文', - placeHolder: '在此输入关键词', - validateInput: value => (value.length <= 30 ? null : '最多输入30个字符'), - }) - if (!searchKey) return - - await postDataProvider.search({ key: searchKey }) -} - -export const clearPostSearchResults = () => postDataProvider.clearSearch() - -export const refreshPostSearchResults = () => postDataProvider.refreshSearch() diff --git a/src/cmd/post-list/upload-post.ts b/src/cmd/post-list/upload-post.ts index 8b6edc1a..d2421809 100644 --- a/src/cmd/post-list/upload-post.ts +++ b/src/cmd/post-list/upload-post.ts @@ -9,13 +9,13 @@ import { openPostInVscode } from './open-post-in-vscode' import { openPostFile } from './open-post-file' import { searchPostByTitle } from '@/service/post/search-post-by-title' import * as path from 'path' -import { refreshPostList } from './refresh-post-list' import { PostEditDto } from '@/model/post-edit-dto' import { PostCfgPanel } from '@/service/post/post-cfg-panel' import { saveFilePendingChanges } from '@/infra/save-file-pending-changes' import { extractImg } from '@/cmd/extract-img' import { PostTreeItem } from '@/tree-view/model/post-tree-item' import { MarkdownCfg } from '@/ctx/cfg/markdown' +import { PostListView } from '@/cmd/post-list/post-list-view' async function parseFileUri(fileUri: Uri | undefined) { if (fileUri !== undefined && fileUri.scheme !== 'file') return undefined @@ -53,7 +53,7 @@ async function saveLocalDraft(localDraft: LocalDraft) { breadcrumbs: ['新建博文', '博文设置', post.title], post, successCallback: async savedPost => { - await refreshPostList() + await PostListView.refresh() await openPostFile(localDraft) await PostFileMapManager.updateOrCreate(savedPost.id, localDraft.filePath) @@ -150,7 +150,7 @@ export async function uploadPost(input: Post | PostTreeItem | PostEditDto | unde isSaved = true progress.report({ increment: 100 }) void Alert.info('上传成功') - await refreshPostList() + await PostListView.refresh() } catch (err) { progress.report({ increment: 100 }) void Alert.err(`上传失败\n${err instanceof Error ? err.message : JSON.stringify(err)}`) @@ -257,11 +257,10 @@ export async function uploadPostNoConfirm(input: Post | PostTreeItem | PostEditD isSaved = true progress.report({ increment: 100 }) void Alert.info('上传成功') - await refreshPostList() - } catch (err) { + await PostListView.refresh() + } catch (e) { progress.report({ increment: 100 }) - void Alert.err(`上传失败\n${err instanceof Error ? err.message : JSON.stringify(err)}`) - console.error(err) + void Alert.err(`上传失败: ${e}`) } return isSaved diff --git a/src/cmd/pull-remote-post.ts b/src/cmd/pull-remote-post.ts deleted file mode 100644 index 4ea10fac..00000000 --- a/src/cmd/pull-remote-post.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { MessageOptions, Uri, window, workspace } from 'vscode' -import { Post } from '@/model/post' -import { PostFileMapManager } from '@/service/post/post-file-map' -import { openPostInVscode } from './post-list/open-post-in-vscode' -import fs from 'fs' -import { PostService } from '@/service/post/post' -import { Alert } from '@/infra/alert' -import path from 'path' -import { revealPostListItem } from '@/service/post/post-list-view' -import { PostTreeItem } from '@/tree-view/model/post-tree-item' -import { MarkdownCfg } from '@/ctx/cfg/markdown' - -export async function pullRemotePost(input: Post | PostTreeItem | Uri | undefined | null) { - const ctxList: CmdCtx[] = [] - let uri: Uri | undefined - input = input instanceof PostTreeItem ? input.post : input - if (parsePostInput(input) && input.id > 0) await handlePostInput(input, ctxList) - else if ((uri = parseUriInput(input))) await handleUriInput(uri, ctxList) - - if (MarkdownCfg.isShowConfirmMsgWhenPullPost()) { - const answer = await Alert.warn( - '确认要拉取远程博文吗?', - { - modal: true, - detail: `本地文件 ${resolveFileNames(ctxList)} 将被覆盖(可通过设置关闭对话框)`, - } as MessageOptions, - '确认' - ) - if (answer !== '确认') return - } - - if (ctxList.length <= 0) return - - await update(ctxList) - - void Alert.info(`本地文件${resolveFileNames(ctxList)}已更新`) -} - -type InputType = Post | Uri | undefined | null -type CmdCtx = { postId: number; fileUri: Uri } - -const parsePostInput = (input: InputType): input is Post => input instanceof Post - -async function handlePostInput(post: Post, contexts: CmdCtx[]) { - const { id: postId } = post - let filePath = PostFileMapManager.getFilePath(postId) - if (filePath && !fs.existsSync(filePath)) { - // 博文关联了本地不存在文件, 此时需要删除这个关联 - - filePath = '' - await PostFileMapManager.updateOrCreate(postId, filePath) - } - if (!filePath) { - // 本地没有这篇博文, 直接将博文下载到本地即可 - return void (await openPostInVscode(postId, false)) - } - - await revealPostListItem(post) - contexts.push({ postId: postId, fileUri: Uri.file(filePath) }) -} - -const parseUriInput = (input: InputType): Uri | undefined => { - if (input instanceof Uri) return input - - const { document } = window.activeTextEditor ?? {} - if (document && !document.isUntitled) return document.uri -} - -const handleUriInput = (fileUri: Uri, contexts: CmdCtx[]): Promise => { - const postId = PostFileMapManager.getPostId(fileUri.fsPath) - if (!postId) return Promise.resolve().then(() => Alert.fileNotLinkedToPost(fileUri)) - - contexts.push({ postId, fileUri }) - return Promise.resolve() -} - -const update = async (contexts: CmdCtx[]) => { - for (const ctx of contexts) { - const { fileUri, postId } = ctx - const { post } = (await PostService.fetchPostEditDto(postId)) ?? {} - if (post) { - const textEditors = window.visibleTextEditors.filter(x => x.document.uri.fsPath === fileUri.fsPath) - await Promise.all(textEditors.map(editor => editor.document.save())) - await workspace.fs.writeFile(fileUri, Buffer.from(post.postBody)) - } - } -} - -const resolveFileNames = (ctxList: CmdCtx[]) => `"${ctxList.map(x => path.basename(x.fileUri.fsPath)).join('", ')}"` diff --git a/src/cmd/set-workspace.ts b/src/cmd/set-workspace.ts deleted file mode 100644 index bbfde82b..00000000 --- a/src/cmd/set-workspace.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { window } from 'vscode' -import { Alert } from '@/infra/alert' -import { WorkspaceCfg } from '@/ctx/cfg/workspace' - -export async function setWorkspace() { - const uris = - (await window.showOpenDialog({ - title: '选择工作空间', - canSelectFolders: true, - canSelectFiles: false, - canSelectMany: false, - defaultUri: WorkspaceCfg.getWorkspaceUri(), - })) ?? [] - - const firstUri = uris[0] - - if (firstUri === undefined) return - - await WorkspaceCfg.setWorkspaceUri(firstUri) - void Alert.info(`工作空间成功修改为: "${WorkspaceCfg.getWorkspaceUri().fsPath}"`) -} diff --git a/src/cmd/workspace.ts b/src/cmd/workspace.ts new file mode 100644 index 00000000..ffcc8533 --- /dev/null +++ b/src/cmd/workspace.ts @@ -0,0 +1,42 @@ +import { MessageOptions } from 'vscode' +import { execCmd } from '@/infra/cmd' +import { Alert } from '@/infra/alert' +import { WorkspaceCfg } from '@/ctx/cfg/workspace' +import { window } from 'vscode' + +export namespace Workspace { + export async function codeOpen() { + const uri = WorkspaceCfg.getWorkspaceUri() + const options = ['在当前窗口中打开', '在新窗口中打开'] + const msg = `即将打开 ${uri.fsPath}` + const input = await Alert.info(msg, { modal: true } as MessageOptions, ...options) + if (input === undefined) return + + const shouldOpenInNewWindow = input === options[1] + + await execCmd('vscode.openFolder', uri, shouldOpenInNewWindow) + } + + export function osOpen() { + void execCmd('revealFileInOS', WorkspaceCfg.getWorkspaceUri()) + } + + export async function set() { + const uris = await window.showOpenDialog({ + title: '选择工作空间', + canSelectFolders: true, + canSelectFiles: false, + canSelectMany: false, + defaultUri: WorkspaceCfg.getWorkspaceUri(), + }) + + if (uris === undefined) return + + const firstUri = uris[0] + + if (firstUri === undefined) return + + await WorkspaceCfg.setWorkspaceUri(firstUri) + void Alert.info(`工作空间成功修改为: "${WorkspaceCfg.getWorkspaceUri().fsPath}"`) + } +} diff --git a/src/infra/uri-handler.ts b/src/infra/uri-handler.ts index c4a4d72e..53bfde0f 100644 --- a/src/infra/uri-handler.ts +++ b/src/infra/uri-handler.ts @@ -13,7 +13,7 @@ class ExtUriHandler implements UriHandler { evEmitter.event(uri => { const { path } = uri const splits = path.split('/') - if (splits.length >= 3 && splits[1] === 'edit-post') { + if (splits.length >= 3 && splits[1] === 'post.edit') { const postId = parseInt(splits[2]) if (postId > 0) openPostInVscode(postId).then(undefined, () => void 0) } diff --git a/src/model/blog-export/export-post.ts b/src/model/blog-export/export-post.ts index d7a72a1b..c631ca62 100644 --- a/src/model/blog-export/export-post.ts +++ b/src/model/blog-export/export-post.ts @@ -1,16 +1,14 @@ -import { Post } from '@/model/post' import { Model } from 'sequelize' +import { Post } from '@/model/post' -export type PostType = 'BlogPost' | 'Message' +export type ExportPostType = 'BlogPost' | 'Message' export type ExportPost = Pick< Post, 'id' | 'title' | 'datePublished' | 'blogId' | 'isMarkdown' | 'accessPermission' | 'entryName' | 'dateUpdated' > & { body?: string | null - postType: PostType + postType: ExportPostType } export class ExportPostModel extends Model {} - -export { Post } from '@/model/post' diff --git a/src/model/blog-post.ts b/src/model/blog-post.ts new file mode 100644 index 00000000..52508d65 --- /dev/null +++ b/src/model/blog-post.ts @@ -0,0 +1,43 @@ +import { AccessPermission, PostType } from '@/model/post' + +export type BlogPost = { + id: number + postType: PostType + accessPermission: AccessPermission + title: string + url: string + postBody: string + categoryIds: [] + collectionIds: [] + inSiteCandidate: boolean + inSiteHome: boolean + siteCategoryId: null + blogTeamIds: [] + isPublished: boolean + displayOnHomePage: boolean + isAllowComments: boolean + includeInMainSyndication: boolean + isPinned: boolean + isOnlyForRegisterUser: boolean + isUpdateDateAdded: boolean + entryName: null + description: string + featuredImage: null + tags: [] + password: null + datePublished: string + dateUpdated: string + isMarkdown: boolean + isDraft: boolean + autoDesc: string + changePostType: boolean + blogId: number + author: string + removeScript: boolean + clientInfo: null + changeCreatedTime: boolean + canChangeCreatedTime: boolean + isContributeToImpressiveBugActivity: boolean + usingEditorId: null + sourceUrl: null +} diff --git a/src/model/my-config.ts b/src/model/my-config.ts new file mode 100644 index 00000000..190bd4c6 --- /dev/null +++ b/src/model/my-config.ts @@ -0,0 +1,84 @@ +import { AccessPermission, PostType } from '@/model/post' + +export type PostListRespItem = { + accessPermission: AccessPermission + aggCount: number + + datePublished: string + dateUpdated: string + + entryName: string + feedBackCount: number + + id: number + + isDraft: boolean + isInSiteCandidate: boolean + isInSiteHome: boolean + isMarkdown: boolean + isPinned: boolean + isPublished: boolean + + postConfig: number + postType: PostType + + title: string + url: string + viewCount: number + webCount: number +} + +export type BlogPost = { + id: number + postType: PostType + accessPermission: AccessPermission + title: string + url: string + postBody: string + categoryIds: [] + collectionIds: [] + inSiteCandidate: boolean + inSiteHome: boolean + siteCategoryId: null + blogTeamIds: [] + isPublished: boolean + displayOnHomePage: boolean + isAllowComments: boolean + includeInMainSyndication: boolean + isPinned: boolean + isOnlyForRegisterUser: boolean + isUpdateDateAdded: boolean + entryName: null + description: string + featuredImage: null + tags: [] + password: null + datePublished: string + dateUpdated: string + isMarkdown: boolean + isDraft: boolean + autoDesc: string + changePostType: boolean + blogId: number + author: string + removeScript: boolean + clientInfo: null + changeCreatedTime: boolean + canChangeCreatedTime: boolean + isContributeToImpressiveBugActivity: boolean + usingEditorId: null + sourceUrl: null +} + +export type MyConfig = { + canInSiteCandidate: boolean + noSiteCandidateMsg: string + canInSiteHome: boolean + noSiteHomeMsg: string + myTeamCollection: [] + editor: { + id: number + host: string + cdnRefreshId: number + } +} diff --git a/src/model/post-cfg.ts b/src/model/post-cfg.ts index 7ba043e8..dadce63e 100644 --- a/src/model/post-cfg.ts +++ b/src/model/post-cfg.ts @@ -1,6 +1,4 @@ -import { Post } from './post' +import { Post } from '@/model/post' -type PostCfg = Partial> & +export type PostCfg = Partial> & Required<{ [P in keyof Post as Post[P] extends boolean ? P : never]: Post[P] }> - -export { PostCfg } diff --git a/src/model/post-edit-dto.ts b/src/model/post-edit-dto.ts index f74e7781..b7e64e09 100644 --- a/src/model/post-edit-dto.ts +++ b/src/model/post-edit-dto.ts @@ -1,4 +1,5 @@ -import { MyConfig, Post } from '@/model/post' +import { Post } from '@/model/post' +import { MyConfig } from '@/model/my-config' export type PostEditDto = { post: Post diff --git a/src/model/post-list-resp-item.ts b/src/model/post-list-resp-item.ts new file mode 100644 index 00000000..c0fad826 --- /dev/null +++ b/src/model/post-list-resp-item.ts @@ -0,0 +1,29 @@ +import { AccessPermission, PostType } from '@/model/post' + +export type PostListRespItem = { + accessPermission: AccessPermission + aggCount: number + + datePublished: string + dateUpdated: string + + entryName: string + feedBackCount: number + + id: number + + isDraft: boolean + isInSiteCandidate: boolean + isInSiteHome: boolean + isMarkdown: boolean + isPinned: boolean + isPublished: boolean + + postConfig: number + postType: PostType + + title: string + url: string + viewCount: number + webCount: number +} diff --git a/src/model/post.ts b/src/model/post.ts index 9108db9b..651430e8 100644 --- a/src/model/post.ts +++ b/src/model/post.ts @@ -1,89 +1,6 @@ import differenceInSeconds from 'date-fns/differenceInSeconds' import parseISO from 'date-fns/parseISO' -export type PostListRespItem = { - accessPermission: AccessPermission - aggCount: number - - datePublished: string - dateUpdated: string - - entryName: string - feedBackCount: number - - id: number - - isDraft: boolean - isInSiteCandidate: boolean - isInSiteHome: boolean - isMarkdown: boolean - isPinned: boolean - isPublished: boolean - - postConfig: number - postType: PostType - - title: string - url: string - viewCount: number - webCount: number -} - -export type BlogPost = { - id: number - postType: PostType - accessPermission: AccessPermission - title: string - url: string - postBody: string - categoryIds: [] - collectionIds: [] - inSiteCandidate: boolean - inSiteHome: boolean - siteCategoryId: null - blogTeamIds: [] - isPublished: boolean - displayOnHomePage: boolean - isAllowComments: boolean - includeInMainSyndication: boolean - isPinned: boolean - isOnlyForRegisterUser: boolean - isUpdateDateAdded: boolean - entryName: null - description: string - featuredImage: null - tags: [] - password: null - datePublished: string - dateUpdated: string - isMarkdown: boolean - isDraft: boolean - autoDesc: string - changePostType: boolean - blogId: number - author: string - removeScript: boolean - clientInfo: null - changeCreatedTime: boolean - canChangeCreatedTime: boolean - isContributeToImpressiveBugActivity: boolean - usingEditorId: null - sourceUrl: null -} - -export type MyConfig = { - canInSiteCandidate: boolean - noSiteCandidateMsg: string - canInSiteHome: boolean - noSiteHomeMsg: string - myTeamCollection: [] - editor: { - id: number - host: string - cdnRefreshId: number - } -} - export class Post { id = -1 author = '' diff --git a/src/model/webview-cmd.ts b/src/model/webview-cmd.ts index e40d852a..f249c1a9 100644 --- a/src/model/webview-cmd.ts +++ b/src/model/webview-cmd.ts @@ -1,7 +1,7 @@ import { PostCategory } from '@/model/post-category' -export namespace WebviewCmd { - export enum UiCmd { +export namespace Webview.Cmd { + export enum Ui { editPostCfg = 'editPostCfg', showErrorResponse = 'showErrorResponse', updateBreadcrumbs = 'updateBreadcrumbs', @@ -11,7 +11,7 @@ export namespace WebviewCmd { updateChildCategories = 'updateChildCategories', } - export enum ExtCmd { + export enum Ext { uploadPost = 'uploadPost', disposePanel = 'disposePanel', uploadImg = 'uploadImg', @@ -19,22 +19,22 @@ export namespace WebviewCmd { getChildCategories = 'getChildCategories', } - export interface GetChildCategoriesPayload { + export type GetChildCategoriesPayload = { parentId: number } - export interface UpdateChildCategoriesPayload { + export type UpdateChildCategoriesPayload = { parentId: number value: PostCategory[] } - export namespace IngCmd { - export enum UiCmd { + export namespace Ing { + export enum Ui { setAppState = 'setAppState', updateTheme = 'updateTheme', } - export enum ExtCmd { + export enum Ext { refreshingList = 'refreshingList', comment = 'comment', } @@ -55,10 +55,10 @@ export interface WebviewCommonCmd { export interface IngWebviewUiCmd = Record> extends WebviewCommonCmd { - command: WebviewCmd.IngCmd.UiCmd + command: Webview.Cmd.Ing.Ui } export interface IngWebviewHostCmd = Record> extends WebviewCommonCmd { - command: WebviewCmd.IngCmd.ExtCmd + command: Webview.Cmd.Ing.Ext } diff --git a/src/model/webview-msg.ts b/src/model/webview-msg.ts index 6b7d0b4c..06706551 100644 --- a/src/model/webview-msg.ts +++ b/src/model/webview-msg.ts @@ -1,5 +1,5 @@ import { Post } from './post' -import { WebviewCmd } from './webview-cmd' +import { Webview } from './webview-cmd' import { ColorThemeKind } from 'vscode' import { PostTags } from './post-tag' import { IErrorResponse as ErrorResponse } from './error-response' @@ -9,7 +9,7 @@ import { PostCategory } from '@/model/post-category' export namespace WebviewMsg { export interface Msg { - command: WebviewCmd.UiCmd | WebviewCmd.ExtCmd + command: Webview.Cmd.Ui | Webview.Cmd.Ext } export interface EditPostCfgMsg extends Msg { diff --git a/src/service/ing/ing-api.ts b/src/service/ing/ing-api.ts index 34591553..3fa983cd 100644 --- a/src/service/ing/ing-api.ts +++ b/src/service/ing/ing-api.ts @@ -1,26 +1,27 @@ -import { Ing, IngComment, IngPublishModel, IngType } from '@/model/ing' +import { Ing, IngComment, IngType } from '@/model/ing' import { Alert } from '@/infra/alert' -import { globalCtx } from '@/ctx/global-ctx' -import { consUrlPara } from '@/infra/http/infra/url-para' -import { consHeader, ReqHeaderKey } from '@/infra/http/infra/header' -import { AuthedReq } from '@/infra/http/authed-req' -import ContentType = ReqHeaderKey.ContentType +import { IngReq } from '@/wasm' +import { AccountManagerNg } from '@/auth/account-manager' + +async function getAuthedIngReq() { + const token = await AccountManagerNg.acquireToken() + // TODO: need better solution + const isPatToken = token.length === 64 + return new IngReq(token, isPatToken) +} -async function getIngComment(id: number) { - const url = `${globalCtx.config.openApiUrl}/api/statuses/${id}/comments` - const resp = await AuthedReq.get(url, consHeader()) +async function getComment(id: number) { + const req = await getAuthedIngReq() + const resp = await req.getComment(id) const list = JSON.parse(resp) as [] return list.map(IngComment.parse) } export namespace IngApi { - export async function publishIng(ing: IngPublishModel): Promise { - const url = `${globalCtx.config.openApiUrl}/api/statuses` - const header = consHeader([ReqHeaderKey.CONTENT_TYPE, ContentType.appJson]) - const body = JSON.stringify(ing) - + export async function pub(content: string, isPrivate: boolean) { try { - await AuthedReq.post(url, header, body) + const req = await getAuthedIngReq() + await req.pub(content, isPrivate) return true } catch (e) { void Alert.err(`闪存发布失败: ${e}`) @@ -28,46 +29,28 @@ export namespace IngApi { } } - export async function list({ pageIndex = 1, pageSize = 30, type = IngType.all } = {}) { - const para = consUrlPara(['pageIndex', `${pageIndex}`], ['pageSize', `${pageSize}`]) - const url = `${globalCtx.config.openApiUrl}/api/statuses/@${type}?${para}` - - let list: Ing[] + export async function getList({ pageIndex = 1, pageSize = 30, type = IngType.all } = {}) { try { - const resp = await AuthedReq.get(url, consHeader()) + const req = await getAuthedIngReq() + const resp = await req.getList(pageIndex, pageSize, type) const arr = JSON.parse(resp) as unknown[] - list = arr.map(Ing.parse) + return arr.map(Ing.parse) } catch (e) { void Alert.err(`获取闪存列表失败: ${e}`) - list = [] + return [] } - - return list } - export async function listComments(...ingIds: number[]) { - const futList = ingIds.map(async id => { - const comment = await getIngComment(id) - return { [id]: comment } - }) + export async function getCommentList(...ingIds: number[]) { + const futList = ingIds.map(async id => ({ [id]: await getComment(id) })) const resList = await Promise.all(futList) return resList.reduce((acc, it) => Object.assign(it, acc), {}) } - export async function comment( - ingId: number, - data: { - replyTo?: number - parentCommentId?: number - content: string - } - ) { - const url = `${globalCtx.config.openApiUrl}/api/statuses/${ingId}/comments` - const header = consHeader([ReqHeaderKey.CONTENT_TYPE, ContentType.appJson]) - const body = JSON.stringify(data) - + export async function comment(ingId: number, content: string, replyTo?: number, parentCommentId?: number) { try { - await AuthedReq.post(url, header, body) + const req = await getAuthedIngReq() + await req.comment(ingId, content, replyTo, parentCommentId) return true } catch (e) { void Alert.err(`发表评论失败, ${e}`) diff --git a/src/service/ing/ing-list-webview-provider.ts b/src/service/ing/ing-list-webview-provider.ts index 17b49de8..3b1a3c7a 100644 --- a/src/service/ing/ing-list-webview-provider.ts +++ b/src/service/ing/ing-list-webview-provider.ts @@ -2,14 +2,14 @@ import { globalCtx } from '@/ctx/global-ctx' import { CancellationToken, Disposable, - Webview, + Webview as CodeWebview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, } from 'vscode' import { parseWebviewHtml } from '@/service/parse-webview-html' -import { IngWebviewHostCmd, IngWebviewUiCmd, WebviewCmd } from '@/model/webview-cmd' +import { IngWebviewHostCmd, IngWebviewUiCmd, Webview } from '@/model/webview-cmd' import { IngApi } from '@/service/ing/ing-api' import { IngAppState } from '@/model/ing-view' import { IngType, IngTypesMetadata } from '@/model/ing' @@ -61,7 +61,7 @@ export class IngListWebviewProvider implements WebviewViewProvider { webviewView.webview.onDidReceiveMessage(this.observer.observer, disposables) webviewView.webview.html = await this.provideHtml(webviewView.webview) window.onDidChangeActiveColorTheme( - () => webviewView.webview.postMessage(WebviewCmd.UiCmd.updateTheme), + () => webviewView.webview.postMessage(Webview.Cmd.Ui.updateTheme), disposables ) webviewView.onDidDispose(() => { @@ -81,10 +81,10 @@ export class IngListWebviewProvider implements WebviewViewProvider { await this._view.webview .postMessage({ payload: { isRefreshing: true }, - command: WebviewCmd.IngCmd.UiCmd.setAppState, + command: Webview.Cmd.Ing.Ui.setAppState, } as IngWebviewUiCmd>) .then(undefined, () => undefined) - const rawIngList = await IngApi.list({ + const rawIngList = await IngApi.getList({ type: ingType, pageIndex, pageSize: 30, @@ -94,10 +94,10 @@ export class IngListWebviewProvider implements WebviewViewProvider { if (UiCfg.isEnableTextIngStar()) ing.icons = ingStarToText(ing.icons) return ing }) - const comments = await IngApi.listComments(...ingList.map(x => x.id)) + const comments = await IngApi.getCommentList(...ingList.map(x => x.id)) await this._view.webview .postMessage({ - command: WebviewCmd.IngCmd.UiCmd.setAppState, + command: Webview.Cmd.Ing.Ui.setAppState, payload: { ingList, isRefreshing: false, @@ -117,16 +117,16 @@ export class IngListWebviewProvider implements WebviewViewProvider { async updateComments(ingIds: number[]) { if (!this._view || !this._view.visible) return - const comments = await IngApi.listComments(...ingIds) + const comments = await IngApi.getCommentList(...ingIds) await this._view.webview.postMessage({ - command: WebviewCmd.IngCmd.UiCmd.setAppState, + command: Webview.Cmd.Ing.Ui.setAppState, payload: { comments, }, } as IngWebviewUiCmd>) } - private provideHtml(webview: Webview) { + private provideHtml(webview: CodeWebview) { return parseWebviewHtml('ing', webview) } @@ -171,7 +171,7 @@ class IngWebviewMessageObserver { observer = ({ command, payload }: IngWebviewHostCmd) => { switch (command) { - case WebviewCmd.IngCmd.ExtCmd.refreshingList: { + case Webview.Cmd.Ing.Ext.refreshingList: { const { ingType, pageIndex } = payload return this._provider.refreshingList({ ingType: @@ -181,8 +181,8 @@ class IngWebviewMessageObserver { pageIndex: isNumber(pageIndex) ? pageIndex : undefined, }) } - case WebviewCmd.IngCmd.ExtCmd.comment: { - const { atUser, ingId, ingContent, parentCommentId } = payload as WebviewCmd.IngCmd.CommentCmdPayload + case Webview.Cmd.Ing.Ext.comment: { + const { atUser, ingId, ingContent, parentCommentId } = payload as Webview.Cmd.Ing.CommentCmdPayload return new CommentIngCmdHandler(ingId, ingContent, parentCommentId, atUser).handle() } } diff --git a/src/service/post/post-cfg-panel.ts b/src/service/post/post-cfg-panel.ts index 08345e65..53f57e19 100644 --- a/src/service/post/post-cfg-panel.ts +++ b/src/service/post/post-cfg-panel.ts @@ -8,7 +8,7 @@ import { PostTagService } from './post-tag' import { PostService } from './post' import { isErrorResponse } from '@/model/error-response' import { WebviewMsg } from '@/model/webview-msg' -import { WebviewCommonCmd, WebviewCmd } from '@/model/webview-cmd' +import { WebviewCommonCmd, Webview } from '@/model/webview-cmd' import { uploadImg } from '@/cmd/upload-img/upload-img' import { ImgUploadStatusId } from '@/model/img-upload-status' import { openPostFile } from '@/cmd/post-list/open-post-file' @@ -51,13 +51,13 @@ export namespace PostCfgPanel { disposables.push( webview.onDidReceiveMessage(async ({ command }: WebviewMsg.Msg) => { - if (command === WebviewCmd.ExtCmd.refreshPost) { + if (command === Webview.Cmd.Ext.refreshPost) { await webview.postMessage({ - command: WebviewCmd.UiCmd.setFluentIconBaseUrl, + command: Webview.Cmd.Ui.setFluentIconBaseUrl, baseUrl: webview.asWebviewUri(Uri.joinPath(resourceRootUri(), 'fonts')).toString() + '/', } as WebviewMsg.SetFluentIconBaseUrlMsg) await webview.postMessage({ - command: WebviewCmd.UiCmd.editPostCfg, + command: Webview.Cmd.Ui.editPostCfg, post: cloneDeep(post), activeTheme: vscode.window.activeColorTheme.kind, personalCategories: cloneDeep(await PostCategoryService.listCategories()), @@ -89,7 +89,7 @@ export namespace PostCfgPanel { const { breadcrumbs } = options const { webview } = panel void webview.postMessage({ - command: WebviewCmd.UiCmd.updateBreadcrumbs, + command: Webview.Cmd.Ui.updateBreadcrumbs, breadcrumbs, } as WebviewMsg.UpdateBreadcrumbMsg) panel.reveal() @@ -118,7 +118,7 @@ export namespace PostCfgPanel { const { webview } = panel await webview.postMessage({ - command: WebviewCmd.UiCmd.updateImageUploadStatus, + command: Webview.Cmd.Ui.updateImageUploadStatus, status: { id: ImgUploadStatusId.uploading, }, @@ -127,7 +127,7 @@ export namespace PostCfgPanel { try { const imageUrl = await uploadImg() await webview.postMessage({ - command: WebviewCmd.UiCmd.updateImageUploadStatus, + command: Webview.Cmd.Ui.updateImageUploadStatus, status: { imageUrl, id: ImgUploadStatusId.uploaded, @@ -137,7 +137,7 @@ export namespace PostCfgPanel { } catch (err) { if (isErrorResponse(err)) { await webview.postMessage({ - command: WebviewCmd.UiCmd.updateImageUploadStatus, + command: Webview.Cmd.Ui.updateImageUploadStatus, status: { id: ImgUploadStatusId.failed, errors: err.errors, @@ -154,7 +154,7 @@ export namespace PostCfgPanel { const { webview } = panel return vscode.window.onDidChangeActiveColorTheme(async theme => { await webview.postMessage({ - command: WebviewCmd.UiCmd.updateTheme, + command: Webview.Cmd.Ui.updateTheme, colorThemeKind: theme.kind, } as WebviewMsg.ChangeThemeMsg) }) @@ -171,7 +171,7 @@ export namespace PostCfgPanel { return webview.onDidReceiveMessage(async message => { const { command } = (message ?? {}) as WebviewMsg.Msg switch (command) { - case WebviewCmd.ExtCmd.uploadPost: + case Webview.Cmd.Ext.uploadPost: try { if (!panel) return @@ -188,7 +188,7 @@ export namespace PostCfgPanel { } catch (err) { if (isErrorResponse(err)) { await webview.postMessage({ - command: WebviewCmd.UiCmd.showErrorResponse, + command: Webview.Cmd.Ui.showErrorResponse, errorResponse: err, } as WebviewMsg.ShowErrRespMsg) } else { @@ -196,24 +196,24 @@ export namespace PostCfgPanel { } } break - case WebviewCmd.ExtCmd.disposePanel: + case Webview.Cmd.Ext.disposePanel: panel?.dispose() break - case WebviewCmd.ExtCmd.uploadImg: + case Webview.Cmd.Ext.uploadImg: await onUploadImageCmd(panel, message) break - case WebviewCmd.ExtCmd.getChildCategories: + case Webview.Cmd.Ext.getChildCategories: { - const { payload } = message as WebviewCommonCmd + const { payload } = message as WebviewCommonCmd await webview.postMessage({ - command: WebviewCmd.UiCmd.updateChildCategories, + command: Webview.Cmd.Ui.updateChildCategories, payload: { value: await PostCategoryService.listCategories({ parentId: payload.parentId }).catch( () => [] ), parentId: payload.parentId, }, - } as WebviewCommonCmd) + } as WebviewCommonCmd) } break } diff --git a/src/service/post/post-list-view.ts b/src/service/post/post-list-view.ts index a7b686f6..293801e7 100644 --- a/src/service/post/post-list-view.ts +++ b/src/service/post/post-list-view.ts @@ -1,11 +1,11 @@ import { Post } from '@/model/post' import { extTreeViews } from '@/tree-view/tree-view-register' -export const revealPostListItem = async ( - post: Post, +export async function revealPostListItem( + post: Post | undefined, options?: { select?: boolean; focus?: boolean; expand?: boolean | number } -) => { - if (!post) return +) { + if (post === undefined) return const view = extTreeViews.visiblePostList() await view?.reveal(post, options) diff --git a/src/service/post/post.ts b/src/service/post/post.ts index e315e847..c043bb4c 100644 --- a/src/service/post/post.ts +++ b/src/service/post/post.ts @@ -1,4 +1,3 @@ -import { MyConfig, Post, PostListRespItem } from '@/model/post' import { globalCtx } from '@/ctx/global-ctx' import { PostEditDto } from '@/model/post-edit-dto' import { PostUpdatedResp } from '@/model/post-updated-response' @@ -11,6 +10,9 @@ import { consUrlPara } from '@/infra/http/infra/url-para' import { consHeader, ReqHeaderKey } from '@/infra/http/infra/header' import { AuthedReq } from '@/infra/http/authed-req' import { Page, PageList } from '@/model/page' +import { Post } from '@/model/post' +import { PostListRespItem } from '@/model/post-list-resp-item' +import { MyConfig } from '@/model/my-config' let newPostTemplate: PostEditDto | undefined @@ -45,6 +47,18 @@ export namespace PostService { } } + // TODO: need better impl + export async function* allPostIter() { + const result = await PostService.fetchPostList({ pageSize: 1 }) + const postCount = result.matchedPostCount + for (const i of Array(postCount).keys()) { + const { page } = await PostService.fetchPostList({ pageIndex: i + 1, pageSize: 1 }) + const id = page.items[0].id + const dto = await PostService.fetchPostEditDto(id) + yield dto.post + } + } + export async function fetchPostEditDto(postId: number) { const url = `${getBaseUrl()}/api/posts/${postId}` @@ -53,7 +67,8 @@ export namespace PostService { const { blogPost, myConfig } = <{ blogPost?: Post; myConfig?: MyConfig }>JSON.parse(resp) - if (blogPost === undefined) return + // TODO: need better impl + if (blogPost === undefined) throw Error('博文不存在') return { post: Object.assign(new Post(), blogPost), @@ -61,6 +76,7 @@ export namespace PostService { } } catch (e) { void Alert.err(`获取博文失败: ${e}`) + throw e } } diff --git a/src/setup/setup-cmd.ts b/src/setup/setup-cmd.ts index 8ef8f156..6ba1dbfd 100644 --- a/src/setup/setup-cmd.ts +++ b/src/setup/setup-cmd.ts @@ -1,42 +1,17 @@ -import { switchIngType } from '@/cmd/ing/select-ing-type' -import { refreshIngList } from '@/cmd/ing/refresh-ing-list' -import { goIngList1stPage, goIngListNextPage, goIngListPrevPage } from '@/cmd/ing/switch-ing-list-page' -import { openMyAccountSetting } from '@/cmd/open/open-my-account-setting' -import { openMyWebBlogConsole } from '@/cmd/open/open-my-blog-console' -import { openMyHomePage } from '@/cmd/open/open-my-home-page' -import { openMyBlog } from '@/cmd/open/open-my-blog' import { globalCtx } from '@/ctx/global-ctx' -import { goNextPostList, goPrevPostList, refreshPostList, seekPostList } from '@/cmd/post-list/refresh-post-list' import { uploadPostFile, uploadPost, uploadPostNoConfirm, uploadPostFileNoConfirm } from '@/cmd/post-list/upload-post' -import { createLocalDraft } from '@/cmd/post-list/create-local-draft' -import { delSelectedPost } from '@/cmd/post-list/del-post' -import { modifyPostSetting } from '@/cmd/post-list/modify-post-setting' import { uploadImg } from '@/cmd/upload-img/upload-img' import { osOpenLocalPostFile } from '@/cmd/open/os-open-local-post-file' import { showLocalFileToPostInfo } from '@/cmd/show-local-file-to-post-info' import { newPostCategory } from '@/cmd/post-category/new-post-category' import { refreshPostCategoryList } from '@/cmd/post-category/refresh-post-category-list' import { handleUpdatePostCategory } from '@/cmd/post-category/update-post-category' -import { openPostInVscode } from '@/cmd/post-list/open-post-in-vscode' -import { delPostToLocalFileMap } from '@/cmd/post-list/del-post-to-local-file-map' -import { renamePost } from '@/cmd/post-list/rename-post' import { openPostInBlogAdmin } from '@/cmd/open/open-post-in-blog-admin' -import { openWorkspace } from '@/cmd/open/open-workspace' -import { setWorkspace } from '@/cmd/set-workspace' -import { osOpenWorkspace } from '@/cmd/open/os-open-workspace' import { viewPostOnline } from '@/cmd/view-post-online' -import { pullRemotePost } from '@/cmd/pull-remote-post' import { extractImg } from '@/cmd/extract-img' -import { clearPostSearchResults, refreshPostSearchResults, searchPost } from '@/cmd/post-list/search' import { handleDeletePostCategories } from '@/cmd/post-category/del-selected-category' -import { PublishIngCmdHandler } from '@/cmd/ing/publish-ing' -import { CopyPostLinkCmdHandler } from '@/cmd/post-list/copy-link' import { regCmd } from '@/infra/cmd' import { exportPostToPdf } from '@/cmd/pdf/export-pdf' -import { openCnbHome } from '@/cmd/open/open-cnb-home' -import { openCnbNews } from '@/cmd/open/open-cnb-news' -import { openCnbQ } from '@/cmd/open/open-cnb-q' -import { openCnbIng } from '@/cmd/open/open-cnb-ing' import { editExportPost } from '@/cmd/blog-export/edit' import { createBlogExport } from '@/cmd/blog-export/create' import { downloadBlogExport } from '@/cmd/blog-export/download' @@ -48,6 +23,21 @@ import { AccountManagerNg } from '@/auth/account-manager' import { uploadFsImage } from '@/cmd/upload-img/upload-fs-img' import { uploadClipboardImg } from '@/cmd/upload-img/upload-clipboard-img' import { insertImgLinkToActiveEditor } from '@/cmd/upload-img/upload-img-util' +import { Workspace } from '@/cmd/workspace' +import { Browser } from '@/cmd/browser' +import { Ing } from '@/cmd/ing/ing-page-list' +import { PostListView } from '@/cmd/post-list/post-list-view' +import { postPull } from '@/cmd/post-list/post-pull' +import { postPullAll } from '@/cmd/post-list/post-pull-all' +import { delPostToLocalFileMap } from '@/cmd/post-list/del-post-to-local-file-map' +import { CopyPostLinkCmdHandler } from '@/cmd/post-list/copy-link' +import { createLocalDraft } from '@/cmd/post-list/create-local-draft' +import { modifyPostSetting } from '@/cmd/post-list/modify-post-setting' +import { renamePost } from '@/cmd/post-list/rename-post' +import { openPostInVscode } from '@/cmd/post-list/open-post-in-vscode' +import { delSelectedPost } from '@/cmd/post-list/del-post' +import { pubIngWithInput } from '@/cmd/ing/pub-ing-with-input' +import { pubIngWithSelect } from '@/cmd/ing/pub-ing-with-select' function withPrefix(prefix: string) { return (rest: string) => `${prefix}${rest}` @@ -59,82 +49,84 @@ export function setupExtCmd() { const tokens = [ // auth - regCmd(withAppName('.webLogin'), AccountManagerNg.webLogin), - regCmd(withAppName('.patLogin'), AccountManagerNg.patLogin), + regCmd(withAppName('.login.web'), AccountManagerNg.webLogin), + regCmd(withAppName('.login.pat'), AccountManagerNg.patLogin), regCmd(withAppName('.logout'), AccountManagerNg.logout), - // post-list - regCmd(withAppName('.refresh-post-list'), refreshPostList), - regCmd(withAppName('.post-list.prev'), goPrevPostList), - regCmd(withAppName('.post-list.next'), goNextPostList), - regCmd(withAppName('.post-list.seek'), seekPostList), + // post.list-view + regCmd(withAppName('.post.list-view.refresh'), PostListView.refresh), + regCmd(withAppName('.post.list-view.prev'), PostListView.goPrev), + regCmd(withAppName('.post.list-view.next'), PostListView.goNext), + regCmd(withAppName('.post.list-view.seek'), PostListView.seek), + + regCmd(withAppName('.post.list-view.search.clear'), PostListView.Search.clear), + regCmd(withAppName('.post.list-view.search.refresh'), PostListView.Search.refresh), // post - regCmd(withAppName('.del-post'), delSelectedPost), - regCmd(withAppName('.edit-post'), openPostInVscode), - regCmd(withAppName('.search-post'), searchPost), - regCmd(withAppName('.rename-post'), renamePost), - regCmd(withAppName('.modify-post-setting'), modifyPostSetting), - regCmd(withAppName('.create-local-draft'), createLocalDraft), - regCmd(withAppName('.upload-post'), uploadPost), - regCmd(withAppName('.upload-post-file'), uploadPostFile), - regCmd(withAppName('.upload-post-no-confirm'), uploadPostNoConfirm), - regCmd(withAppName('.upload-post-file-no-confirm'), uploadPostFileNoConfirm), - regCmd(withAppName('.pull-remote-post'), pullRemotePost), - regCmd(withAppName('.open-post-in-blog-admin'), openPostInBlogAdmin), - regCmd(withAppName('.del-post-to-local-file-map'), delPostToLocalFileMap), - regCmd(withAppName('.view-post-online'), viewPostOnline), - regCmd(withAppName('.export-post-to-pdf'), exportPostToPdf), - regCmd(withAppName('.clear-post-search-results'), clearPostSearchResults), - regCmd(withAppName('.refresh-post-search-results'), refreshPostSearchResults), - regCmd(withAppName('.copy-post-link'), input => new CopyPostLinkCmdHandler(input).handle()), - regCmd(withAppName('.reveal-local-post-file-in-os'), osOpenLocalPostFile), - regCmd(withAppName('.show-post-to-local-file-info'), showLocalFileToPostInfo), + regCmd(withAppName('.post.del'), delSelectedPost), + regCmd(withAppName('.post.edit'), openPostInVscode), + regCmd(withAppName('.post.search'), PostListView.Search.search), + regCmd(withAppName('.post.rename'), renamePost), + regCmd(withAppName('.post.modify-setting'), modifyPostSetting), + regCmd(withAppName('.post.create-local-draft'), createLocalDraft), + regCmd(withAppName('.post.upload'), uploadPost), + regCmd(withAppName('.post.upload-file'), uploadPostFile), + regCmd(withAppName('.post.upload-no-confirm'), uploadPostNoConfirm), + regCmd(withAppName('.post.upload-file-no-confirm'), uploadPostFileNoConfirm), + regCmd(withAppName('.post.pull'), postPull), + regCmd(withAppName('.post.pull-all'), postPullAll), + regCmd(withAppName('.post.open-in-blog-admin'), openPostInBlogAdmin), + regCmd(withAppName('.post.del-local-map'), delPostToLocalFileMap), + regCmd(withAppName('.post.view-in-browser'), viewPostOnline), + regCmd(withAppName('.post.export-to-pdf'), exportPostToPdf), + regCmd(withAppName('.post.copy-link'), input => new CopyPostLinkCmdHandler(input).handle()), + regCmd(withAppName('.post.os-open-local-file'), osOpenLocalPostFile), + regCmd(withAppName('.post.show-local-file-info'), showLocalFileToPostInfo), // img - regCmd(withAppName('.extract-img'), extractImg), - regCmd(withAppName('.upload-img'), uploadImg), - regCmd(withAppName('.upload-img-fs'), async () => { + regCmd(withAppName('.img.extract'), extractImg), + regCmd(withAppName('.img.upload'), uploadImg), + regCmd(withAppName('.img.upload-fs'), async () => { const link = await uploadFsImage() if (link !== undefined) await insertImgLinkToActiveEditor(link) }), - regCmd(withAppName('.upload-img-clipboard'), async () => { + regCmd(withAppName('.img.upload-clipboard'), async () => { const link = await uploadClipboardImg() if (link !== undefined) await insertImgLinkToActiveEditor(link) }), // post category - regCmd(withAppName('.new-post-category'), newPostCategory), - regCmd(withAppName('.del-selected-post-category'), handleDeletePostCategories), - regCmd(withAppName('.refresh-post-category-list'), refreshPostCategoryList), - regCmd(withAppName('.update-post-category'), handleUpdatePostCategory), + regCmd(withAppName('.post-category.new'), newPostCategory), + regCmd(withAppName('.post-category.del-select'), handleDeletePostCategories), + regCmd(withAppName('.post-category.refresh'), refreshPostCategoryList), + regCmd(withAppName('.post-category.update'), handleUpdatePostCategory), // workspace - regCmd(withAppName('.open-workspace'), openWorkspace), - regCmd(withAppName('.set-workspace'), setWorkspace), - regCmd(withAppName('.reveal-workspace-in-os'), osOpenWorkspace), + regCmd(withAppName('.workspace.set'), Workspace.set), + regCmd(withAppName('.workspace.os-open'), Workspace.osOpen), + regCmd(withAppName('.workspace.code-open'), Workspace.codeOpen), // ing - regCmd(withAppName('.ing.publish'), () => new PublishIngCmdHandler('input').handle()), - regCmd(withAppName('.ing.publish-select'), () => new PublishIngCmdHandler('select').handle()), + regCmd(withAppName('.ing.pub'), () => pubIngWithInput('')), + regCmd(withAppName('.ing.pub-select'), pubIngWithSelect), // open in browser - regCmd(withAppName('.open-cnb-home'), openCnbHome), - regCmd(withAppName('.open-cnb-news'), openCnbNews), - regCmd(withAppName('.open-cnb-q'), openCnbQ), - regCmd(withAppName('.open-cnb-ing'), openCnbIng), - regCmd(withAppName('.open-my-blog'), openMyBlog), - regCmd(withAppName('.open-my-home-page'), openMyHomePage), - regCmd(withAppName('.open-my-blog-console'), openMyWebBlogConsole), - regCmd(withAppName('.open-my-account-setting'), openMyAccountSetting), + regCmd(withAppName('.open.cnb-q'), Browser.Open.Cnb.q), + regCmd(withAppName('.open.cnb-ing'), Browser.Open.Cnb.ing), + regCmd(withAppName('.open.cnb-home'), Browser.Open.Cnb.home), + regCmd(withAppName('.open.cnb-news'), Browser.Open.Cnb.news), + regCmd(withAppName('.open.my-blog'), Browser.Open.User.blog), + regCmd(withAppName('.open.my-home'), Browser.Open.User.home), + regCmd(withAppName('.open.blog-console'), Browser.Open.User.blogConsole), + regCmd(withAppName('.open.account-setting'), Browser.Open.User.accountSetting), // ing list - regCmd(withAppName('.ing-list.refresh'), refreshIngList), - regCmd(withAppName('.ing-list.next'), goIngListNextPage), - regCmd(withAppName('.ing-list.prev'), goIngListPrevPage), - regCmd(withAppName('.ing-list.first'), goIngList1stPage), - regCmd(withAppName('.ing-list.switch-type'), switchIngType), - regCmd(withAppName('.ing-list.open-in-browser'), openCnbIng), + regCmd(withAppName('.ing-list.next'), Ing.ListView.goNext), + regCmd(withAppName('.ing-list.prev'), Ing.ListView.goPrev), + regCmd(withAppName('.ing-list.first'), Ing.ListView.goFirst), + regCmd(withAppName('.ing-list.refresh'), Ing.ListView.refresh), + regCmd(withAppName('.ing-list.switch-type'), Ing.ListView.switchType), + regCmd(withAppName('.ing-list.open-in-browser'), Browser.Open.Cnb.ing), // blog export - regCmd(withAppName('.blog-export.refresh-record'), refreshExportRecord), - regCmd(withAppName('.blog-export.open-local-export'), openLocalExport), - regCmd(withAppName('.blog-export.edit'), editExportPost), - regCmd(withAppName('.blog-export.create'), createBlogExport), - regCmd(withAppName('.blog-export.download'), downloadBlogExport), - regCmd(withAppName('.blog-export.view-post'), viewPostBlogExport), - regCmd(withAppName('.blog-export.delete'), deleteBlogExport), + regCmd(withAppName('.backup.refresh-record'), refreshExportRecord), + regCmd(withAppName('.backup.open-local'), openLocalExport), + regCmd(withAppName('.backup.edit'), editExportPost), + regCmd(withAppName('.backup.create'), createBlogExport), + regCmd(withAppName('.backup.download'), downloadBlogExport), + regCmd(withAppName('.backup.view-post'), viewPostBlogExport), + regCmd(withAppName('.backup.delete'), deleteBlogExport), ] ctx.subscriptions.push(...tokens) diff --git a/src/setup/setup-watch.ts b/src/setup/setup-watch.ts index 7d6c1036..273cb7f5 100644 --- a/src/setup/setup-watch.ts +++ b/src/setup/setup-watch.ts @@ -2,10 +2,10 @@ import { workspace } from 'vscode' import { isTargetWorkspace } from '@/service/is-target-workspace' import { PostFileMapManager } from '@/service/post/post-file-map' import { refreshPostCategoryList } from '@/cmd/post-category/refresh-post-category-list' -import { refreshPostList } from '@/cmd/post-list/refresh-post-list' import { execCmd } from '@/infra/cmd' import { setupUi } from '@/setup/setup-ui' import { LocalState } from '@/ctx/local-state' +import { PostListView } from '@/cmd/post-list/post-list-view' export const setupCfgWatch = () => workspace.onDidChangeConfiguration(ev => { @@ -14,7 +14,7 @@ export const setupCfgWatch = () => if (ev.affectsConfiguration('workbench.iconTheme')) refreshPostCategoryList() if (ev.affectsConfiguration('cnblogsClient.pageSize.postList')) - refreshPostList({ queue: true }).catch(() => undefined) + PostListView.refresh({ queue: true }).catch(() => undefined) if (ev.affectsConfiguration('cnblogsClient.markdown')) execCmd('markdown.preview.refresh').then(undefined, () => undefined) diff --git a/src/tree-view/convert.ts b/src/tree-view/convert.ts index e21cf900..57b3c879 100644 --- a/src/tree-view/convert.ts +++ b/src/tree-view/convert.ts @@ -40,7 +40,7 @@ const postConverter: Converter = obj => { return Object.assign(new TreeItem(`${obj.title}`, TreeItemCollapsibleState.Collapsed), { tooltip: new MarkdownString(`[${url}](${url})` + descDatePublished + descLocalPath), command: { - command: `${globalCtx.extName}.edit-post`, + command: `${globalCtx.extName}.post.edit`, arguments: [obj.id], title: '编辑博文', }, diff --git a/src/tree-view/model/blog-export/post.ts b/src/tree-view/model/blog-export/post.ts index 328818f9..4078437b 100644 --- a/src/tree-view/model/blog-export/post.ts +++ b/src/tree-view/model/blog-export/post.ts @@ -27,7 +27,7 @@ export class ExportPostTreeItem extends BaseTreeItemSource { collapsibleState: TreeItemCollapsibleState.None, command: { title: '查看博文', - command: `${globalCtx.extName}.blog-export.view-post`, + command: `${globalCtx.extName}.backup.view-post`, arguments: [this], }, resourceUri: Uri.joinPath(WorkspaceCfg.getWorkspaceUri(), title + (isMarkdown ? '.md' : '.html')), diff --git a/src/tree-view/navi-view.ts b/src/tree-view/navi-view.ts index df57616e..8e897a3f 100644 --- a/src/tree-view/navi-view.ts +++ b/src/tree-view/navi-view.ts @@ -12,7 +12,7 @@ export class NaviViewDataProvider implements TreeDataProvider { label: '首页', command: { title: '打开博客园首页', - command: 'vscode-cnb.open-cnb-home', + command: 'vscode-cnb.open.cnb-home', }, iconPath: new ThemeIcon('home'), }, @@ -20,7 +20,7 @@ export class NaviViewDataProvider implements TreeDataProvider { label: '新闻', command: { title: '打开博客园新闻', - command: 'vscode-cnb.open-cnb-news', + command: 'vscode-cnb.open.cnb-news', }, iconPath: new ThemeIcon('preview'), }, @@ -28,7 +28,7 @@ export class NaviViewDataProvider implements TreeDataProvider { label: '博问', command: { title: '打开博问', - command: 'vscode-cnb.open-cnb-q', + command: 'vscode-cnb.open.cnb-q', }, iconPath: new ThemeIcon('question'), }, @@ -36,7 +36,7 @@ export class NaviViewDataProvider implements TreeDataProvider { label: '闪存', command: { title: '打开闪存', - command: 'vscode-cnb.open-cnb-ing', + command: 'vscode-cnb.open.cnb-ing', }, iconPath: new ThemeIcon('comment'), }, diff --git a/src/tree-view/provider/account-view-data-provider.ts b/src/tree-view/provider/account-view-data-provider.ts index ea8e605e..2fe7cd75 100644 --- a/src/tree-view/provider/account-view-data-provider.ts +++ b/src/tree-view/provider/account-view-data-provider.ts @@ -15,14 +15,14 @@ export class AccountViewDataProvider implements TreeDataProvider { getChildren(element?: TreeItem): ProviderResult { if (!accountManager.isAuthorized || element) return [] - const u = accountManager.currentUser + const userName = accountManager.currentUser?.userInfo.DisplayName return [ - { label: u.name, tooltip: '用户名', iconPath: new ThemeIcon('account') }, + { label: userName, tooltip: '用户名', iconPath: new ThemeIcon('account') }, { label: '账户设置', command: { title: '打开账户设置', - command: 'vscode-cnb.open-my-account-setting', + command: 'vscode-cnb.open.account-setting', }, iconPath: new ThemeIcon('gear'), }, @@ -30,7 +30,7 @@ export class AccountViewDataProvider implements TreeDataProvider { label: '博客后台', command: { title: '打开博客后台', - command: 'vscode-cnb.open-my-blog-console', + command: 'vscode-cnb.open.blog-console', }, iconPath: new ThemeIcon('console'), }, @@ -38,7 +38,7 @@ export class AccountViewDataProvider implements TreeDataProvider { label: '我的博客', command: { title: '打开我的博客', - command: 'vscode-cnb.open-my-blog', + command: 'vscode-cnb.open.my-blog', }, iconPath: new ThemeIcon('window'), }, @@ -46,7 +46,7 @@ export class AccountViewDataProvider implements TreeDataProvider { label: '我的主页', command: { title: '打开我的主页', - command: 'vscode-cnb.open-my-home-page', + command: 'vscode-cnb.open.my-home', }, iconPath: new ThemeIcon('home'), }, diff --git a/src/tree-view/provider/post-data-provider.ts b/src/tree-view/provider/post-data-provider.ts index 38609d7e..6dc60f32 100644 --- a/src/tree-view/provider/post-data-provider.ts +++ b/src/tree-view/provider/post-data-provider.ts @@ -1,5 +1,4 @@ import { EventEmitter, ProviderResult, TreeDataProvider, TreeItem } from 'vscode' -import { refreshPostList } from '@/cmd/post-list/refresh-post-list' import { Post } from '@/model/post' import { Alert } from '@/infra/alert' import { PostService } from '@/service/post/post' @@ -9,6 +8,7 @@ import { PostSearchResultEntry } from '@/tree-view/model/post-search-result-entr import { PostTreeItem } from '@/tree-view/model/post-tree-item' import { PostListCfg } from '@/ctx/cfg/post-list' import { Page } from '@/model/page' +import { PostListView } from '@/cmd/post-list/post-list-view' export type PostListTreeItem = Post | PostTreeItem | TreeItem | PostMetadata | PostSearchResultEntry @@ -30,7 +30,7 @@ export class PostDataProvider implements TreeDataProvider { const items: PostListTreeItem[] = this._searchResultEntry == null ? [] : [this._searchResultEntry] if (this.page == null) { - void refreshPostList() + void PostListView.refresh() return items } diff --git a/ui/ing/App.tsx b/ui/ing/App.tsx index d4c74cdb..4eca81ea 100644 --- a/ui/ing/App.tsx +++ b/ui/ing/App.tsx @@ -1,5 +1,5 @@ import React, { Component, ReactNode } from 'react' -import { IngWebviewUiCmd, WebviewCmd } from '@/model/webview-cmd' +import { IngWebviewUiCmd, Webview } from '@/model/webview-cmd' import { IngList } from 'ing/IngList' import { getVsCodeApiSingleton } from 'share/vscode-api' import { IngAppState } from '@/model/ing-view' @@ -48,7 +48,7 @@ export class App extends Component { private observeMessages() { window.addEventListener('message', ({ data: { command, payload } }: { data: IngWebviewUiCmd }) => { - if (command === WebviewCmd.IngCmd.UiCmd.setAppState) { + if (command === Webview.Cmd.Ing.Ui.setAppState) { const { ingList, isRefreshing, comments } = payload as Partial this.setState({ ingList: ingList?.map(Ing.parse) ?? this.state.ingList, @@ -66,7 +66,7 @@ export class App extends Component { }) return } - if (command === WebviewCmd.IngCmd.UiCmd.updateTheme) { + if (command === Webview.Cmd.Ing.Ui.updateTheme) { this.setState({ theme: activeThemeProvider.activeTheme() }) return } @@ -75,7 +75,7 @@ export class App extends Component { private refresh() { getVsCodeApiSingleton().postMessage({ - command: WebviewCmd.IngCmd.ExtCmd.refreshingList, + command: Webview.Cmd.Ing.Ext.refreshingList, payload: {}, }) } diff --git a/ui/ing/IngItem.tsx b/ui/ing/IngItem.tsx index 422755b2..59c51f7d 100644 --- a/ui/ing/IngItem.tsx +++ b/ui/ing/IngItem.tsx @@ -6,7 +6,7 @@ import { ActivityItem, IPersonaProps, Link, Text } from '@fluentui/react' import { format, formatDistanceStrict } from 'date-fns' import { zhCN } from 'date-fns/locale' import { getVsCodeApiSingleton } from 'share/vscode-api' -import { IngWebviewHostCmd, WebviewCmd } from '@/model/webview-cmd' +import { IngWebviewHostCmd, Webview } from '@/model/webview-cmd' interface IngItemProps { ing: Ing @@ -197,11 +197,11 @@ class IngItem extends Component { } } - private comment(payload: WebviewCmd.IngCmd.CommentCmdPayload) { + private comment(payload: Webview.Cmd.Ing.CommentCmdPayload) { getVsCodeApiSingleton().postMessage({ - command: WebviewCmd.IngCmd.ExtCmd.comment, + command: Webview.Cmd.Ing.Ext.comment, payload, - } as IngWebviewHostCmd) + } as IngWebviewHostCmd) } } diff --git a/ui/post-cfg/App.tsx b/ui/post-cfg/App.tsx index cf56f9e5..245db6e2 100644 --- a/ui/post-cfg/App.tsx +++ b/ui/post-cfg/App.tsx @@ -7,7 +7,7 @@ import { personalCategoriesStore } from './service/personal-category-store' import { siteCategoriesStore } from './service/site-category-store' import { tagsStore } from './service/tags-store' import { WebviewMsg } from '@/model/webview-msg' -import { WebviewCmd } from '@/model/webview-cmd' +import { Webview } from '@/model/webview-cmd' import { PostFormContextProvider } from './components/PostFormContextProvider' import { activeThemeProvider } from 'share/active-theme-provider' import { darkTheme, lightTheme } from 'share/theme' @@ -28,7 +28,7 @@ class App extends Component { super(props) this.state = { theme: activeThemeProvider.activeTheme(), fileName: '', useNestCategoriesSelect: false } this.observerMessages() - getVsCodeApiSingleton().postMessage({ command: WebviewCmd.ExtCmd.refreshPost }) + getVsCodeApiSingleton().postMessage({ command: Webview.Cmd.Ext.refreshPost }) } render() { @@ -87,7 +87,7 @@ class App extends Component { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const message = ev.data as any - if (command === WebviewCmd.UiCmd.editPostCfg) { + if (command === Webview.Cmd.Ui.editPostCfg) { const { post, activeTheme, personalCategories, siteCategories, tags, breadcrumbs, fileName } = message as WebviewMsg.EditPostCfgMsg personalCategoriesStore.set(personalCategories) @@ -101,13 +101,13 @@ class App extends Component { fileName, useNestCategoriesSelect: personalCategories.some(c => c.childCount > 0), }) - } else if (command === WebviewCmd.UiCmd.updateBreadcrumbs) { + } else if (command === Webview.Cmd.Ui.updateBreadcrumbs) { const { breadcrumbs } = message as WebviewMsg.UpdateBreadcrumbMsg this.setState({ breadcrumbs }) - } else if (command === WebviewCmd.UiCmd.setFluentIconBaseUrl) { + } else if (command === Webview.Cmd.Ui.setFluentIconBaseUrl) { const { baseUrl } = message as WebviewMsg.SetFluentIconBaseUrlMsg initializeIcons(baseUrl) - } else if (command === WebviewCmd.UiCmd.updateTheme) { + } else if (command === Webview.Cmd.Ui.updateTheme) { this.setState({ theme: activeThemeProvider.activeTheme() }) } }) diff --git a/ui/post-cfg/components/ErrorResponse.tsx b/ui/post-cfg/components/ErrorResponse.tsx index 3e327e9c..c6b74efa 100644 --- a/ui/post-cfg/components/ErrorResponse.tsx +++ b/ui/post-cfg/components/ErrorResponse.tsx @@ -1,5 +1,5 @@ import { MessageBar, MessageBarType } from '@fluentui/react' -import { WebviewCmd } from '@/model/webview-cmd' +import { Webview } from '@/model/webview-cmd' import { WebviewMsg } from '@/model/webview-msg' import React from 'react' import { Optional } from 'utility-types' @@ -23,7 +23,7 @@ export class ErrorResponse extends React.Component { const { command, errorResponse } = (msg.data ?? {}) as any as Optional - if (command === WebviewCmd.UiCmd.showErrorResponse) { + if (command === Webview.Cmd.Ui.showErrorResponse) { this.setState({ errors: errorResponse.errors ?? [] }, () => this.reveal()) this.context.set({ disabled: false, status: '' }) } diff --git a/ui/post-cfg/components/InputSummary.tsx b/ui/post-cfg/components/InputSummary.tsx index 434d7a43..fbf87110 100644 --- a/ui/post-cfg/components/InputSummary.tsx +++ b/ui/post-cfg/components/InputSummary.tsx @@ -1,6 +1,6 @@ import { ActionButton, Label, MessageBar, MessageBarType, Stack, TextField, Text } from '@fluentui/react' import { ImgUploadStatusId } from '@/model/img-upload-status' -import { WebviewCmd } from '@/model/webview-cmd' +import { Webview } from '@/model/webview-cmd' import { WebviewMsg } from '@/model/webview-msg' import React from 'react' import { getVsCodeApiSingleton } from 'share/vscode-api' @@ -105,7 +105,7 @@ export class InputSummary extends React.Component { private onConfirm() { this.context.set({ disabled: true, status: 'submitting' }) getVsCodeApiSingleton().postMessage({ - command: WebviewCmd.ExtCmd.uploadPost, + command: Webview.Cmd.Ext.uploadPost, post: Object.assign({}, this.props.post, this.state), } as WebviewMsg.UploadPostMsg) } private onCancel() { - getVsCodeApiSingleton().postMessage({ command: WebviewCmd.ExtCmd.disposePanel } as WebviewMsg.Msg) + getVsCodeApiSingleton().postMessage({ command: Webview.Cmd.Ext.disposePanel } as WebviewMsg.Msg) } } diff --git a/ui/post-cfg/service/personal-category-store.ts b/ui/post-cfg/service/personal-category-store.ts index 4dcd0a28..f45f8986 100644 --- a/ui/post-cfg/service/personal-category-store.ts +++ b/ui/post-cfg/service/personal-category-store.ts @@ -1,4 +1,4 @@ -import { WebviewCommonCmd, WebviewCmd } from '@/model/webview-cmd' +import { WebviewCommonCmd, Webview } from '@/model/webview-cmd' import { getVsCodeApiSingleton } from 'share/vscode-api' import { PostCategory } from '@/model/post-category' @@ -28,7 +28,7 @@ export namespace personalCategoriesStore { const onUpdate = ({ data: message, }: { - data: WebviewCommonCmd + data: WebviewCommonCmd }) => { console.log('onUpdate', message) if (message.payload.parentId === parent) { @@ -40,14 +40,14 @@ export namespace personalCategoriesStore { } } - window.addEventListener>( + window.addEventListener>( 'message', onUpdate ) }).finally(() => pendingChildrenQuery?.delete(parent)) - vscode.postMessage>({ - command: WebviewCmd.ExtCmd.getChildCategories, + vscode.postMessage>({ + command: Webview.Cmd.Ext.getChildCategories, payload: { parentId: parent }, }) }