From 33d78369b9516dbc00cb9a860ab1e1725b0cb595 Mon Sep 17 00:00:00 2001 From: Thaumy Date: Wed, 16 Aug 2023 13:50:14 +0800 Subject: [PATCH] fix: img extract (#178) --- package.json | 112 ++++++------- rs/src/base64.rs | 6 +- rs/src/cnb/api_base.rs | 32 ++++ rs/src/cnb/ing/comment.rs | 24 +-- rs/src/cnb/ing/get_comment.rs | 22 +-- rs/src/cnb/ing/get_list.rs | 22 +-- rs/src/cnb/ing/mod.rs | 2 - rs/src/cnb/ing/pub.rs | 21 +-- rs/src/cnb/mod.rs | 3 + rs/src/cnb/oauth/get_token.rs | 21 +-- rs/src/cnb/oauth/mod.rs | 2 - rs/src/cnb/oauth/revoke_token.rs | 22 +-- rs/src/cnb/post/del_one.rs | 29 ++++ rs/src/cnb/post/del_some.rs | 32 ++++ rs/src/cnb/post/get_count.rs | 43 +++++ rs/src/cnb/post/get_list.rs | 40 +++++ rs/src/cnb/post/get_one.rs | 29 ++++ rs/src/cnb/post/mod.rs | 28 ++++ rs/src/cnb/post/update.rs | 32 ++++ rs/src/cnb/post_category/create.rs | 33 ++++ rs/src/cnb/post_category/del.rs | 30 ++++ rs/src/cnb/post_category/get_all.rs | 29 ++++ .../post_category/get_cnb_category_list.rs | 29 ++++ rs/src/cnb/post_category/get_one.rs | 30 ++++ rs/src/cnb/post_category/mod.rs | 28 ++++ rs/src/cnb/post_category/update.rs | 34 ++++ rs/src/cnb/user/get_info.rs | 18 +- rs/src/cnb/user/mod.rs | 2 +- rs/src/http/del.rs | 4 +- rs/src/http/get.rs | 4 +- rs/src/http/mod.rs | 23 ++- rs/src/http/post.rs | 4 +- rs/src/http/put.rs | 4 +- rs/src/infra/http.rs | 8 +- rs/src/infra/log.rs | 17 ++ rs/src/infra/mod.rs | 1 + rs/src/infra/result.rs | 37 ++++- rs/src/lib.rs | 1 + rs/src/regex.rs | 13 +- rs/src/text.rs | 24 +++ .../{account-manager.ts => auth-manager.ts} | 89 ++++------ src/auth/auth-provider.ts | 41 ++--- src/auth/oauth.ts | 7 +- src/cmd/blog-export/create.ts | 27 +-- src/cmd/blog-export/delete.ts | 4 +- src/cmd/browser.ts | 6 +- src/cmd/cmd-handler.ts | 4 +- src/cmd/extract-img.ts | 122 -------------- src/cmd/extract-img/convert-img-info.ts | 96 +++++++++++ src/cmd/extract-img/extract-img.ts | 99 +++++++++++ .../extract-img}/find-img-link.ts | 16 +- src/cmd/ing/comment-ing.ts | 4 +- src/cmd/ing/pub-ing.ts | 15 +- src/cmd/pdf/export-pdf.ts | 8 +- src/cmd/pdf/post-pdf-template-builder.ts | 6 +- .../post-category/del-selected-category.ts | 2 +- src/cmd/post-category/new-post-category.ts | 58 +++---- src/cmd/post-category/update-post-category.ts | 22 +-- src/cmd/post-list/copy-link.ts | 2 +- .../post-list/del-post-to-local-file-map.ts | 17 +- src/cmd/post-list/del-post.ts | 12 +- src/cmd/post-list/modify-post-setting.ts | 8 +- src/cmd/post-list/open-post-in-vscode.ts | 4 +- src/cmd/post-list/post-list-view.ts | 15 +- src/cmd/post-list/post-pull-all.ts | 4 +- src/cmd/post-list/post-pull.ts | 2 +- src/cmd/post-list/rename-post.ts | 6 +- src/cmd/post-list/upload-post.ts | 14 +- src/cmd/show-local-file-to-post-info.ts | 4 +- src/cmd/view-post-online.ts | 4 +- src/ctx/app-const.ts | 17 ++ src/ctx/cfg/markdown.ts | 2 +- src/ctx/global-ctx.ts | 36 ++-- src/ctx/local-state.ts | 25 +++ src/extension.ts | 7 +- src/infra/alert.ts | 26 +-- src/infra/convert/ing-star-to-text.ts | 2 +- src/infra/http-client.ts | 4 +- src/infra/http/authed-req.ts | 4 +- src/infra/input-post-setting.ts | 6 +- src/infra/typed-keys.ts | 3 - src/infra/untildify.test.ts | 11 -- src/infra/untildify.ts | 7 - src/markdown/mkd-img-extractor.ts | 157 ------------------ src/model/blog-export.ts | 6 +- src/model/blog-post.ts | 35 ++-- src/model/blog-setting.ts | 4 +- src/model/clipboard-img.ts | 2 +- src/model/config.ts | 53 ------ src/model/ing-view.ts | 4 +- src/model/my-config.ts | 72 -------- src/model/webview-cmd.ts | 4 +- src/model/webview-msg.ts | 2 +- .../blog-export/blog-export-post.store.ts | 2 +- src/service/blog-export/blog-export.ts | 4 +- src/service/blog-setting.ts | 4 +- src/service/downloaded-export.store.ts | 12 +- src/service/img.ts | 4 +- src/service/ing/ing-list-webview-provider.ts | 8 +- src/service/ing/{ing-api.ts => ing.ts} | 6 +- src/service/multi-step-input.ts | 2 +- src/service/post/post-category.ts | 123 +++++++------- src/service/post/post-cfg-panel.ts | 41 +++-- src/service/post/post-file-map.ts | 6 +- src/service/post/post-tag.ts | 4 +- src/service/post/post-title-sanitizer.ts | 10 +- src/service/post/post.ts | 91 +++++----- src/service/site-category.ts | 24 --- src/setup/setup-cmd.ts | 10 +- src/tree-view/model/base-entry-tree-item.ts | 2 +- .../model/post-category-tree-item.ts | 5 +- src/tree-view/model/post-metadata.ts | 8 +- .../provider/account-view-data-provider.ts | 6 +- .../post-category-tree-data-provider.ts | 60 ++++--- src/tree-view/provider/post-data-provider.ts | 18 +- ui/global.d.ts | 9 +- ui/ing/App.tsx | 11 +- ui/ing/IngItem.tsx | 12 +- ui/ing/IngList.tsx | 6 +- ui/ing/index.html | 1 + ui/post-cfg/App.tsx | 28 ++-- .../components/AccessPermissionSelector.tsx | 40 ++--- ui/post-cfg/components/CategorySelect.tsx | 22 ++- ui/post-cfg/components/CommonOptions.tsx | 4 +- ui/post-cfg/components/ErrorResponse.tsx | 8 +- ui/post-cfg/components/InputSummary.tsx | 30 ++-- ui/post-cfg/components/NestCategorySelect.tsx | 13 +- ui/post-cfg/components/PasswordInput.tsx | 2 +- ui/post-cfg/components/PostEntryNameInput.tsx | 7 +- ui/post-cfg/components/PostForm.tsx | 3 +- ui/post-cfg/components/PostFormContext.ts | 2 +- .../components/PostFormContextProvider.tsx | 4 +- ui/post-cfg/components/PostTitleInput.tsx | 4 +- .../components/SiteCategorySelector.tsx | 11 +- .../SiteHomeContributionOptionsSelector.tsx | 2 +- ui/post-cfg/components/TagsInput.tsx | 10 +- .../model/site-home-contribution-options.ts | 4 +- .../service/personal-category-store.ts | 21 ++- ui/post-cfg/service/site-category-store.ts | 12 +- ui/post-cfg/service/tag-store.ts | 11 ++ ui/post-cfg/service/tags-store.ts | 8 - ui/share/active-theme-provider.ts | 7 +- ui/share/theme.ts | 6 +- 143 files changed, 1539 insertions(+), 1311 deletions(-) create mode 100644 rs/src/cnb/api_base.rs create mode 100644 rs/src/cnb/post/del_one.rs create mode 100644 rs/src/cnb/post/del_some.rs create mode 100644 rs/src/cnb/post/get_count.rs create mode 100644 rs/src/cnb/post/get_list.rs create mode 100644 rs/src/cnb/post/get_one.rs create mode 100644 rs/src/cnb/post/mod.rs create mode 100644 rs/src/cnb/post/update.rs create mode 100644 rs/src/cnb/post_category/create.rs create mode 100644 rs/src/cnb/post_category/del.rs create mode 100644 rs/src/cnb/post_category/get_all.rs create mode 100644 rs/src/cnb/post_category/get_cnb_category_list.rs create mode 100644 rs/src/cnb/post_category/get_one.rs create mode 100644 rs/src/cnb/post_category/mod.rs create mode 100644 rs/src/cnb/post_category/update.rs create mode 100644 rs/src/infra/log.rs create mode 100644 rs/src/text.rs rename src/auth/{account-manager.ts => auth-manager.ts} (56%) delete mode 100644 src/cmd/extract-img.ts create mode 100644 src/cmd/extract-img/convert-img-info.ts create mode 100644 src/cmd/extract-img/extract-img.ts rename src/{infra/filter => cmd/extract-img}/find-img-link.ts (80%) create mode 100644 src/ctx/app-const.ts delete mode 100644 src/infra/typed-keys.ts delete mode 100644 src/infra/untildify.test.ts delete mode 100644 src/infra/untildify.ts delete mode 100644 src/markdown/mkd-img-extractor.ts delete mode 100644 src/model/config.ts rename src/service/ing/{ing-api.ts => ing.ts} (93%) delete mode 100644 src/service/site-category.ts create mode 100644 ui/post-cfg/service/tag-store.ts delete mode 100644 ui/post-cfg/service/tags-store.ts diff --git a/package.json b/package.json index f424df6c..df56de01 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "compile-tests": "tsc -p ./tsconfig.integration.json --outDir out", "test:integration:watch": "npm run compile-tests -- --watch", "test:integration": "npm run compile-tests && node ./out/test/runTest.js", - "test:unit": "jest -c ./test/jest.config.mjs", + "test:unit": "jest -c ./test/jest.config.mjs --passWithNoTests", "lint": "eslint src/ ui/", "format": "prettier --write .", "format-check": "prettier --check .", @@ -124,7 +124,7 @@ "commands": [ { "title": "登录到博客园", - "enablement": "!vscode-cnb.isAuthorized", + "enablement": "!vscode-cnb.isAuthed", "command": "vscode-cnb.login.web" }, { @@ -132,41 +132,41 @@ "title": "登出", "icon": "$(log-out)", "category": "Cnblogs Account", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.post.list-view.prev", "title": "上一页", "icon": "$(chevron-left)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view.refreshing && vscode-cnb.post.list-view.hasPrev", + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.post.list-view.refreshing && vscode-cnb.post.list-view.hasPrev", "category": "Cnblogs Post List" }, { "command": "vscode-cnb.post.list-view.seek", "title": "跳页", "icon": "$(vscode-cnb-icon-seek)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view.refreshing && vscode-cnb.post.list-view.pageCount > 0", + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.post.list-view.refreshing && vscode-cnb.post.list-view.pageCount > 0", "category": "Cnblogs Post List" }, { "command": "vscode-cnb.post.list-view.next", "title": "下一页", "icon": "$(chevron-right)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view.refreshing && vscode-cnb.post.list-view.hasNext", + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.post.list-view.refreshing && vscode-cnb.post.list-view.hasNext", "category": "Cnblogs Post List" }, { "command": "vscode-cnb.post.list-view.refresh", "title": "刷新", "icon": "$(refresh)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view.refreshing", + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.post.list-view.refreshing", "category": "Cnblogs Post List" }, { "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", + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.post.list-view.refreshing && vscode-cnb.post.list-view.pageCount > 0", "category": "Cnblogs Post List" }, { @@ -174,7 +174,7 @@ "title": "删除随笔(支持多选)", "icon": "$(trash)", "category": "Cnblogs Post List", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.post.create-local-draft", @@ -187,20 +187,20 @@ "title": "博文设置", "icon": "$(gear)", "category": "Cnblogs Post List", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.post.upload", "title": "上传博文", "icon": "$(cloud-upload)", "category": "Cnblogs Post List", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.post.upload-file", "title": "上传到博客园", "icon": "$(vscode-cnb-cloud-upload)", - "enablement": "vscode-cnb.isAuthorized", + "enablement": "vscode-cnb.isAuthed", "category": "Cnblogs" }, { @@ -208,13 +208,13 @@ "title": "上传博文", "icon": "$(cloud-upload)", "category": "Cnblogs Post List", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.post.upload-file-no-confirm", "title": "上传到博客园", "icon": "$(vscode-cnb-cloud-upload)", - "enablement": "vscode-cnb.isAuthorized", + "enablement": "vscode-cnb.isAuthed", "category": "Cnblogs" }, { @@ -222,19 +222,19 @@ "title": "上传图片到博客园", "category": "Cnblogs", "icon": "$(vscode-cnb-image-upload)", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.img.upload-clipboard", "title": "上传剪贴板图片到博客园", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.img.upload-fs", "title": "上传本地图片到博客园", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.post.os-open-local-file", @@ -245,27 +245,27 @@ "command": "vscode-cnb.post.pull", "title": "拉取博文", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized", + "enablement": "vscode-cnb.isAuthed", "icon": "$(cloud-download)" }, { "command": "vscode-cnb.post.show-local-file-info", "title": "博客园关联博文", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.post-category.new", "title": "新建博文分类", "icon": "$(add)", "category": "Cnblogs Post Categories Management", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.post-category.del-select", "title": "删除", "icon": "$(trash)", "category": "Cnblogs Post Categories Management", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.postCategoriesList.isRefreshing" + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.postCategoriesList.isRefreshing" }, { "command": "vscode-cnb.post-category.refresh", @@ -290,13 +290,13 @@ "command": "vscode-cnb.post.rename", "title": "重命名博文", "category": "Cnblogs Post List", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.post.open-in-blog-admin", "title": "在博客后台中编辑", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.workspace.code-open", @@ -312,142 +312,142 @@ "command": "vscode-cnb.post.export-to-pdf", "title": "导出 PDF", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.img.extract", "title": "提取图片", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.post.search", "title": "搜索博文", "category": "Cnblogs", "icon": "$(vscode-cnb-post-list-search)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view.refreshing" + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.post.list-view.refreshing" }, { "command": "vscode-cnb.post.list-view.search.clear", "title": "清除随笔搜索结果", "category": "Cnblogs", "icon": "$(clear-all)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view.refreshing" + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.post.list-view.refreshing" }, { "command": "vscode-cnb.post.list-view.search.refresh", "title": "刷新随笔搜索结果", "category": "Cnblogs", "icon": "$(refresh)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.post.list-view-refreshing" + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.post.list-view-refreshing" }, { "command": "vscode-cnb.ing.pub", "title": "发闪存", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.ingList.isRefreshing", + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.ingList.isRefreshing", "icon": "$(add)" }, { "command": "vscode-cnb.ing.pub-select", "title": "将选中内容发到闪存", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized && editorHasSelection == true && isInDiffEditor == false && isInEmbeddedEditor == false" + "enablement": "vscode-cnb.isAuthed && editorHasSelection == true && isInDiffEditor == false && isInEmbeddedEditor == false" }, { "command": "vscode-cnb.ing-list.refresh", "title": "刷新闪存列表", "category": "Cnblogs", "icon": "$(refresh)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.ingList.isRefreshing" + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.ingList.isRefreshing" }, { "command": "vscode-cnb.ing-list.prev", "title": "上一页", "category": "Cnblogs", "icon": "$(chevron-left)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.ingList.isRefreshing && vscode-cnb.ingList.pageIndex > 1" + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.ingList.isRefreshing && vscode-cnb.ingList.pageIndex > 1" }, { "command": "vscode-cnb.ing-list.next", "title": "下一页", "category": "Cnblogs", "icon": "$(chevron-right)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.ingList.isRefreshing && vscode-cnb.ingList.pageIndex < 500" + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.ingList.isRefreshing && vscode-cnb.ingList.pageIndex < 500" }, { "command": "vscode-cnb.ing-list.first", "title": "第一页", "category": "Cnblogs", "icon": "$(vscode-cnb-first-page)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.ingList.isRefreshing && vscode-cnb.ingList.pageIndex < 500" + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.ingList.isRefreshing && vscode-cnb.ingList.pageIndex < 500" }, { "command": "vscode-cnb.ing-list.switch-type", "title": "切换闪存类型", "category": "Cnblogs", "icon": "$(list-filter)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.ingList.isRefreshing" + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.ingList.isRefreshing" }, { "command": "vscode-cnb.ing-list.open-in-browser", "title": "在浏览器中打开闪存", "icon": "$(globe)", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.post.copy-link", "title": "复制博文链接", "icon": "$(link)", - "enablement": "vscode-cnb.isAuthorized" + "enablement": "vscode-cnb.isAuthed" }, { "command": "vscode-cnb.backup.refresh-record", "title": "刷新博客备份记录", "category": "Cnblogs", "icon": "$(refresh)", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.backup.records.isRefreshing" + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.backup.records.isRefreshing" }, { "command": "vscode-cnb.backup.open-local", "title": "打开本地已下载博客备份文件", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.backup.records.isRefreshing", + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.backup.records.isRefreshing", "icon": "$(file-add)" }, { "command": "vscode-cnb.backup.edit", "title": "编辑博文", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized", + "enablement": "vscode-cnb.isAuthed", "icon": "$(pencil)" }, { "command": "vscode-cnb.backup.create", "title": "新建博客备份", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.backup.records.isRefreshing", + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.backup.records.isRefreshing", "icon": "$(add)" }, { "command": "vscode-cnb.backup.download", "title": "下载备份", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.backup.records.isRefreshing && !vscode-cnb.backup.downloading", + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.backup.records.isRefreshing && !vscode-cnb.backup.downloading", "icon": "$(cloud-download)" }, { "command": "vscode-cnb.backup.view-post", "title": "查看备份博文", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.backup.records.isRefreshing", + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.backup.records.isRefreshing", "icon": "$(eye)" }, { "command": "vscode-cnb.backup.delete", "title": "delete", "category": "Cnblogs", - "enablement": "vscode-cnb.isAuthorized && !vscode-cnb.backup.records.isRefreshing && !vscode-cnb.backup.downloading", + "enablement": "vscode-cnb.isAuthed && !vscode-cnb.backup.records.isRefreshing && !vscode-cnb.backup.downloading", "icon": "$(trash)" } ], @@ -762,57 +762,57 @@ { "id": "cnblogs-post-list", "name": "随笔列表", - "when": "vscode-cnb.isAuthorized && vscode-cnb.isTargetWorkspace" + "when": "vscode-cnb.isAuthed && vscode-cnb.isTargetWorkspace" } ], "cnblogs": [ { "id": "cnblogs-authorize", "name": "登录/授权", - "when": "!vscode-cnb.isAuthorized", + "when": "!vscode-cnb.isAuthed", "visibility": "visible" }, { "id": "vscode-cnb-workspace", "name": "工作空间", - "when": "vscode-cnb.isAuthorized", + "when": "vscode-cnb.isAuthed", "visibility": "collapsed" }, { "id": "cnblogs-post-list-another", "name": "随笔列表", - "when": "vscode-cnb.isAuthorized", + "when": "vscode-cnb.isAuthed", "visibility": "collapsed" }, { "id": "cnblogs-post-category-list", "name": "分类列表", - "when": "vscode-cnb.isAuthorized", + "when": "vscode-cnb.isAuthed", "visibility": "collapsed" }, { "id": "vscode-cnb.blog-export", "name": "博客备份", - "when": "vscode-cnb.isAuthorized", + "when": "vscode-cnb.isAuthed", "visibility": "collapsed" }, { "id": "cnblogs-account", "name": "账户信息", - "when": "vscode-cnb.isAuthorized", + "when": "vscode-cnb.isAuthed", "visibility": "collapsed" }, { "id": "cnblogs-navi", "name": "博客园导航", - "when": "vscode-cnb.isAuthorized", + "when": "vscode-cnb.isAuthed", "visibility": "collapsed" }, { "id": "vscode-cnb.ing-list-webview", "type": "webview", "name": "闪存", - "when": "vscode-cnb.isAuthorized", + "when": "vscode-cnb.isAuthed", "visibility": "collapsed" } ] @@ -876,7 +876,7 @@ }, { "command": "vscode-cnb.ing-list.first", - "when": "view == vscode-cnb.ing-list-webview && vscode-cnb.isAuthorized && !vscode-cnb.ingList.isRefreshing && vscode-cnb.ingList.pageIndex > 2", + "when": "view == vscode-cnb.ing-list-webview && vscode-cnb.isAuthed && !vscode-cnb.ingList.isRefreshing && vscode-cnb.ingList.pageIndex > 2", "group": "navigation@1" }, { @@ -1179,7 +1179,7 @@ "editor/context": [ { "command": "vscode-cnb.login.web", - "when": "!vscode-cnb.isAuthorized" + "when": "!vscode-cnb.isAuthed" }, { "command": "vscode-cnb.post.show-local-file-info", @@ -1237,7 +1237,7 @@ "explorer/context": [ { "command": "vscode-cnb.login.web", - "when": "!vscode-cnb.isAuthorized" + "when": "!vscode-cnb.isAuthed" }, { "command": "vscode-cnb.post.upload-file", diff --git a/rs/src/base64.rs b/rs/src/base64.rs index 697aedf9..92a3ddeb 100644 --- a/rs/src/base64.rs +++ b/rs/src/base64.rs @@ -1,4 +1,4 @@ -use crate::infra::result::{homo_result_string, HomoResult, IntoResult}; +use crate::infra::result::{HomoResult, IntoResult, ResultExt}; use crate::panic_hook; use alloc::string::String; use anyhow::Result; @@ -20,7 +20,7 @@ impl RsBase64 { panic_hook!(); let text = decode(base64); - homo_result_string(text) + text.homo_string() } #[wasm_bindgen(js_name = encodeUrl)] @@ -33,7 +33,7 @@ impl RsBase64 { panic_hook!(); let text = decode_url(base64url); - homo_result_string(text) + text.homo_string() } } diff --git a/rs/src/cnb/api_base.rs b/rs/src/cnb/api_base.rs new file mode 100644 index 00000000..d14be079 --- /dev/null +++ b/rs/src/cnb/api_base.rs @@ -0,0 +1,32 @@ +pub const BLOG_BACKEND: &str = "https://i.cnblogs.com/api"; +#[macro_export] +macro_rules! blog_backend { + ($($arg:tt)*) => {{ + use $crate::cnb::api_base::BLOG_BACKEND; + use alloc::format; + let rest = format!($($arg)*); + format!("{}{}", BLOG_BACKEND, rest) + }}; +} + +pub const OPENAPI: &str = "https://api.cnblogs.com/api"; +#[macro_export] +macro_rules! openapi { + ($($arg:tt)*) => {{ + use $crate::cnb::api_base::OPENAPI; + use alloc::format; + let rest = format!($($arg)*); + format!("{}{}", OPENAPI, rest) + }}; +} + +pub const OAUTH: &str = "https://oauth.cnblogs.com"; +#[macro_export] +macro_rules! oauth { + ($($arg:tt)*) => {{ + use $crate::cnb::api_base::OAUTH; + use alloc::format; + let rest = format!($($arg)*); + format!("{}{}", OAUTH, rest) + }}; +} diff --git a/rs/src/cnb/ing/comment.rs b/rs/src/cnb/ing/comment.rs index 1b3f47fc..23e3ce2c 100644 --- a/rs/src/cnb/ing/comment.rs +++ b/rs/src/cnb/ing/comment.rs @@ -1,11 +1,11 @@ -use crate::cnb::ing::{IngReq, ING_API_BASE_URL}; +use crate::cnb::ing::IngReq; +use crate::http::unit_or_err; use crate::infra::http::{setup_auth, APPLICATION_JSON}; -use crate::infra::result::IntoResult; -use crate::panic_hook; +use crate::infra::result::ResultExt; +use crate::{openapi, panic_hook}; use alloc::format; -use alloc::string::{String, ToString}; -use anyhow::{anyhow, Result}; -use core::ops::Not; +use alloc::string::String; +use anyhow::Result; use reqwest::header::CONTENT_TYPE; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; @@ -32,7 +32,7 @@ impl IngReq { parent_comment_id: Option, ) -> Result<(), String> { panic_hook!(); - let url = format!("{ING_API_BASE_URL}/{ing_id}/comments"); + let url = openapi!("/statuses/{}/comments", ing_id); let client = reqwest::Client::new().post(url); @@ -47,16 +47,10 @@ impl IngReq { 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()? - } + unit_or_err(resp).await? }; - result.map_err(|e| e.to_string()) + result.err_to_string() } } diff --git a/rs/src/cnb/ing/get_comment.rs b/rs/src/cnb/ing/get_comment.rs index adaa0f8c..d3320be8 100644 --- a/rs/src/cnb/ing/get_comment.rs +++ b/rs/src/cnb/ing/get_comment.rs @@ -1,10 +1,11 @@ -use crate::cnb::ing::{IngReq, ING_API_BASE_URL}; +use crate::cnb::ing::IngReq; +use crate::http::body_or_err; use crate::infra::http::setup_auth; -use crate::infra::result::{homo_result_string, HomoResult, IntoResult}; -use crate::panic_hook; +use crate::infra::result::{HomoResult, ResultExt}; +use crate::{openapi, panic_hook}; use alloc::format; use alloc::string::String; -use anyhow::{anyhow, Result}; +use anyhow::Result; use wasm_bindgen::prelude::*; #[wasm_bindgen(js_class = IngReq)] @@ -12,7 +13,7 @@ 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 url = openapi!("/statuses/{}/comments", ing_id); let client = reqwest::Client::new().get(url); @@ -20,16 +21,9 @@ impl IngReq { 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()? - } + body_or_err(resp).await? }; - homo_result_string(result) + result.homo_string() } } diff --git a/rs/src/cnb/ing/get_list.rs b/rs/src/cnb/ing/get_list.rs index 27e5a70b..8ff057aa 100644 --- a/rs/src/cnb/ing/get_list.rs +++ b/rs/src/cnb/ing/get_list.rs @@ -1,10 +1,11 @@ -use crate::cnb::ing::{IngReq, ING_API_BASE_URL}; +use crate::cnb::ing::IngReq; +use crate::http::body_or_err; use crate::infra::http::setup_auth; -use crate::infra::result::{homo_result_string, HomoResult, IntoResult}; -use crate::panic_hook; +use crate::infra::result::{HomoResult, ResultExt}; +use crate::{openapi, panic_hook}; use alloc::string::String; use alloc::{format, vec}; -use anyhow::{anyhow, Result}; +use anyhow::Result; use wasm_bindgen::prelude::*; #[wasm_bindgen(js_class = IngReq)] @@ -17,7 +18,7 @@ impl IngReq { ing_type: usize, ) -> HomoResult { panic_hook!(); - let url = format!("{ING_API_BASE_URL}/@{ing_type}"); + let url = openapi!("/statuses/@{}", ing_type); let client = reqwest::Client::new().get(url); @@ -26,16 +27,9 @@ impl IngReq { 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()? - } + body_or_err(resp).await? }; - homo_result_string(result) + result.homo_string() } } diff --git a/rs/src/cnb/ing/mod.rs b/rs/src/cnb/ing/mod.rs index aa041b0d..992bf2c2 100644 --- a/rs/src/cnb/ing/mod.rs +++ b/rs/src/cnb/ing/mod.rs @@ -7,8 +7,6 @@ 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, diff --git a/rs/src/cnb/ing/pub.rs b/rs/src/cnb/ing/pub.rs index bd2252fe..7b8852db 100644 --- a/rs/src/cnb/ing/pub.rs +++ b/rs/src/cnb/ing/pub.rs @@ -1,10 +1,10 @@ -use crate::cnb::ing::{IngReq, ING_API_BASE_URL}; +use crate::cnb::ing::IngReq; +use crate::http::unit_or_err; use crate::infra::http::{setup_auth, APPLICATION_JSON}; -use crate::infra::result::IntoResult; -use crate::panic_hook; +use crate::infra::result::ResultExt; +use crate::{openapi, panic_hook}; use alloc::string::{String, ToString}; -use anyhow::{anyhow, Result}; -use core::ops::Not; +use anyhow::Result; use reqwest::header::CONTENT_TYPE; use serde_json::json; use wasm_bindgen::prelude::*; @@ -14,7 +14,7 @@ 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 url = openapi!("/statuses"); let body = json!({ "content": content, @@ -30,14 +30,9 @@ impl IngReq { 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()? - } + unit_or_err(resp).await? }; - result.map_err(|e| e.to_string()) + result.err_to_string() } } diff --git a/rs/src/cnb/mod.rs b/rs/src/cnb/mod.rs index b3b10d9f..5b3d347b 100644 --- a/rs/src/cnb/mod.rs +++ b/rs/src/cnb/mod.rs @@ -1,3 +1,6 @@ +pub mod api_base; pub mod ing; pub mod oauth; +pub mod post; +pub mod post_category; pub mod user; diff --git a/rs/src/cnb/oauth/get_token.rs b/rs/src/cnb/oauth/get_token.rs index 561cade5..02946d91 100644 --- a/rs/src/cnb/oauth/get_token.rs +++ b/rs/src/cnb/oauth/get_token.rs @@ -1,11 +1,11 @@ use crate::cnb::oauth::OauthReq; -use crate::cnb::oauth::OAUTH_API_BASE_URL; +use crate::http::body_or_err; use crate::infra::http::{cons_query_string, APPLICATION_X3WFU}; -use crate::infra::result::{homo_result_string, HomoResult, IntoResult}; -use crate::panic_hook; +use crate::infra::result::{HomoResult, ResultExt}; +use crate::{oauth, panic_hook}; use alloc::string::String; use alloc::{format, vec}; -use anyhow::{anyhow, Result}; +use anyhow::Result; use reqwest::header::CONTENT_TYPE; use wasm_bindgen::prelude::*; @@ -19,7 +19,7 @@ impl OauthReq { callback_url: &str, ) -> HomoResult { panic_hook!(); - let url = format!("{OAUTH_API_BASE_URL}/connect/token"); + let url = oauth!("/connect/token"); let client = reqwest::Client::new().post(url); @@ -38,16 +38,9 @@ impl OauthReq { 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()? - } + body_or_err(resp).await? }; - homo_result_string(result) + result.homo_string() } } diff --git a/rs/src/cnb/oauth/mod.rs b/rs/src/cnb/oauth/mod.rs index a203e436..b6b6bb1a 100644 --- a/rs/src/cnb/oauth/mod.rs +++ b/rs/src/cnb/oauth/mod.rs @@ -5,8 +5,6 @@ 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, diff --git a/rs/src/cnb/oauth/revoke_token.rs b/rs/src/cnb/oauth/revoke_token.rs index ecd8062e..fa948c1b 100644 --- a/rs/src/cnb/oauth/revoke_token.rs +++ b/rs/src/cnb/oauth/revoke_token.rs @@ -1,14 +1,13 @@ use crate::cnb::oauth::OauthReq; -use crate::cnb::oauth::OAUTH_API_BASE_URL; +use crate::http::unit_or_err; 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 crate::infra::result::ResultExt; +use crate::{basic, oauth, panic_hook}; +use alloc::string::String; use alloc::{format, vec}; -use anyhow::{anyhow, Result}; +use anyhow::Result; use base64::engine::general_purpose; use base64::Engine; -use core::ops::Not; use reqwest::header::{AUTHORIZATION, CONTENT_TYPE}; use wasm_bindgen::prelude::*; @@ -19,7 +18,7 @@ impl OauthReq { 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 url = oauth!("/connect/revocation"); let client = reqwest::Client::new().post(url); @@ -36,14 +35,9 @@ impl OauthReq { 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()? - } + unit_or_err(resp).await? }; - result.map_err(|e| e.to_string()) + result.err_to_string() } } diff --git a/rs/src/cnb/post/del_one.rs b/rs/src/cnb/post/del_one.rs new file mode 100644 index 00000000..fdbe339e --- /dev/null +++ b/rs/src/cnb/post/del_one.rs @@ -0,0 +1,29 @@ +use crate::cnb::post::PostReq; +use crate::http::unit_or_err; +use crate::infra::http::setup_auth; +use crate::infra::result::ResultExt; +use crate::{blog_backend, panic_hook}; +use alloc::format; +use alloc::string::String; +use anyhow::Result; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = PostReq)] +impl PostReq { + #[wasm_bindgen(js_name = delOne)] + pub async fn export_del_one(&self, post_id: usize) -> Result<(), String> { + panic_hook!(); + let url = blog_backend!("/posts/{}", post_id); + + let client = reqwest::Client::new().delete(url); + + let req = setup_auth(client, &self.token, self.is_pat_token); + + let result: Result<()> = try { + let resp = req.send().await?; + unit_or_err(resp).await? + }; + + result.err_to_string() + } +} diff --git a/rs/src/cnb/post/del_some.rs b/rs/src/cnb/post/del_some.rs new file mode 100644 index 00000000..90b943a5 --- /dev/null +++ b/rs/src/cnb/post/del_some.rs @@ -0,0 +1,32 @@ +use crate::cnb::post::PostReq; +use crate::http::unit_or_err; +use crate::infra::http::{cons_query_string, setup_auth}; +use crate::infra::result::ResultExt; +use crate::{blog_backend, panic_hook}; +use alloc::format; +use alloc::string::String; +use alloc::vec::Vec; +use anyhow::Result; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = PostReq)] +impl PostReq { + #[wasm_bindgen(js_name = delSome)] + pub async fn export_del_some(&self, post_ids: &[usize]) -> Result<(), String> { + panic_hook!(); + let post_ids: Vec<(&str, &usize)> = post_ids.iter().map(|id| ("postIds", id)).collect(); + let query = cons_query_string(post_ids); + let url = blog_backend!("/bulk-operation/post?{}", query); + + let client = reqwest::Client::new().delete(url); + + let req = setup_auth(client, &self.token, self.is_pat_token); + + let result: Result<()> = try { + let resp = req.send().await?; + unit_or_err(resp).await? + }; + + result.err_to_string() + } +} diff --git a/rs/src/cnb/post/get_count.rs b/rs/src/cnb/post/get_count.rs new file mode 100644 index 00000000..132943a5 --- /dev/null +++ b/rs/src/cnb/post/get_count.rs @@ -0,0 +1,43 @@ +use crate::cnb::post::PostReq; +use crate::infra::http::{cons_query_string, setup_auth}; +use crate::infra::result::{IntoResult, ResultExt}; +use crate::{blog_backend, panic_hook}; +use alloc::string::String; +use alloc::{format, vec}; +use anyhow::{anyhow, Result}; +use serde_json::Value; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = PostReq)] +impl PostReq { + #[wasm_bindgen(js_name = getCount)] + pub async fn export_get_count(&self) -> Result { + panic_hook!(); + let query = vec![('p', 1), ('s', 1)]; + let query = cons_query_string(query); + let url = blog_backend!("/posts/list?{}", query); + + 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(); + let body = resp.text().await?; + + if code.is_success() { + let obj: Value = serde_json::from_str(&body)?; + let val = obj + .get("postsCount") + .and_then(|v| v.as_i64()) + .map(|v| v as usize); + val.expect("Unable to parse resp json") + } else { + anyhow!("{}: {}", code, body).into_err()? + } + }; + + result.err_to_string() + } +} diff --git a/rs/src/cnb/post/get_list.rs b/rs/src/cnb/post/get_list.rs new file mode 100644 index 00000000..aab2305f --- /dev/null +++ b/rs/src/cnb/post/get_list.rs @@ -0,0 +1,40 @@ +use crate::cnb::post::PostReq; +use crate::infra::http::{cons_query_string, setup_auth}; +use crate::infra::result::{HomoResult, IntoResult, ResultExt}; +use crate::{blog_backend, panic_hook}; +use alloc::string::{String, ToString}; +use alloc::{format, vec}; +use anyhow::{anyhow, Result}; +use serde_json::Value; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = PostReq)] +impl PostReq { + #[wasm_bindgen(js_name = getList)] + pub async fn export_get_list(&self, page_index: usize, page_cap: usize) -> HomoResult { + panic_hook!(); + let query = vec![('t', 1), ('p', page_index), ('s', page_cap)]; + let query = cons_query_string(query); + let url = blog_backend!("/posts/list?{}", query); + + 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(); + let body = resp.text().await?; + + if code.is_success() { + let obj: Value = serde_json::from_str(&body)?; + let val = obj.get("postList").map(|v| v.to_string()); + val.expect("Unable to parse resp json") + } else { + anyhow!("{}: {}", code, body).into_err()? + } + }; + + result.homo_string() + } +} diff --git a/rs/src/cnb/post/get_one.rs b/rs/src/cnb/post/get_one.rs new file mode 100644 index 00000000..d3c7a440 --- /dev/null +++ b/rs/src/cnb/post/get_one.rs @@ -0,0 +1,29 @@ +use crate::cnb::post::PostReq; +use crate::http::body_or_err; +use crate::infra::http::setup_auth; +use crate::infra::result::{HomoResult, ResultExt}; +use crate::{blog_backend, panic_hook}; +use alloc::format; +use alloc::string::String; +use anyhow::Result; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = PostReq)] +impl PostReq { + #[wasm_bindgen(js_name = getOne)] + pub async fn export_get_one(&self, post_id: usize) -> HomoResult { + panic_hook!(); + let url = blog_backend!("/posts/{}", post_id); + + 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?; + body_or_err(resp).await? + }; + + result.homo_string() + } +} diff --git a/rs/src/cnb/post/mod.rs b/rs/src/cnb/post/mod.rs new file mode 100644 index 00000000..8ee047f1 --- /dev/null +++ b/rs/src/cnb/post/mod.rs @@ -0,0 +1,28 @@ +mod del_one; +mod del_some; +mod get_count; +mod get_list; +mod get_one; +mod update; + +use crate::panic_hook; +use alloc::string::String; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = PostReq)] +pub struct PostReq { + token: String, + is_pat_token: bool, +} + +#[wasm_bindgen(js_class = PostReq)] +impl PostReq { + #[wasm_bindgen(constructor)] + pub fn new(token: String, is_pat_token: bool) -> PostReq { + panic_hook!(); + PostReq { + token, + is_pat_token, + } + } +} diff --git a/rs/src/cnb/post/update.rs b/rs/src/cnb/post/update.rs new file mode 100644 index 00000000..8ea10591 --- /dev/null +++ b/rs/src/cnb/post/update.rs @@ -0,0 +1,32 @@ +use crate::cnb::post::PostReq; +use crate::http::body_or_err; +use crate::infra::http::{setup_auth, APPLICATION_JSON}; +use crate::infra::result::{HomoResult, ResultExt}; +use crate::{blog_backend, panic_hook}; +use alloc::format; +use alloc::string::String; +use anyhow::Result; +use reqwest::header::CONTENT_TYPE; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = PostReq)] +impl PostReq { + #[wasm_bindgen(js_name = update)] + pub async fn export_update(&self, post_json: String) -> HomoResult { + panic_hook!(); + let url = blog_backend!("/posts"); + + let client = reqwest::Client::new().post(url); + + let req = setup_auth(client, &self.token, self.is_pat_token) + .header(CONTENT_TYPE, APPLICATION_JSON) + .body(post_json); + + let result: Result = try { + let resp = req.send().await?; + body_or_err(resp).await? + }; + + result.homo_string() + } +} diff --git a/rs/src/cnb/post_category/create.rs b/rs/src/cnb/post_category/create.rs new file mode 100644 index 00000000..bbd48c34 --- /dev/null +++ b/rs/src/cnb/post_category/create.rs @@ -0,0 +1,33 @@ +use crate::cnb::post_category::PostCategoryReq; +use crate::http::unit_or_err; +use crate::infra::http::{setup_auth, APPLICATION_JSON}; +use crate::infra::result::ResultExt; +use crate::{blog_backend, panic_hook}; +use alloc::format; +use alloc::string::String; +use anyhow::Result; +use reqwest::header::CONTENT_TYPE; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = PostCategoryReq)] +impl PostCategoryReq { + #[wasm_bindgen(js_name = create)] + pub async fn export_create(&self, category_dto_json: String) -> Result<(), String> { + panic_hook!(); + + let url = blog_backend!("/category/blog/1"); + + let client = reqwest::Client::new().post(url); + + let req = setup_auth(client, &self.token, self.is_pat_token) + .header(CONTENT_TYPE, APPLICATION_JSON) + .body(category_dto_json); + + let result: Result<()> = try { + let resp = req.send().await?; + unit_or_err(resp).await? + }; + + result.err_to_string() + } +} diff --git a/rs/src/cnb/post_category/del.rs b/rs/src/cnb/post_category/del.rs new file mode 100644 index 00000000..d6316b76 --- /dev/null +++ b/rs/src/cnb/post_category/del.rs @@ -0,0 +1,30 @@ +use crate::cnb::post_category::PostCategoryReq; +use crate::http::unit_or_err; +use crate::infra::http::setup_auth; +use crate::infra::result::ResultExt; +use crate::{blog_backend, panic_hook}; +use alloc::format; +use alloc::string::String; +use anyhow::Result; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = PostCategoryReq)] +impl PostCategoryReq { + #[wasm_bindgen(js_name = del)] + pub async fn export_del(&self, category_id: usize) -> Result<(), String> { + panic_hook!(); + + let url = blog_backend!("/category/blog/{}", category_id); + + let client = reqwest::Client::new().delete(url); + + let req = setup_auth(client, &self.token, self.is_pat_token); + + let result: Result<()> = try { + let resp = req.send().await?; + unit_or_err(resp).await? + }; + + result.err_to_string() + } +} diff --git a/rs/src/cnb/post_category/get_all.rs b/rs/src/cnb/post_category/get_all.rs new file mode 100644 index 00000000..645cd1e9 --- /dev/null +++ b/rs/src/cnb/post_category/get_all.rs @@ -0,0 +1,29 @@ +use crate::cnb::post_category::PostCategoryReq; +use crate::http::body_or_err; +use crate::infra::http::setup_auth; +use crate::infra::result::{HomoResult, ResultExt}; +use crate::{blog_backend, panic_hook}; +use alloc::format; +use alloc::string::String; +use anyhow::Result; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = PostCategoryReq)] +impl PostCategoryReq { + #[wasm_bindgen(js_name = getAll)] + pub async fn export_get_all(&self) -> HomoResult { + panic_hook!(); + let url = blog_backend!("/v2/blog-category-types/1/categories"); + + 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?; + body_or_err(resp).await? + }; + + result.homo_string() + } +} diff --git a/rs/src/cnb/post_category/get_cnb_category_list.rs b/rs/src/cnb/post_category/get_cnb_category_list.rs new file mode 100644 index 00000000..36461e4e --- /dev/null +++ b/rs/src/cnb/post_category/get_cnb_category_list.rs @@ -0,0 +1,29 @@ +use crate::cnb::post_category::PostCategoryReq; +use crate::http::body_or_err; +use crate::infra::http::setup_auth; +use crate::infra::result::{HomoResult, ResultExt}; +use crate::{blog_backend, panic_hook}; +use alloc::format; +use alloc::string::String; +use anyhow::Result; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = PostCategoryReq)] +impl PostCategoryReq { + #[wasm_bindgen(js_name = getSitePresetList)] + pub async fn export_get_site_preset_list(&self) -> HomoResult { + panic_hook!(); + let url = blog_backend!("/category/site"); + + 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?; + body_or_err(resp).await? + }; + + result.err_to_string() + } +} diff --git a/rs/src/cnb/post_category/get_one.rs b/rs/src/cnb/post_category/get_one.rs new file mode 100644 index 00000000..fceea5e1 --- /dev/null +++ b/rs/src/cnb/post_category/get_one.rs @@ -0,0 +1,30 @@ +use crate::cnb::post_category::PostCategoryReq; +use crate::http::body_or_err; +use crate::infra::http::setup_auth; +use crate::infra::result::{HomoResult, ResultExt}; +use crate::{blog_backend, panic_hook}; +use alloc::format; +use alloc::string::String; +use anyhow::Result; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = PostCategoryReq)] +impl PostCategoryReq { + #[wasm_bindgen(js_name = getOne)] + pub async fn export_get_one(&self, category_id: usize) -> HomoResult { + panic_hook!(); + let query = format!("parent={}", category_id); + let url = blog_backend!("/v2/blog-category-types/1/categories?{}", query); + + 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?; + body_or_err(resp).await? + }; + + result.homo_string() + } +} diff --git a/rs/src/cnb/post_category/mod.rs b/rs/src/cnb/post_category/mod.rs new file mode 100644 index 00000000..44292e51 --- /dev/null +++ b/rs/src/cnb/post_category/mod.rs @@ -0,0 +1,28 @@ +mod create; +mod del; +mod get_all; +mod get_cnb_category_list; +mod get_one; +mod update; + +use crate::panic_hook; +use alloc::string::String; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = PostCategoryReq)] +pub struct PostCategoryReq { + token: String, + is_pat_token: bool, +} + +#[wasm_bindgen(js_class = PostCategoryReq)] +impl PostCategoryReq { + #[wasm_bindgen(constructor)] + pub fn new(token: String, is_pat_token: bool) -> PostCategoryReq { + panic_hook!(); + PostCategoryReq { + token, + is_pat_token, + } + } +} diff --git a/rs/src/cnb/post_category/update.rs b/rs/src/cnb/post_category/update.rs new file mode 100644 index 00000000..7147c568 --- /dev/null +++ b/rs/src/cnb/post_category/update.rs @@ -0,0 +1,34 @@ +use crate::cnb::post_category::PostCategoryReq; +use crate::http::unit_or_err; +use crate::infra::http::setup_auth; +use crate::infra::result::ResultExt; +use crate::{blog_backend, panic_hook}; +use alloc::format; +use alloc::string::String; +use anyhow::Result; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = PostCategoryReq)] +impl PostCategoryReq { + #[wasm_bindgen(js_name = update)] + pub async fn export_update( + &self, + category_id: usize, + category_json: String, + ) -> Result<(), String> { + panic_hook!(); + + let url = blog_backend!("/category/blog/{}", category_id); + + let client = reqwest::Client::new().put(url); + + let req = setup_auth(client, &self.token, self.is_pat_token).body(category_json); + + let result: Result<()> = try { + let resp = req.send().await?; + unit_or_err(resp).await? + }; + + result.err_to_string() + } +} diff --git a/rs/src/cnb/user/get_info.rs b/rs/src/cnb/user/get_info.rs index b1d87cd8..f9839fd8 100644 --- a/rs/src/cnb/user/get_info.rs +++ b/rs/src/cnb/user/get_info.rs @@ -1,10 +1,11 @@ use crate::cnb::user::{UserReq, API_BASE_URL}; +use crate::http::body_or_err; use crate::infra::http::setup_auth; -use crate::infra::result::{homo_result_string, HomoResult, IntoResult}; +use crate::infra::result::{HomoResult, ResultExt}; use crate::panic_hook; use alloc::format; use alloc::string::String; -use anyhow::{anyhow, Result}; +use anyhow::Result; use wasm_bindgen::prelude::*; #[wasm_bindgen(js_class = UserReq)] @@ -12,7 +13,7 @@ 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 url = format!("{}/users", API_BASE_URL); let client = reqwest::Client::new().get(url); @@ -20,16 +21,9 @@ impl UserReq { 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()? - } + body_or_err(resp).await? }; - homo_result_string(result) + result.homo_string() } } diff --git a/rs/src/cnb/user/mod.rs b/rs/src/cnb/user/mod.rs index f61c7727..637dd99f 100644 --- a/rs/src/cnb/user/mod.rs +++ b/rs/src/cnb/user/mod.rs @@ -4,7 +4,7 @@ use crate::panic_hook; use alloc::string::{String, ToString}; use wasm_bindgen::prelude::*; -const API_BASE_URL: &str = "https://api.cnblogs.com/api"; +pub(crate) const API_BASE_URL: &str = "https://api.cnblogs.com/api"; #[wasm_bindgen(js_name = UserReq)] pub struct UserReq { diff --git a/rs/src/http/del.rs b/rs/src/http/del.rs index 45aa1b40..34e89ba7 100644 --- a/rs/src/http/del.rs +++ b/rs/src/http/del.rs @@ -1,5 +1,5 @@ use crate::http::{body_or_err, header_json_to_header_map, RsHttp}; -use crate::infra::result::{homo_result_string, HomoResult}; +use crate::infra::result::{HomoResult, ResultExt}; use crate::panic_hook; use alloc::string::String; use anyhow::Result; @@ -12,7 +12,7 @@ impl RsHttp { panic_hook!(); let body = del(url, header_json).await; - homo_result_string(body) + body.homo_string() } } diff --git a/rs/src/http/get.rs b/rs/src/http/get.rs index 8bdb731c..24af4ece 100644 --- a/rs/src/http/get.rs +++ b/rs/src/http/get.rs @@ -1,5 +1,5 @@ use crate::http::{body_or_err, header_json_to_header_map, RsHttp}; -use crate::infra::result::{homo_result_string, HomoResult}; +use crate::infra::result::{HomoResult, ResultExt}; use crate::panic_hook; use alloc::string::String; use anyhow::Result; @@ -12,7 +12,7 @@ impl RsHttp { panic_hook!(); let body = get(url, header_json).await; - homo_result_string(body) + body.homo_string() } } diff --git a/rs/src/http/mod.rs b/rs/src/http/mod.rs index 8d5f8b17..201694e9 100644 --- a/rs/src/http/mod.rs +++ b/rs/src/http/mod.rs @@ -4,9 +4,8 @@ pub mod post; pub mod put; use crate::infra::result::IntoResult; -use crate::panic_hook; -use alloc::string::{String, ToString}; -use anyhow::{anyhow, bail, Result}; +use alloc::string::String; +use anyhow::{bail, Result}; use core::convert::TryFrom; use core::ops::Not; use core::str::FromStr; @@ -20,7 +19,6 @@ use wasm_bindgen::prelude::*; pub struct RsHttp; fn header_json_to_header_map(header_json: &str) -> Result { - panic_hook!(); let header_json = Value::from_str(header_json)?; let header = serde_json::from_value::>(header_json)?; let header_map = HeaderMap::try_from(&header)?; @@ -28,16 +26,15 @@ fn header_json_to_header_map(header_json: &str) -> Result { header_map.into_ok() } -pub async fn unit_or_err(resp: Response) -> Result<(), String> { +pub async fn unit_or_err(resp: Response) -> Result<()> { 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()) + let body = resp.text().await?; + + if code.is_success().not() { + bail!("{}: {}", code, body); + } + + Ok(()) } pub async fn body_or_err(resp: Response) -> Result { diff --git a/rs/src/http/post.rs b/rs/src/http/post.rs index c4518f3b..717c3b95 100644 --- a/rs/src/http/post.rs +++ b/rs/src/http/post.rs @@ -1,5 +1,5 @@ use crate::http::{body_or_err, header_json_to_header_map, RsHttp}; -use crate::infra::result::{homo_result_string, HomoResult}; +use crate::infra::result::{HomoResult, ResultExt}; use crate::panic_hook; use alloc::string::String; use anyhow::Result; @@ -12,7 +12,7 @@ impl RsHttp { panic_hook!(); let body = post(url, header_json, body).await; - homo_result_string(body) + body.homo_string() } } diff --git a/rs/src/http/put.rs b/rs/src/http/put.rs index 6d414b42..024463f9 100644 --- a/rs/src/http/put.rs +++ b/rs/src/http/put.rs @@ -1,5 +1,5 @@ use crate::http::{body_or_err, header_json_to_header_map, RsHttp}; -use crate::infra::result::{homo_result_string, HomoResult}; +use crate::infra::result::{HomoResult, ResultExt}; use crate::panic_hook; use alloc::string::String; use anyhow::Result; @@ -12,7 +12,7 @@ impl RsHttp { panic_hook!(); let body = put(url, header_json, body).await; - homo_result_string(body) + body.homo_string() } } diff --git a/rs/src/infra/http.rs b/rs/src/infra/http.rs index f8534464..2dac80fd 100644 --- a/rs/src/infra/http.rs +++ b/rs/src/infra/http.rs @@ -35,9 +35,13 @@ pub fn setup_auth(builder: RequestBuilder, token: &str, is_pat_token: bool) -> R } } -pub fn cons_query_string(queries: Vec<(&str, &str)>) -> String { +pub fn cons_query_string(queries: Vec<(impl ToString, impl ToString)>) -> String { queries .into_iter() - .map(|(k, v)| format!("{k}={v}")) + .map(|(k, v)| { + let s_k = k.to_string(); + let s_v = v.to_string(); + format!("{}={}", s_k, s_v) + }) .fold("".to_string(), |acc, q| format!("{acc}&{q}")) } diff --git a/rs/src/infra/log.rs b/rs/src/infra/log.rs new file mode 100644 index 00000000..77d08ce0 --- /dev/null +++ b/rs/src/infra/log.rs @@ -0,0 +1,17 @@ +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console)] + pub fn log(text: &str); +} + +#[macro_export] +macro_rules! console_log { + ($text:expr) => { + use alloc::format; + use $crate::infra::log::log; + let text = format!("{}", $text.to_string()); + log(&text); + }; +} diff --git a/rs/src/infra/mod.rs b/rs/src/infra/mod.rs index d2d40234..65f876bc 100644 --- a/rs/src/infra/mod.rs +++ b/rs/src/infra/mod.rs @@ -1,4 +1,5 @@ pub mod http; +pub mod log; pub mod option; pub mod panic_hook; pub mod result; diff --git a/rs/src/infra/result.rs b/rs/src/infra/result.rs index 9b52980f..2ad2e607 100644 --- a/rs/src/infra/result.rs +++ b/rs/src/infra/result.rs @@ -19,12 +19,35 @@ impl IntoResult for T {} pub type HomoResult = Result; -pub fn homo_result_string(r: Result, E>) -> HomoResult -where - E: ToString, -{ - match r { - Ok(o) => Ok(o.into()), - Err(e) => Err(e.to_string()), +pub trait ResultExt { + fn err_to_string(self) -> Result + where + E: ToString; + + fn homo_string(self) -> HomoResult + where + O: ToString, + E: ToString; +} + +impl ResultExt for Result { + #[inline] + fn err_to_string(self) -> Result + where + E: ToString, + { + self.map_err(|e| e.to_string()) + } + + #[inline] + fn homo_string(self) -> HomoResult + where + O: ToString, + E: ToString, + { + match self { + Ok(o) => Ok(o.to_string()), + Err(e) => Err(e.to_string()), + } } } diff --git a/rs/src/lib.rs b/rs/src/lib.rs index 25be9605..97737ff8 100644 --- a/rs/src/lib.rs +++ b/rs/src/lib.rs @@ -8,3 +8,4 @@ pub mod http; pub mod infra; pub mod rand; pub mod regex; +pub mod text; diff --git a/rs/src/regex.rs b/rs/src/regex.rs index fa9ab3af..43848d69 100644 --- a/rs/src/regex.rs +++ b/rs/src/regex.rs @@ -8,12 +8,12 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen(typescript_custom_section)] const _: &str = r" -export type RsMatch = { offset: number, groups: string[] } +export type RsMatch = { byte_offset: number, groups: string[] } "; #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct RsMatch { - offset: usize, + byte_offset: usize, groups: Vec, } @@ -46,13 +46,16 @@ fn matches(regex: &str, text: &str) -> Vec { regex .captures_iter(text) .map(|caps| { - let offset = caps.get(0).unwrap().start(); + let byte_offset = caps.get(0).unwrap().start(); let groups = caps .iter() .map(|m| m.map(|s| s.as_str()).unwrap_or("").to_string()) .collect(); - RsMatch { offset, groups } + RsMatch { + byte_offset, + groups, + } }) .collect() } @@ -63,7 +66,7 @@ fn test_matches() { let regex = r#"(!\[.*?]\()(data:image\/.*?,[a-zA-Z0-9+/]*?=?=?)\)"#; let mgs = matches(regex, text); let expect = vec![RsMatch { - offset: 0, + byte_offset: 0, groups: vec![ "![img](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAQCAYAAADNo/U5AAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAZ25vbWUtc2NyZWVuc2hvdO8Dvz4AAAAtdEVYdENyZWF0aW9uIFRpbWUARnJpIDE0IEp1bCAyMDIzIDExOjAzOjAwIEFNIENTVCKwI9kAAACwSURBVCiR7dK/DsFwFMXx76WNLrUR7Jq+hcRTGD2C0XuYvISIRYLEKDFLTBYmpP41oaXyM4iIomnMznhOPtO9cvTOilC8yZRZpYpeyGP12uGZxFsTI38UhZT8gCSVumPvFB/p2QwAwWbH9eDGQ8m0iWEXAdi2Oq+jUsinNwJwB0PmtTqIYJZLGLYFvo87Gn9HAPtun1WjyXm+eJYi0eiRYLnm4jiIpqEXcvFQOD8d9wYk9T77dSRnWwAAAABJRU5ErkJggg==)".to_string(), "![img](".to_string(), diff --git a/rs/src/text.rs b/rs/src/text.rs new file mode 100644 index 00000000..3048a534 --- /dev/null +++ b/rs/src/text.rs @@ -0,0 +1,24 @@ +use crate::panic_hook; +use alloc::string::String; +use alloc::vec::Vec; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(js_name = RsText)] +struct RsText; + +#[wasm_bindgen(js_class = RsText)] +impl RsText { + #[wasm_bindgen(js_name = replaceWithByteOffset)] + pub fn export_replace_with_byte_offset( + raw: String, + start: usize, + end: usize, + replace_with: String, + ) -> String { + panic_hook!(); + let mut vec: Vec = raw.as_bytes().to_vec(); + let replace_with = replace_with.as_bytes().iter().copied(); + vec.splice(start..end, replace_with); + String::from_utf8(vec).unwrap() + } +} diff --git a/src/auth/account-manager.ts b/src/auth/auth-manager.ts similarity index 56% rename from src/auth/account-manager.ts rename to src/auth/auth-manager.ts index cb52569f..108da99e 100644 --- a/src/auth/account-manager.ts +++ b/src/auth/auth-manager.ts @@ -1,5 +1,5 @@ import { globalCtx } from '@/ctx/global-ctx' -import { window, authentication, AuthenticationGetSessionOptions, Disposable } from 'vscode' +import { window, authentication, AuthenticationGetSessionOptions as AuthGetSessionOpt } 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' @@ -10,15 +10,31 @@ import { BlogExportProvider } from '@/tree-view/provider/blog-export-provider' import { Alert } from '@/infra/alert' import { execCmd } from '@/infra/cmd' -const isAuthorizedStorageKey = 'isAuthorized' +let authSession: AuthSession | null = null -export const ACQUIRE_TOKEN_REJECT_UNAUTHENTICATED = 'unauthenticated' -export const ACQUIRE_TOKEN_REJECT_EXPIRED = 'expired' +authProvider.onDidChangeSessions(async ({ added }) => { + authSession = null + if (added != null && added.length > 0) await AuthManager.ensureSession() -let authSession: AuthSession | null = null + await AuthManager.updateAuthStatus() + + accountViewDataProvider.fireTreeDataChangedEvent() + postDataProvider.fireTreeDataChangedEvent(undefined) + postCategoryDataProvider.fireTreeDataChangedEvent() + + BlogExportProvider.optionalInstance?.refreshRecords({ force: false, clearCache: true }).catch(console.warn) +}) + +export namespace AuthManager { + export function isAuthed() { + return authSession !== null + } -export namespace AccountManagerNg { - export async function ensureSession(opt?: AuthenticationGetSessionOptions) { + export function getUserInfo() { + return authSession?.account.userInfo + } + + export async function ensureSession(opt?: AuthGetSessionOpt) { let session try { const result = await authentication.getSession(authProvider.providerId, [], opt) @@ -47,7 +63,7 @@ export namespace AccountManagerNg { export async function patLogin() { const opt = { title: '请输入您的个人访问令牌 (PAT)', - prompt: '可通过 https://account.cnblogs.com/tokens 获取', + prompt: '可通过 https://account.cnblos.com/tokens 获取', password: true, } const pat = await window.showInputBox(opt) @@ -56,20 +72,16 @@ export namespace AccountManagerNg { try { await authProvider.onAccessTokenGranted(pat) await ensureSession() - await AccountManagerNg.updateAuthStatus() + await AuthManager.updateAuthStatus() } catch (e) { void Alert.err(`授权失败: ${e}`) } } export async function logout() { - if (!accountManager.isAuthorized) return + if (!AuthManager.isAuthed()) return const session = await authentication.getSession(authProvider.providerId, []) - - // WRN: For old version compatibility, **never** remove this line - await globalCtx.storage.update('user', undefined) - if (session === undefined) return try { @@ -83,56 +95,23 @@ export namespace AccountManagerNg { export async function acquireToken() { const session = await ensureSession({ createIfNone: false }) - if (session == null) return Promise.reject(ACQUIRE_TOKEN_REJECT_UNAUTHENTICATED) - - if (session.isExpired) return Promise.reject(ACQUIRE_TOKEN_REJECT_EXPIRED) + if (session == null) throw Error('未授权') + if (session.isExpired) throw Error('授权已过期') return session.accessToken } export async function updateAuthStatus() { - await AccountManagerNg.ensureSession({ createIfNone: false }) + await AuthManager.ensureSession({ createIfNone: false }) + const isAuthed = AuthManager.isAuthed() - await execCmd('setContext', `${globalCtx.extName}.${isAuthorizedStorageKey}`, accountManager.isAuthorized) + await execCmd('setContext', `${globalCtx.extName}.isAuthed`, isAuthed) - if (!accountManager.isAuthorized) return + if (!isAuthed) return await execCmd('setContext', `${globalCtx.extName}.user`, { - name: accountManager.currentUser?.userInfo.DisplayName, - avatar: accountManager.currentUser?.userInfo.Avatar, - }) - } -} - -class AccountManager extends Disposable { - private readonly _disposable = Disposable.from( - authProvider.onDidChangeSessions(async ({ added }) => { - authSession = null - if (added != null && added.length > 0) await AccountManagerNg.ensureSession() - - await AccountManagerNg.updateAuthStatus() - - accountViewDataProvider.fireTreeDataChangedEvent() - postDataProvider.fireTreeDataChangedEvent(undefined) - postCategoryDataProvider.fireTreeDataChangedEvent() - - BlogExportProvider.optionalInstance?.refreshRecords({ force: false, clearCache: true }).catch(console.warn) - }) - ) - - constructor() { - super(() => { - this._disposable.dispose() + name: AuthManager.getUserInfo()?.DisplayName, + avatar: AuthManager.getUserInfo()?.Avatar, }) } - - get isAuthorized() { - return authSession !== null - } - - get currentUser() { - return authSession?.account - } } - -export const accountManager = new AccountManager() diff --git a/src/auth/auth-provider.ts b/src/auth/auth-provider.ts index 9dfb1db3..371ce611 100644 --- a/src/auth/auth-provider.ts +++ b/src/auth/auth-provider.ts @@ -3,7 +3,7 @@ import { genVerifyChallengePair } from '@/service/code-challenge' import { authentication, AuthenticationProvider, - AuthenticationProviderAuthenticationSessionsChangeEvent as VscAuthProviderAuthSessionChEv, + AuthenticationProviderAuthenticationSessionsChangeEvent as APASCE, CancellationTokenSource, Disposable, env, @@ -16,27 +16,25 @@ import { globalCtx } from '@/ctx/global-ctx' import { Oauth } from '@/auth/oauth' import { extUriHandler } from '@/infra/uri-handler' import { AccountInfo } from '@/auth/account-info' -import { TokenInfo } from '@/model/token-info' -import { Optional } from 'utility-types' import { consUrlPara } from '@/infra/http/infra/url-para' import { RsRand } from '@/wasm' import { Alert } from '@/infra/alert' +import { LocalState } from '@/ctx/local-state' +import { AppConst } from '@/ctx/app-const' async function browserSignIn(challengeCode: string, scopes: string[]) { - const { clientId, responseType, authRoute, authority, clientSecret } = globalCtx.config.oauth - const para = consUrlPara( - ['client_id', clientId], - ['client_secret', clientSecret], - ['response_type', responseType], + ['client_id', AppConst.CLIENT_ID], + ['client_secret', AppConst.CLIENT_SEC], + ['response_type', 'code'], ['nonce', RsRand.string(32)], ['code_challenge', challengeCode], ['code_challenge_method', 'S256'], ['scope', scopes.join(' ')], - ['redirect_uri', globalCtx.extensionUrl] + ['redirect_uri', globalCtx.extUrl] ) - const uri = Uri.parse(`${authority}${authRoute}?${para}`) + const uri = Uri.parse(`${AppConst.ApiBase.OAUTH}/connect/authorize?${para}`) try { await env.openExternal(uri) @@ -50,11 +48,11 @@ export class AuthProvider implements AuthenticationProvider, Disposable { readonly providerName = '博客园Cnblogs' protected readonly sessionStorageKey = `${this.providerId}.sessions` - protected readonly allScopes = globalCtx.config.oauth.scope.split(' ') + protected readonly allScopes = AppConst.OAUTH_SCOPES private _allSessions?: AuthSession[] | null - private readonly _sessionChangeEmitter = new EventEmitter() + private readonly _sessionChangeEmitter = new EventEmitter() private readonly _disposable = Disposable.from( this._sessionChangeEmitter, authentication.registerAuthenticationProvider(this.providerId, this.providerName, this, { @@ -154,7 +152,7 @@ export class AuthProvider implements AuthenticationProvider, Disposable { }, { remove: [], keep: [] } ) - await globalCtx.extCtx.secrets.store(this.sessionStorageKey, JSON.stringify(data.keep)) + await LocalState.setSecret(this.sessionStorageKey, JSON.stringify(data.keep)) this._sessionChangeEmitter.fire({ removed: data.remove, added: undefined, changed: undefined }) } @@ -181,7 +179,7 @@ export class AuthProvider implements AuthenticationProvider, Disposable { accessToken, this.ensureScopes(null) ) - await globalCtx.secretsStorage.store(this.sessionStorageKey, JSON.stringify([session])) + await LocalState.setSecret(this.sessionStorageKey, JSON.stringify([session])) if (!shouldFireSessionAddedEvent) return session @@ -199,14 +197,8 @@ export class AuthProvider implements AuthenticationProvider, Disposable { } protected async getAllSessions(): Promise { - const legacyToken = LegacyTokenStore.getAccessToken() - if (legacyToken !== undefined) { - await this.onAccessTokenGranted(legacyToken, { shouldFireSessionAddedEvent: false }) - void LegacyTokenStore.remove() - } - if (this._allSessions == null || this._allSessions.length <= 0) { - const storage = await globalCtx.secretsStorage.get(this.sessionStorageKey) + const storage = await LocalState.getSecret(this.sessionStorageKey) const sessions = JSON.parse(storage ?? '[]') as AuthSession[] | null | undefined if (Array.isArray(sessions)) this._allSessions = sessions @@ -225,10 +217,3 @@ export class AuthProvider implements AuthenticationProvider, Disposable { } export const authProvider = new AuthProvider() - -class LegacyTokenStore { - static getAccessToken = () => - globalCtx.storage.get>('user')?.authorizationInfo?.accessToken - - static remove = () => globalCtx.storage.update('user', undefined).then(undefined, console.error) -} diff --git a/src/auth/oauth.ts b/src/auth/oauth.ts index 08e0a2a1..f1ceffbb 100644 --- a/src/auth/oauth.ts +++ b/src/auth/oauth.ts @@ -3,17 +3,16 @@ import { TokenInfo } from '@/model/token-info' import { globalCtx } from '@/ctx/global-ctx' import { Alert } from '@/infra/alert' import { OauthReq } from '@/wasm' +import { AppConst } from '@/ctx/app-const' function getAuthedOauthReq() { - const clientId = globalCtx.config.oauth.clientId - const clientSec = globalCtx.config.oauth.clientSecret - return new OauthReq(clientId, clientSec) + return new OauthReq(AppConst.CLIENT_ID, AppConst.CLIENT_SEC) } export namespace Oauth { export async function getToken(verifyCode: string, authCode: string) { const req = getAuthedOauthReq() - const callback_url = globalCtx.extensionUrl + const callback_url = globalCtx.extUrl try { const resp = await req.getToken(authCode, verifyCode, callback_url) return TokenInfo.fromResp(resp) diff --git a/src/cmd/blog-export/create.ts b/src/cmd/blog-export/create.ts index 40f8e555..9f729886 100644 --- a/src/cmd/blog-export/create.ts +++ b/src/cmd/blog-export/create.ts @@ -1,21 +1,28 @@ import { Alert } from '@/infra/alert' import { BlogExportApi } from '@/service/blog-export/blog-export' import { BlogExportProvider } from '@/tree-view/provider/blog-export-provider' -import { MessageItem } from 'vscode' export async function createBlogExport() { - if (!(await confirm())) return + const isConfirm = await confirm() + if (!isConfirm) return - const isOk = await BlogExportApi.create().catch((e: unknown) => { - Alert.httpErr(typeof e === 'object' && e ? e : {}, { message: '创建博客备份失败' }) + try { + await BlogExportApi.create() + await BlogExportProvider.optionalInstance?.refreshRecords() + } catch (e) { + void Alert.err(`创建博客备份失败: ${e}`) return false - }) - - if (isOk) await BlogExportProvider.optionalInstance?.refreshRecords() + } } async function confirm() { - const items: MessageItem[] = [{ title: '确定', isCloseAffordance: false }] - const result = await Alert.info('确定要创建备份吗?', { modal: true, detail: '一天可以创建一次备份' }, ...items) - return result != null + const result = await Alert.info( + '确定要创建备份吗?', + { modal: true, detail: '一天可以创建一次备份' }, + { + title: '确定', + isCloseAffordance: false, + } + ) + return result !== undefined } diff --git a/src/cmd/blog-export/delete.ts b/src/cmd/blog-export/delete.ts index fa2a1987..b6a2afc1 100644 --- a/src/cmd/blog-export/delete.ts +++ b/src/cmd/blog-export/delete.ts @@ -62,8 +62,8 @@ async function deleteExportRecordItem(item: BlogExportRecordTreeItem) { const { shouldDeleteLocal } = confirmResult const hasDeleted = await BlogExportApi.del(record.id) .then(() => true) - .catch((e: unknown) => { - Alert.httpErr(typeof e === 'object' && e != null ? e : {}) + .catch(e => { + void Alert.err(`删除博客备份失败: ${e}`) return false }) if (hasDeleted) if (downloaded) await removeDownloadedBlogExport(downloaded, { shouldDeleteLocal }) diff --git a/src/cmd/browser.ts b/src/cmd/browser.ts index 37053110..feb4578c 100644 --- a/src/cmd/browser.ts +++ b/src/cmd/browser.ts @@ -1,6 +1,6 @@ import { Uri } from 'vscode' -import { accountManager } from '@/auth/account-manager' import { execCmd } from '@/infra/cmd' +import { AuthManager } from '@/auth/auth-manager' export namespace Browser.Open { export function open(url: string) { @@ -19,14 +19,14 @@ export namespace Browser.Open.User { export const accountSetting = () => open('https://account.cnblogs.com/settings/account') export const blog = () => { - const blogApp = accountManager.currentUser?.userInfo.BlogApp + const blogApp = AuthManager.getUserInfo()?.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 + const accountId = AuthManager.getUserInfo()?.SpaceUserID if (accountId === undefined || accountId <= 0) return const url = `https://home.cnblogs.com/u/${accountId}` diff --git a/src/cmd/cmd-handler.ts b/src/cmd/cmd-handler.ts index 76c73995..5cd866bc 100644 --- a/src/cmd/cmd-handler.ts +++ b/src/cmd/cmd-handler.ts @@ -1,8 +1,8 @@ -export interface CmdHandler { +export type CmdHandler = { handle(): Promise | void } -export interface TreeViewCmdHandler { +export type TreeViewCmdHandler = { readonly input: unknown parseInput(): TData | null | undefined diff --git a/src/cmd/extract-img.ts b/src/cmd/extract-img.ts deleted file mode 100644 index 81e4e2d1..00000000 --- a/src/cmd/extract-img.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { MessageOptions, ProgressLocation, Range, Uri, window, workspace, WorkspaceEdit } from 'vscode' -import { ImgInfo, ImgSrc, MkdImgExtractor } from '@/markdown/mkd-img-extractor' -import { Alert } from '@/infra/alert' -import { findImgLink } from '@/infra/filter/find-img-link' - -export async function extractImg(arg: unknown, inputImgSrc?: ImgSrc) { - if (!(arg instanceof Uri && arg.scheme === 'file')) return - - const editor = window.visibleTextEditors.find(x => x.document.fileName === arg.fsPath) - const textDocument = editor?.document ?? workspace.textDocuments.find(x => x.fileName === arg.fsPath) - - if (!textDocument) return - await textDocument.save() - - const markdown = (await workspace.fs.readFile(arg)).toString() - let imgInfoList = findImgLink(markdown) - - if (imgInfoList.length <= 0) { - if (inputImgSrc === undefined) void Alert.info('没有找到可以提取的图片') - return - } - - const webImgCount = imgInfoList.filter(i => i.src === ImgSrc.web).length - const dataUrlImgCount = imgInfoList.filter(i => i.src === ImgSrc.dataUrl).length - const fsImgCount = imgInfoList.filter(i => i.src === ImgSrc.fs).length - - const options = [ - { title: '提取全部', src: ImgSrc.any }, - { title: '取消', src: undefined, isCloseAffordance: true }, - ] - const detail = ['共找到:'] - - if (webImgCount > 0) { - options.push({ title: '提取网络图片', src: ImgSrc.web }) - detail.push(`${webImgCount} 张可以提取的网络图片`) - } - if (dataUrlImgCount > 0) { - options.push({ title: '提取 Data Url 图片', src: ImgSrc.dataUrl }) - detail.push(`${dataUrlImgCount} 张可以提取的 Data Url 图片`) - } - if (fsImgCount > 0) { - options.push({ title: '提取本地图片', src: ImgSrc.fs }) - detail.push(`${fsImgCount} 张可以提取的本地图片`) - } - - let selectedSrc: ImgSrc | undefined - - if (inputImgSrc !== undefined) { - selectedSrc = inputImgSrc - } else { - // if src is not specified: - const selected = await Alert.info( - '要提取哪些图片? 此操作会替换源文件中的图片链接!', - { - modal: true, - detail: detail.join('\n'), - } as MessageOptions, - ...options - ) - selectedSrc = selected?.src - } - - if (selectedSrc === undefined) return - if (selectedSrc !== ImgSrc.any) imgInfoList = imgInfoList.filter(i => i.src === selectedSrc) - - const extractor = new MkdImgExtractor(arg) - - const failedImages = await window.withProgress( - { title: '正在提取图片', location: ProgressLocation.Notification }, - async progress => { - extractor.onProgress = (count, info) => { - const total = info.length - const image = info[count] - progress.report({ - increment: (count / total) * 50, - message: `[${count + 1} / ${total}] 正在提取 ${image.data}`, - }) - } - - const extracted = await extractor.extract(imgInfoList) - const extractedLen = extracted.length - - const we = extracted - .filter(([, dst]) => dst != null) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .map(([src, dst]) => [src, dst!]) - .map(([src, dst], i) => { - const posL = textDocument.positionAt(src.offset) - const posR = textDocument.positionAt(src.offset + src.data.length) - const range = new Range(posL, posR) - - // just for ts type inferring - const ret: [Range, ImgInfo, number] = [range, dst, i] - return ret - }) - .reduce((we, [range, dst, i]) => { - progress.report({ - increment: (i / extractedLen) * 20 + 80, - message: `正在替换图片链接 ${dst.data}`, - }) - const newText = dst.data - we.replace(textDocument.uri, range, newText, { - needsConfirmation: false, - label: dst.data, - }) - - return we - }, new WorkspaceEdit()) - - await workspace.applyEdit(we) - await textDocument.save() - return extracted.filter(([, dst]) => dst === null).map(([src]) => src) - } - ) - - if (failedImages.length > 0) { - const info = failedImages - .map(info => [info.data, extractor.errors.find(([link]) => link === info.data)?.[1] ?? ''].join(',')) - .join('\n') - Alert.err(`${failedImages.length} 张图片提取失败: ${info}`).then(undefined, console.warn) - } -} diff --git a/src/cmd/extract-img/convert-img-info.ts b/src/cmd/extract-img/convert-img-info.ts new file mode 100644 index 00000000..2b062e7b --- /dev/null +++ b/src/cmd/extract-img/convert-img-info.ts @@ -0,0 +1,96 @@ +import fs from 'fs' +import { ImgService } from '@/service/img' +import { Readable } from 'stream' +import { tmpdir } from 'os' +import { Alert } from '@/infra/alert' +import { Progress } from 'vscode' +import { join } from 'path' + +export type ImgInfo = { + byteOffset: number + data: string + src: ImgSrc +} + +export const enum ImgSrc { + web, + dataUrl, + fs, + any, +} + +export async function convertImgInfo( + fileDir: string, + infoList: ImgInfo[], + progress: Progress<{ + message?: string + increment?: number + }> +) { + const result: [src: ImgInfo, newLink: string][] = [] + const err = [] + + for (const src of infoList) { + progress.report({ + message: `正在提取: ${src.data}`, + }) + // reuse resolved link + const resolvedLink = result.find(it => it[0].data === src.data)?.[1] + if (resolvedLink !== undefined) { + result.push([src, resolvedLink]) + continue + } + + try { + const stream = await resolveImgInfo(fileDir, src) + const newLink = await ImgService.upload(stream) + result.push([src, newLink]) + } catch (e) { + err.push(`提取失败(${src.data}): ${e}`) + } + } + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + err.forEach(Alert.err) + + return result +} + +function resolveDataUrlImg(dataUrl: string) { + // reference for this impl: + // https://stackoverflow.com/questions/6850276/how-to-convert-dataurl-to-file-object-in-javascript/7261048#7261048 + + const regex = /data:image\/(.*?);.*?,([a-zA-Z0-9+/]*=?=?)/g + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const mg = Array.from(dataUrl.matchAll(regex)) + const buf = Buffer.from(mg[0][2], 'base64') + + const ext = mg[0][1] + const fileName = `${Date.now()}.${ext}` + const path = `${tmpdir()}/` + fileName + fs.writeFileSync(path, buf, 'utf8') + + return fs.createReadStream(path) +} + +function resolveFsImg(fileDir: string, path: string) { + path = decodeURIComponent(path) + if (fs.existsSync(path)) return fs.createReadStream(path) + const absPath = join(fileDir, path) + return fs.createReadStream(absPath) +} + +// eslint-disable-next-line require-await +async function resolveImgInfo(fileDir: string, info: ImgInfo): Promise { + // for web img + if (info.src === ImgSrc.web) return ImgService.download(info.data) + + // for fs img + if (info.src === ImgSrc.fs) return resolveFsImg(fileDir, info.data) + + // for data url img + if (info.src === ImgSrc.dataUrl) return resolveDataUrlImg(info.data) + + throw Error('Unreachable code') +} diff --git a/src/cmd/extract-img/extract-img.ts b/src/cmd/extract-img/extract-img.ts new file mode 100644 index 00000000..61437e20 --- /dev/null +++ b/src/cmd/extract-img/extract-img.ts @@ -0,0 +1,99 @@ +import { ProgressLocation, Range, Uri, window, workspace, WorkspaceEdit } from 'vscode' +import { ImgSrc } from '@/cmd/extract-img/convert-img-info' +import { Alert } from '@/infra/alert' +import { RsText } from '@/wasm' +import { findImgLink } from '@/cmd/extract-img/find-img-link' +import { convertImgInfo } from '@/cmd/extract-img/convert-img-info' +import { dirname } from 'path' + +export async function extractImg(arg: unknown, inputImgSrc?: ImgSrc) { + if (!(arg instanceof Uri && arg.scheme === 'file')) return + + const editor = window.visibleTextEditors.find(x => x.document.fileName === arg.fsPath) + const textDocument = editor?.document ?? workspace.textDocuments.find(x => x.fileName === arg.fsPath) + + if (textDocument === undefined) return + + let imgInfoList = findImgLink(textDocument.getText()) + + if (imgInfoList.length <= 0) { + if (inputImgSrc === undefined) void Alert.info('没有找到可以提取的图片') + return + } + + const webImgCount = imgInfoList.filter(i => i.src === ImgSrc.web).length + const dataUrlImgCount = imgInfoList.filter(i => i.src === ImgSrc.dataUrl).length + const fsImgCount = imgInfoList.filter(i => i.src === ImgSrc.fs).length + + const options = [ + { title: '提取全部', src: ImgSrc.any }, + { title: '取消', src: undefined, isCloseAffordance: true }, + ] + const detail = ['共找到:'] + + if (webImgCount > 0) { + options.push({ title: '提取网络图片', src: ImgSrc.web }) + detail.push(`${webImgCount} 张可以提取的网络图片`) + } + if (dataUrlImgCount > 0) { + options.push({ title: '提取 Data Url 图片', src: ImgSrc.dataUrl }) + detail.push(`${dataUrlImgCount} 张可以提取的 Data Url 图片`) + } + if (fsImgCount > 0) { + options.push({ title: '提取本地图片', src: ImgSrc.fs }) + detail.push(`${fsImgCount} 张可以提取的本地图片`) + } + + let selectedSrc: ImgSrc | undefined + + if (inputImgSrc !== undefined) { + selectedSrc = inputImgSrc + } else { + // if src is not specified: + const selected = await Alert.info( + '要提取哪些图片? 此操作会替换源文件中的图片链接!', + { + modal: true, + detail: detail.join('\n'), + }, + ...options + ) + selectedSrc = selected?.src + } + + if (selectedSrc === undefined) return + if (selectedSrc !== ImgSrc.any) imgInfoList = imgInfoList.filter(i => i.src === selectedSrc) + + const opt = { title: '提取图片', location: ProgressLocation.Notification } + await window.withProgress(opt, async p => { + const fileDir = dirname(textDocument.uri.fsPath) + console.log(fileDir) + const extracted = await convertImgInfo(fileDir, imgInfoList, p) + const extractedCount = extracted.length + let text = textDocument.getText() + + extracted + // replace from end + .sort((a, b) => b[0].byteOffset - a[0].byteOffset) + .forEach(([src, newLink], i) => { + p.report({ + increment: (i / extractedCount) * 20 + 80, + message: `正在替换: ${newLink}`, + }) + const start = src.byteOffset + const end = src.byteOffset + Buffer.from(src.data).length + text = RsText.replaceWithByteOffset(text, start, end, newLink) + }) + + const firstLine = textDocument.lineAt(0) + const lastLine = textDocument.lineAt(textDocument.lineCount - 1) + const range = new Range(firstLine.range.start, lastLine.range.end) + const we = new WorkspaceEdit() + we.replace(textDocument.uri, range, text, { + label: '', + needsConfirmation: false, + }) + + await workspace.applyEdit(we) + }) +} diff --git a/src/infra/filter/find-img-link.ts b/src/cmd/extract-img/find-img-link.ts similarity index 80% rename from src/infra/filter/find-img-link.ts rename to src/cmd/extract-img/find-img-link.ts index 7656253e..0582f022 100644 --- a/src/infra/filter/find-img-link.ts +++ b/src/cmd/extract-img/find-img-link.ts @@ -1,4 +1,4 @@ -import { ImgInfo, ImgSrc } from '@/markdown/mkd-img-extractor' +import { ImgInfo, ImgSrc } from '@/cmd/extract-img/convert-img-info' import { r } from '@/infra/convert/string-literal' import { RsMatch, RsRegex } from '@/wasm' @@ -20,16 +20,17 @@ const cnbDomain = r`\.cnblogs\.com\/` export function findImgLink(text: string): ImgInfo[] { const imgTagUrlImgMgs = RsRegex.matches(imgTagUrlImgPat, text) as RsMatch[] const mkdUrlImgMgs = RsRegex.matches(mkdUrlImgPat, text) as RsMatch[] - const urlImgInfo = imgTagUrlImgMgs.concat(mkdUrlImgMgs).map(mg => { + const urlImgInfo = imgTagUrlImgMgs.concat(mkdUrlImgMgs).map(mg => { const data = mg.groups[2] const prefix = mg.groups[1] + const byteOffset = mg.byte_offset + Buffer.from(prefix).length let src if (/https?:\/\//.test(data)) src = ImgSrc.web else src = ImgSrc.fs - return { - offset: mg.offset + prefix.length, + return { + byteOffset, data, src, } @@ -37,12 +38,13 @@ export function findImgLink(text: string): ImgInfo[] { const imgTagDataUrlImgMgs = RsRegex.matches(imgTagDataUrlImgPat, text) as RsMatch[] const mkdDataUrlImgMgs = RsRegex.matches(mkdDataUrlImgPat, text) as RsMatch[] - const dataUrlImgInfo = imgTagDataUrlImgMgs.concat(mkdDataUrlImgMgs).map(mg => { + const dataUrlImgInfo = imgTagDataUrlImgMgs.concat(mkdDataUrlImgMgs).map(mg => { const data = mg.groups[2] const prefix = mg.groups[1] + const byteOffset = mg.byte_offset + Buffer.from(prefix).length - return { - offset: mg.offset + prefix.length, + return { + byteOffset, data, src: ImgSrc.dataUrl, } diff --git a/src/cmd/ing/comment-ing.ts b/src/cmd/ing/comment-ing.ts index 118a251c..93b69c6f 100644 --- a/src/cmd/ing/comment-ing.ts +++ b/src/cmd/ing/comment-ing.ts @@ -1,5 +1,5 @@ import { CmdHandler } from '@/cmd/cmd-handler' -import { IngApi } from '@/service/ing/ing-api' +import { IngService } from '@/service/ing/ing' import { getIngListWebviewProvider } from '@/service/ing/ing-list-webview-provider' import { ProgressLocation, window } from 'vscode' @@ -30,7 +30,7 @@ export class CommentIngCmdHandler implements CmdHandler { if (this._content) { return window.withProgress({ location: ProgressLocation.Notification, title: '正在请求...' }, async p => { p.report({ increment: 30 }) - const isSuccess = await IngApi.comment( + const isSuccess = await IngService.comment( this._ingId, atContent + this._content, atUserId, diff --git a/src/cmd/ing/pub-ing.ts b/src/cmd/ing/pub-ing.ts index 3c98cc11..6ec0c74c 100644 --- a/src/cmd/ing/pub-ing.ts +++ b/src/cmd/ing/pub-ing.ts @@ -1,8 +1,7 @@ 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 { IngService } from '@/service/ing/ing' import { getIngListWebviewProvider } from '@/service/ing/ing-list-webview-provider' import { ProgressLocation, Uri, window } from 'vscode' @@ -14,11 +13,13 @@ async function afterPub(ingIsPrivate: boolean) { const codeOpen = (uri: string) => execCmd('vscode.open', Uri.parse(uri)) + const ingSite = 'https://ing.cnblogs.com' + const options = [ - ['打开闪存', () => codeOpen(globalCtx.config.ingSite)], - ['我的闪存', () => codeOpen(`${globalCtx.config.ingSite}/#my`)], - ['新回应', () => codeOpen(`${globalCtx.config.ingSite}/#recentcomment`)], - ['提到我', () => codeOpen(`${globalCtx.config.ingSite}/#mention`)], + ['打开闪存', () => codeOpen(ingSite)], + ['我的闪存', () => codeOpen(`${ingSite}/#my`)], + ['新回应', () => codeOpen(`${ingSite}/#recentcomment`)], + ['提到我', () => codeOpen(`${ingSite}/#mention`)], ] as const const selected = await Alert.info('闪存已发布, 快去看看吧', ...options.map(v => ({ title: v[0], id: v[0] }))) @@ -34,7 +35,7 @@ export function pubIng(content: string, isPrivate: boolean) { void window.withProgress(opt, async p => { p.report({ increment: 40 }) - const isSuccess = await IngApi.pub(content, isPrivate) + const isSuccess = await IngService.pub(content, isPrivate) p.report({ increment: 100 }) if (isSuccess) void afterPub(isPrivate) p.report({ increment: 100 }) diff --git a/src/cmd/pdf/export-pdf.ts b/src/cmd/pdf/export-pdf.ts index baf0dcf9..adb3c35b 100644 --- a/src/cmd/pdf/export-pdf.ts +++ b/src/cmd/pdf/export-pdf.ts @@ -8,12 +8,12 @@ import { PostFileMapManager } from '@/service/post/post-file-map' import { PostService } from '@/service/post/post' import { extTreeViews } from '@/tree-view/tree-view-register' import { ChromiumPathProvider } from '@/infra/chromium-path-provider' -import { accountManager } from '@/auth/account-manager' import { Alert } from '@/infra/alert' import { PostTreeItem } from '@/tree-view/model/post-tree-item' import { PostEditDto } from '@/model/post-edit-dto' import { PostPdfTemplateBuilder } from '@/cmd/pdf/post-pdf-template-builder' import { ChromiumCfg } from '@/ctx/cfg/chromium' +import { AuthManager } from '@/auth/auth-manager' async function launchBrowser(chromiumPath: string) { try { @@ -146,7 +146,7 @@ async function handleUriInput(uri: Uri) { const postList: Post[] = [] const { fsPath } = uri const postId = PostFileMapManager.getPostId(fsPath) - const { post: inputPost } = (await PostService.fetchPostEditDto(postId && postId > 0 ? postId : -1)) ?? {} + const { post: inputPost } = (await PostService.getPostEditDto(postId && postId > 0 ? postId : -1)) ?? {} if (!inputPost) { return [] @@ -164,7 +164,7 @@ async function handleUriInput(uri: Uri) { } const mapToPostEditDto = async (postList: Post[]) => - (await Promise.all(postList.map(p => PostService.fetchPostEditDto(p.id)))) + (await Promise.all(postList.map(p => PostService.getPostEditDto(p.id)))) .filter((x): x is PostEditDto => x != null) .map(x => x?.post) @@ -183,7 +183,7 @@ export async function exportPostToPdf(input?: Post | PostTreeItem | Uri): Promis const chromiumPath = await retrieveChromiumPath() if (chromiumPath === undefined) return - const blogApp = accountManager.currentUser?.userInfo.BlogApp + const blogApp = AuthManager.getUserInfo()?.BlogApp if (blogApp === undefined) return void Alert.warn('无法获取博客地址, 请检查登录状态') reportErrors( diff --git a/src/cmd/pdf/post-pdf-template-builder.ts b/src/cmd/pdf/post-pdf-template-builder.ts index 1cbbff9a..27cf2d92 100644 --- a/src/cmd/pdf/post-pdf-template-builder.ts +++ b/src/cmd/pdf/post-pdf-template-builder.ts @@ -2,10 +2,10 @@ import { Post } from '@/model/post' import { PostFileMapManager } from '@/service/post/post-file-map' import fs from 'fs' import { BlogSettingService } from '@/service/blog-setting' -import { accountManager } from '@/auth/account-manager' import { PostCategoryService } from '@/service/post/post-category' import { PostCategory } from '@/model/post-category' import { markdownItFactory } from '@cnblogs/markdown-it-presets' +import { AuthManager } from '@/auth/auth-manager' export namespace PostPdfTemplateBuilder { export const HighlightedMessage = 'markdown-highlight-finished' @@ -36,7 +36,7 @@ export namespace PostPdfTemplateBuilder { } const buildCategoryHtml = async (): Promise => { - const categories = await PostCategoryService.listCategories() + const categories = await PostCategoryService.getAll() const postCategories = post.categoryIds ?.map(categoryId => categories.find(x => x.categoryId === categoryId)) @@ -66,7 +66,7 @@ export namespace PostPdfTemplateBuilder { blogId, } = setting - const userId = accountManager.currentUser?.userInfo.UserId + const userId = AuthManager.getUserInfo()?.UserId return ` diff --git a/src/cmd/post-category/del-selected-category.ts b/src/cmd/post-category/del-selected-category.ts index f4af7125..49e82831 100644 --- a/src/cmd/post-category/del-selected-category.ts +++ b/src/cmd/post-category/del-selected-category.ts @@ -36,7 +36,7 @@ export class DeletePostCategoriesHandler extends BaseMultiSelectablePostCategory try { const increment = Math.round(10 + idx / selectedCategories.length / 90) p.report({ increment, message: `正在删除: 📂${category.title}` }) - await PostCategoryService.deleteCategory(category.categoryId) + await PostCategoryService.del(category.categoryId) idx++ } catch (err) { errs.push([category, err]) diff --git a/src/cmd/post-category/new-post-category.ts b/src/cmd/post-category/new-post-category.ts index 066fb1ff..8bd2436a 100644 --- a/src/cmd/post-category/new-post-category.ts +++ b/src/cmd/post-category/new-post-category.ts @@ -1,43 +1,37 @@ -import { MessageOptions, ProgressLocation, window } from 'vscode' +import { ProgressLocation, window } from 'vscode' import { PostCategoryService } from '@/service/post/post-category' import { extTreeViews } from '@/tree-view/tree-view-register' import { inputPostCategory } from './input-post-category' import { refreshPostCategoryList } from './refresh-post-category-list' -import { Alert } from '@/infra/alert' export const newPostCategory = async () => { const input = await inputPostCategory({ title: '新建分类', }) - if (!input) return + if (input === undefined) return - await window.withProgress( - { - title: '正在新建博文分类', - location: ProgressLocation.Notification, - }, - async p => { - p.report({ - increment: 50, - }) - try { - await PostCategoryService.newCategory(input) - p.report({ - increment: 90, - }) - refreshPostCategoryList() - const newCategory = (await PostCategoryService.listCategories()).find(x => x.title === input.title) - if (newCategory) await extTreeViews.postCategoriesList.reveal(newCategory) - } catch (err) { - void Alert.err('新建博文分类时遇到错误', { - modal: true, - detail: `服务器反回了错误\n${err instanceof Error ? err.message : JSON.stringify(err)}`, - } as MessageOptions) - } finally { - p.report({ - increment: 100, - }) - } - } - ) + const opt = { + title: '正在新建博文分类', + location: ProgressLocation.Notification, + } + + await window.withProgress(opt, async p => { + p.report({ + increment: 30, + }) + await PostCategoryService.create(input) + p.report({ + increment: 70, + }) + + refreshPostCategoryList() + + const allCategory = await PostCategoryService.getAll() + const newCategory = allCategory.find(x => x.title === input.title) + if (newCategory !== undefined) await extTreeViews.postCategoriesList.reveal(newCategory) + + p.report({ + increment: 100, + }) + }) } diff --git a/src/cmd/post-category/update-post-category.ts b/src/cmd/post-category/update-post-category.ts index e9941eba..13fd303c 100644 --- a/src/cmd/post-category/update-post-category.ts +++ b/src/cmd/post-category/update-post-category.ts @@ -1,5 +1,5 @@ import fs from 'fs' -import { MessageOptions, ProgressLocation, window, Uri, workspace } from 'vscode' +import { ProgressLocation, window, Uri, workspace } from 'vscode' import { PostCategory } from '@/model/post-category' import { PostCategoryService } from '@/service/post/post-category' import { inputPostCategory } from './input-post-category' @@ -7,6 +7,7 @@ import { refreshPostCategoryList } from './refresh-post-category-list' import { BasePostCategoryTreeViewCmdHandler } from './base-tree-view-cmd-handler' import { PostCategoryCfg } from '@/ctx/cfg/post-category' import { WorkspaceCfg } from '@/ctx/cfg/workspace' +import { Alert } from '@/infra/alert' class UpdatePostCategoryTreeViewCmdHandler extends BasePostCategoryTreeViewCmdHandler { async handle(): Promise { @@ -17,7 +18,7 @@ class UpdatePostCategoryTreeViewCmdHandler extends BasePostCategoryTreeViewCmdHa title: '编辑博文分类', category, }) - if (!addDto) return + if (addDto === undefined) return const updateDto = Object.assign(new PostCategory(), category, addDto) if (!updateDto) return @@ -30,7 +31,7 @@ class UpdatePostCategoryTreeViewCmdHandler extends BasePostCategoryTreeViewCmdHa async p => { p.report({ increment: 10 }) try { - await PostCategoryService.updateCategory(updateDto) + await PostCategoryService.update(updateDto) refreshPostCategoryList() // 如果选择了createLocalPostFileWithCategory模式且本地有该目录,则重命名该目录 const workspaceUri = WorkspaceCfg.getWorkspaceUri() @@ -42,23 +43,14 @@ class UpdatePostCategoryTreeViewCmdHandler extends BasePostCategoryTreeViewCmdHa const newUri = Uri.joinPath(workspaceUri, addDto.title) await workspace.fs.rename(oldUri, newUri) } - } catch (err) { - this.notifyFailed(err) - } finally { + p.report({ increment: 100 }) + } catch (e) { + void Alert.err(`更新博文失败: ${e}`) p.report({ increment: 100 }) } } ) } - - private notifyFailed(err: unknown) { - void window - .showErrorMessage('更新博文分类失败', { - detail: `服务器返回了错误, ${err instanceof Error ? err.message : JSON.stringify(err)}`, - modal: true, - } as MessageOptions) - .then(undefined, undefined) - } } export const handleUpdatePostCategory = (arg: unknown) => new UpdatePostCategoryTreeViewCmdHandler(arg).handle() diff --git a/src/cmd/post-list/copy-link.ts b/src/cmd/post-list/copy-link.ts index 92c4adde..97c8f8c3 100644 --- a/src/cmd/post-list/copy-link.ts +++ b/src/cmd/post-list/copy-link.ts @@ -53,7 +53,7 @@ export class CopyPostLinkCmdHandler implements TreeViewCmdHandler void Alert.fileNotLinkedToPost(input)) - : PostService.fetchPostEditDto(postId).then(v => v?.post) + : PostService.getPostEditDto(postId).then(v => v?.post) } return Promise.resolve(undefined) diff --git a/src/cmd/post-list/del-post-to-local-file-map.ts b/src/cmd/post-list/del-post-to-local-file-map.ts index 9db10e8e..c400c18e 100644 --- a/src/cmd/post-list/del-post-to-local-file-map.ts +++ b/src/cmd/post-list/del-post-to-local-file-map.ts @@ -1,4 +1,3 @@ -import { MessageOptions } from 'vscode' import { Post } from '@/model/post' import { PostFileMap, PostFileMapManager } from '@/service/post/post-file-map' import { revealPostListItem } from '@/service/post/post-list-view' @@ -7,16 +6,12 @@ import { extTreeViews } from '@/tree-view/tree-view-register' import { Alert } from '@/infra/alert' async function confirm(postList: Post[]): Promise { - const options = ['确定'] - const input = await Alert.info( - '确定要取消这些博文与本地文件的关联吗?', - { - detail: postList.map(x => x.title).join(', '), - modal: true, - } as MessageOptions, - ...options - ) - return input === options[0] + const options = { + detail: postList.map(x => x.title).join(', '), + modal: true, + } + const answer = await Alert.info('确定要取消这些博文与本地文件的关联吗?', options) + return answer === '确定' } export async function delPostToLocalFileMap(post: Post | PostTreeItem) { diff --git a/src/cmd/post-list/del-post.ts b/src/cmd/post-list/del-post.ts index 030dfbd0..8e66dd5c 100644 --- a/src/cmd/post-list/del-post.ts +++ b/src/cmd/post-list/del-post.ts @@ -84,15 +84,11 @@ export async function delSelectedPost(arg: unknown) { refreshPost: true, postIds: selectedPost.map(({ id }) => id), }) - } catch (err) { - void Alert.err('删除博文失败', { - detail: `服务器返回了错误, ${err instanceof Error ? err.message : JSON.stringify(err)}`, - } as MessageOptions) - } finally { - progress.report({ - increment: 100, - }) + } catch (e) { + void Alert.err(`删除博文失败: ${e}`) } + + progress.report({ increment: 100 }) }) isDeleting = false diff --git a/src/cmd/post-list/modify-post-setting.ts b/src/cmd/post-list/modify-post-setting.ts index 1297939b..12fa51e3 100644 --- a/src/cmd/post-list/modify-post-setting.ts +++ b/src/cmd/post-list/modify-post-setting.ts @@ -20,7 +20,8 @@ export async function modifyPostSetting(input: Post | PostTreeItem | Uri) { if (input instanceof Post) { post = input postId = input.id - } else if (input instanceof Uri) { + } else { + //type of input is Uri postId = PostFileMapManager.getPostId(input.fsPath) ?? -1 if (postId < 0) return Alert.fileNotLinkedToPost(input) } @@ -29,7 +30,7 @@ export async function modifyPostSetting(input: Post | PostTreeItem | Uri) { if (post) await revealPostListItem(post) - const editDto = await PostService.fetchPostEditDto(postId) + const editDto = await PostService.getPostEditDto(postId) if (!editDto) return const postEditDto = editDto.post @@ -47,8 +48,7 @@ export async function modifyPostSetting(input: Post | PostTreeItem | Uri) { beforeUpdate: async post => { if (localFilePath && fs.existsSync(localFilePath)) { await saveFilePendingChanges(localFilePath) - const content = await new LocalDraft(localFilePath).readAllText() - post.postBody = content + post.postBody = await new LocalDraft(localFilePath).readAllText() } return true }, diff --git a/src/cmd/post-list/open-post-in-vscode.ts b/src/cmd/post-list/open-post-in-vscode.ts index fd00c85f..736c7037 100644 --- a/src/cmd/post-list/open-post-in-vscode.ts +++ b/src/cmd/post-list/open-post-in-vscode.ts @@ -22,7 +22,7 @@ export async function buildLocalPostFileUri(post: Post, includePostId = false): if (!shouldCreateLocalPostFileWithCategory) return Uri.joinPath(workspaceUri, `${postTitle}${postIdSegment}${ext}`) const firstCategoryId = post.categoryIds?.[0] ?? null - let i = firstCategoryId ? await PostCategoryService.find(firstCategoryId) : null + let i = firstCategoryId ? await PostCategoryService.getOne(firstCategoryId) : null let categoryTitle = '' while (i != null) { categoryTitle = path.join( @@ -52,7 +52,7 @@ export async function openPostInVscode(postId: number, forceUpdateLocalPostFile mappedPostFilePath = undefined } - const { post } = await PostService.fetchPostEditDto(postId) + const { post } = await PostService.getPostEditDto(postId) const workspaceUri = WorkspaceCfg.getWorkspaceUri() await mkDirIfNotExist(workspaceUri) diff --git a/src/cmd/post-list/post-list-view.ts b/src/cmd/post-list/post-list-view.ts index e528bce6..7679c20a 100644 --- a/src/cmd/post-list/post-list-view.ts +++ b/src/cmd/post-list/post-list-view.ts @@ -39,7 +39,7 @@ async function goPage(f: (currentIndex: number) => number) { return } - await PostService.updatePostListStateNg(index, state.pageCap, state.pageItemCount, state.pageCount) + await PostService.updatePostListState(index, state.pageCap, state.pageItemCount, state.pageCount) await PostListView.refresh() } @@ -62,6 +62,8 @@ function updatePostListViewTitle() { } export namespace PostListView { + import calcPageCount = PageList.calcPageCount + export async function refresh({ queue = false } = {}): Promise { if (isRefreshing && !queue) { await refreshTask @@ -73,16 +75,15 @@ export namespace PostListView { 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 page = await postDataProvider.loadPost() + const postCount = await PostService.getPostCount() + const pageCount = calcPageCount(page.cap, postCount) + const pageIndex = page.index const hasPrev = PageList.hasPrev(pageIndex) const hasNext = PageList.hasNext(pageIndex, pageCount) await setPostListContext(pageCount, hasPrev, hasNext) - await PostService.updatePostListStateNg(pageIndex, pageCap, pageItemsCount, pageCount) + await PostService.updatePostListState(pageIndex, page.cap, page.items.length, pageCount) updatePostListViewTitle() await postDataProvider.refreshSearch() await setRefreshing(false) diff --git a/src/cmd/post-list/post-pull-all.ts b/src/cmd/post-list/post-pull-all.ts index 9d614336..32ad1b53 100644 --- a/src/cmd/post-list/post-pull-all.ts +++ b/src/cmd/post-list/post-pull-all.ts @@ -5,7 +5,7 @@ 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' +import { AuthManager } from '@/auth/auth-manager' enum ConflictStrategy { ask, @@ -26,7 +26,7 @@ const MAX_POST_LIMIT = 1000 const MAX_BYTE_LIMIT = MAX_POST_LIMIT * 10000 export async function postPullAll() { - const isVip = accountManager.currentUser?.userInfo.IsVip ?? false + const isVip = AuthManager.getUserInfo()?.IsVip ?? false if (!isVip) { void Alert.info('下载随笔: 您是普通用户, 此功能目前仅面向 [VIP](https://cnblogs.vip/) 用户开放') return diff --git a/src/cmd/post-list/post-pull.ts b/src/cmd/post-list/post-pull.ts index 47b03432..38311e81 100644 --- a/src/cmd/post-list/post-pull.ts +++ b/src/cmd/post-list/post-pull.ts @@ -78,7 +78,7 @@ async function update(contexts: CmdCtx[]) { for (const ctx of contexts) { const { fileUri, postId } = ctx - const { post } = await PostService.fetchPostEditDto(postId) + const { post } = await PostService.getPostEditDto(postId) const textEditors = window.visibleTextEditors.filter(x => x.document.uri.fsPath === fileUri.fsPath) await Promise.all(textEditors.map(editor => editor.document.save())) diff --git a/src/cmd/post-list/rename-post.ts b/src/cmd/post-list/rename-post.ts index c8f43656..2e197eb5 100644 --- a/src/cmd/post-list/rename-post.ts +++ b/src/cmd/post-list/rename-post.ts @@ -12,7 +12,7 @@ import { Alert } from '@/infra/alert' async function renameLinkedFile(post: Post): Promise { const filePath = PostFileMapManager.getFilePath(post.id) - if (!filePath) return + if (filePath === undefined) return const fileUri = Uri.file(filePath) @@ -46,7 +46,7 @@ export async function renamePost(arg: Post | PostTreeItem) { value: post.title, }) - if (!input) return + if (input === undefined) return return window .withProgress( @@ -56,7 +56,7 @@ export async function renamePost(arg: Post | PostTreeItem) { }, async progress => { progress.report({ increment: 10 }) - const editDto = await PostService.fetchPostEditDto(post.id) + const editDto = await PostService.getPostEditDto(post.id) if (!editDto) return false progress.report({ increment: 60 }) diff --git a/src/cmd/post-list/upload-post.ts b/src/cmd/post-list/upload-post.ts index d2421809..cb5c988d 100644 --- a/src/cmd/post-list/upload-post.ts +++ b/src/cmd/post-list/upload-post.ts @@ -12,10 +12,10 @@ import * as path from 'path' 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' +import { extractImg } from '@/cmd/extract-img/extract-img' async function parseFileUri(fileUri: Uri | undefined) { if (fileUri !== undefined && fileUri.scheme !== 'file') return undefined @@ -93,7 +93,7 @@ export async function uploadPost(input: Post | PostTreeItem | PostEditDto | unde let post: Post | undefined if (input instanceof Post) { - const dto = await PostService.fetchPostEditDto(input.id) + const dto = await PostService.getPostEditDto(input.id) post = dto?.post } else { post = input.post @@ -170,7 +170,7 @@ export async function uploadPostFile(fileUri: Uri | undefined) { const postId = PostFileMapManager.getPostId(filePath) if (postId !== undefined && postId >= 0) { - const dto = await PostService.fetchPostEditDto(postId) + const dto = await PostService.getPostEditDto(postId) if (dto !== undefined) await uploadPost(dto) return } @@ -195,7 +195,7 @@ export async function uploadPostFile(fileUri: Uri | undefined) { if (selectedPost === undefined) return await PostFileMapManager.updateOrCreate(selectedPost.id, filePath) - const postEditDto = await PostService.fetchPostEditDto(selectedPost.id) + const postEditDto = await PostService.getPostEditDto(selectedPost.id) if (postEditDto === undefined) return if (!fileContent) await workspace.fs.writeFile(parsedFileUri, Buffer.from(postEditDto.post.postBody)) @@ -212,7 +212,7 @@ export async function uploadPostNoConfirm(input: Post | PostTreeItem | PostEditD let post: Post | undefined if (input instanceof Post) { - const dto = await PostService.fetchPostEditDto(input.id) + const dto = await PostService.getPostEditDto(input.id) post = dto?.post } else { post = input.post @@ -276,7 +276,7 @@ export async function uploadPostFileNoConfirm(fileUri: Uri | undefined) { const postId = PostFileMapManager.getPostId(filePath) if (postId !== undefined && postId >= 0) { - const dto = await PostService.fetchPostEditDto(postId) + const dto = await PostService.getPostEditDto(postId) if (dto !== undefined) await uploadPostNoConfirm(dto) return } @@ -301,7 +301,7 @@ export async function uploadPostFileNoConfirm(fileUri: Uri | undefined) { if (selectedPost === undefined) return await PostFileMapManager.updateOrCreate(selectedPost.id, filePath) - const postEditDto = await PostService.fetchPostEditDto(selectedPost.id) + const postEditDto = await PostService.getPostEditDto(selectedPost.id) if (postEditDto === undefined) return if (!fileContent) await workspace.fs.writeFile(parsedFileUri, Buffer.from(postEditDto.post.postBody)) diff --git a/src/cmd/show-local-file-to-post-info.ts b/src/cmd/show-local-file-to-post-info.ts index 93a934a1..785360b7 100644 --- a/src/cmd/show-local-file-to-post-info.ts +++ b/src/cmd/show-local-file-to-post-info.ts @@ -49,10 +49,10 @@ export async function showLocalFileToPostInfo(input: Uri | number): Promise= 0)) return - const post = (await PostService.fetchPostEditDto(postId))?.post + const post = (await PostService.getPostEditDto(postId))?.post if (!post) return - let categories = await PostCategoryService.listCategories() + let categories = await PostCategoryService.getAll() categories = categories.filter(x => post.categoryIds?.includes(x.categoryId)) const categoryDesc = categories.length > 0 ? `博文分类: ${categories.map(c => c.title).join(', ')}\n` : '' const tagsDesc = post.tags?.length ?? 0 > 0 ? `博文标签: ${post.tags?.join(', ')}\n` : '' diff --git a/src/cmd/view-post-online.ts b/src/cmd/view-post-online.ts index ce939e2a..021e4a98 100644 --- a/src/cmd/view-post-online.ts +++ b/src/cmd/view-post-online.ts @@ -11,10 +11,10 @@ export async function viewPostOnline(input?: Post | PostTreeItem | Uri) { if (input instanceof Uri) { const postId = PostFileMapManager.getPostId(input.fsPath) - if (postId !== undefined) post = (await PostService.fetchPostEditDto(postId))?.post + if (postId !== undefined) post = (await PostService.getPostEditDto(postId))?.post } - if (!post) return + if (post === undefined) return await execCmd('vscode.open', Uri.parse(post.url)) } diff --git a/src/ctx/app-const.ts b/src/ctx/app-const.ts new file mode 100644 index 00000000..2b537e66 --- /dev/null +++ b/src/ctx/app-const.ts @@ -0,0 +1,17 @@ +declare const CNBLOGS_CLIENTID: string +declare const CNBLOGS_CLIENTSECRET: string + +export const isDevEnv = () => process.env.NODE_ENV === 'Development' + +export namespace AppConst { + export const CLIENT_ID = CNBLOGS_CLIENTID + export const CLIENT_SEC = CNBLOGS_CLIENTSECRET + + export namespace ApiBase { + export const BLOG_BACKEND = 'https://i.cnblogs.com/api' + export const OPENAPI = 'https://api.cnblogs.com/api' + export const OAUTH = 'https://oauth.cnblogs.com' + } + + export const OAUTH_SCOPES = ['openid', 'profile', 'CnBlogsApi', 'CnblogsAdminApi'] +} diff --git a/src/ctx/cfg/markdown.ts b/src/ctx/cfg/markdown.ts index a889f29a..ab137524 100644 --- a/src/ctx/cfg/markdown.ts +++ b/src/ctx/cfg/markdown.ts @@ -1,6 +1,6 @@ import { LocalState } from '@/ctx/local-state' import getExtCfg = LocalState.getExtCfg -import { ImgSrc } from '@/markdown/mkd-img-extractor' +import { ImgSrc } from '@/cmd/extract-img/convert-img-info' export namespace MarkdownCfg { const cfgGet = (key: string) => getExtCfg().get(`markdown.${key}`) diff --git a/src/ctx/global-ctx.ts b/src/ctx/global-ctx.ts index 55fdb96a..f277a072 100644 --- a/src/ctx/global-ctx.ts +++ b/src/ctx/global-ctx.ts @@ -1,26 +1,11 @@ import { env, ExtensionContext, Uri } from 'vscode' -import { defaultConfig, devConfig, ExtConst, isDevEnv } from '@/model/config' import path from 'path' export class GlobalCtx { private _extensionContext: ExtensionContext | null = null - private readonly _config: ExtConst = defaultConfig - private readonly _devConfig: ExtConst = devConfig - - get secretsStorage() { - return this.extCtx.secrets - } - - get storage() { - return this.extCtx.globalState - } - - get config(): ExtConst { - return isDevEnv() ? this._devConfig : this._config - } get extCtx(): ExtensionContext { - if (this._extensionContext == null) throw Error('extension context not exist') + if (this._extensionContext == null) throw Error('ext ctx not exist') return this._extensionContext } @@ -29,24 +14,29 @@ export class GlobalCtx { } get extName(): string { - const { name } = <{ name?: string }>this.extCtx.extension.packageJSON - return name ?? 'vscode-cnb' + const name = this.extCtx.extension.packageJSON.name + if (name === undefined) throw Error('ext name not exist') + return name } get publisher(): string { - const { publisher } = <{ publisher?: string }>this.extCtx.extension.packageJSON - return publisher ?? 'cnblogs' + const publisher = this.extCtx.extension.packageJSON.publisher + if (publisher === undefined) throw Error('ext publisher not exist') + return publisher } get displayName() { - return this.extCtx.extension.packageJSON.displayName as string + const displayName = this.extCtx.extension.packageJSON.displayName + if (displayName === undefined) throw Error('ext displayName not exist') + return displayName } get assetsUri() { - return Uri.file(path.join(globalCtx.extCtx.extensionPath, 'dist', 'assets')) + const joined = path.join(globalCtx.extCtx.extensionPath, 'dist', 'assets') + return Uri.file(joined) } - get extensionUrl() { + get extUrl() { return `${env.uriScheme}://${this.publisher}.${this.extName}` } } diff --git a/src/ctx/local-state.ts b/src/ctx/local-state.ts index 15d76ba3..6bfb8533 100644 --- a/src/ctx/local-state.ts +++ b/src/ctx/local-state.ts @@ -1,7 +1,32 @@ import { workspace } from 'vscode' +import { globalCtx } from '@/ctx/global-ctx' export namespace LocalState { export function getExtCfg() { return workspace.getConfiguration('cnblogsClient') } + + export function getState(key: string) { + return globalCtx.extCtx.globalState.get(key) + } + + export function setState(key: string, val: any) { + return globalCtx.extCtx.globalState.update(key, val) + } + + export function delState(key: string) { + return setState(key, undefined) + } + + export function getSecret(key: string) { + return globalCtx.extCtx.secrets.get(key) + } + + export function setSecret(key: string, val: string) { + return globalCtx.extCtx.secrets.store(key, val) + } + + export function delSecret(key: string) { + return globalCtx.extCtx.secrets.delete(key) + } } diff --git a/src/extension.ts b/src/extension.ts index b7a0d0d3..58b14434 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,7 +2,7 @@ import { setupExtTreeView } from '@/tree-view/tree-view-register' import { setupExtCmd } from '@/setup/setup-cmd' import { globalCtx } from '@/ctx/global-ctx' import { window, ExtensionContext } from 'vscode' -import { accountManager, AccountManagerNg } from '@/auth/account-manager' +import { AuthManager } from '@/auth/auth-manager' import { setupWorkspaceWatch, setupCfgWatch, setupWorkspaceFileWatch } from '@/setup/setup-watch' import { extUriHandler } from '@/infra/uri-handler' import { extendMarkdownIt } from '@/markdown/extend-markdownIt' @@ -13,7 +13,8 @@ import { LocalState } from '@/ctx/local-state' export function activate(ctx: ExtensionContext) { globalCtx.extCtx = ctx - ctx.subscriptions.push(accountManager) + // WRN: For old version compatibility, NEVER remove this line + void LocalState.delSecret('user') setupExtCmd() setupExtTreeView() @@ -27,7 +28,7 @@ export function activate(ctx: ExtensionContext) { window.registerUriHandler(extUriHandler) - void AccountManagerNg.updateAuthStatus() + void AuthManager.updateAuthStatus() setupUi(LocalState.getExtCfg()) diff --git a/src/infra/alert.ts b/src/infra/alert.ts index 9968a6e2..8fd64614 100644 --- a/src/infra/alert.ts +++ b/src/infra/alert.ts @@ -1,8 +1,5 @@ -import type { HTTPError } from 'got' -import { isArray } from 'lodash-es' import path from 'path' -import { MessageOptions, ProgressLocation, Uri, window } from 'vscode' -import { isDevEnv } from '@/model/config' +import { ProgressLocation, Uri, window } from 'vscode' export namespace Alert { export const err = window.showErrorMessage @@ -24,21 +21,6 @@ export namespace Alert { ) } - export function dev(log: string) { - if (isDevEnv()) console.log(log) - } - - export function httpErr(httpError: Partial, { message = '' } = {}) { - const body = httpError.response?.body as { errors: string[] | undefined } | undefined | null - let parsedError = '' - const errors = body?.errors - if (isArray(errors)) parsedError = errors.filter(i => typeof i === 'string' && i.length > 0).join(', ') - else if (httpError.message) parsedError = httpError.message - else parsedError = '未知网络错误' - - void Alert.warn((message ? message + (parsedError ? ', ' : '') : '') + parsedError) - } - /** * alert that file not linked to the post * @param file the file path, could be a string or {@link Uri} object @@ -49,10 +31,4 @@ export namespace Alert { file = trimExt ? path.basename(file, path.extname(file)) : file void Alert.warn(`本地文件 ${file} 未关联博客园博文`) } - - export async function alertUnAuth({ onLoginActionHook }: { onLoginActionHook?: () => unknown } = {}) { - const options = ['立即登录'] - const input = await Alert.warn('登录状态已过期, 请重新登录', { modal: true } as MessageOptions, ...options) - if (input === options[0]) onLoginActionHook?.() - } } diff --git a/src/infra/convert/ing-star-to-text.ts b/src/infra/convert/ing-star-to-text.ts index 4c2ce2f2..6c4f1047 100644 --- a/src/infra/convert/ing-star-to-text.ts +++ b/src/infra/convert/ing-star-to-text.ts @@ -2,6 +2,6 @@ export function ingStarToText(ingIcon: string) { const imgTagReg = //gi const mg = Array.from(ingIcon.matchAll(imgTagReg)) - if (mg[0] !== undefined) return `「${mg[0][1]}」` + if (mg[0] !== undefined) return `✧${mg[0][1]}✧` else return '' } diff --git a/src/infra/http-client.ts b/src/infra/http-client.ts index 061df1e8..98c13ecd 100644 --- a/src/infra/http-client.ts +++ b/src/infra/http-client.ts @@ -1,4 +1,4 @@ -import { AccountManagerNg } from '@/auth/account-manager' +import { AuthManager } from '@/auth/auth-manager' import got, { BeforeRequestHook } from 'got' import { isString } from 'lodash-es' import { ReqHeaderKey } from '@/infra/http/infra/header' @@ -11,7 +11,7 @@ const bearerTokenHook: BeforeRequestHook = async opt => { const keyIndex = headerKeys.findIndex(x => x.toLowerCase() === ReqHeaderKey.AUTHORIZATION.toLowerCase()) if (keyIndex < 0) { - const token = await AccountManagerNg.acquireToken() + const token = await AuthManager.acquireToken() if (isString(token)) headers[ReqHeaderKey.AUTHORIZATION] = bearer(token) diff --git a/src/infra/http/authed-req.ts b/src/infra/http/authed-req.ts index aeb1e752..708c6bc4 100644 --- a/src/infra/http/authed-req.ts +++ b/src/infra/http/authed-req.ts @@ -1,4 +1,4 @@ -import { AccountManagerNg } from '@/auth/account-manager' +import { AuthManager } from '@/auth/auth-manager' import { ReqHeaderKey } from '@/infra/http/infra/header' import { bearer } from '@/infra/http/infra/auth-type' @@ -7,7 +7,7 @@ import { Req } from '@/infra/http/req' type Header = Map async function makeAuthed(header: Header) { - const token = await AccountManagerNg.acquireToken() + const token = await AuthManager.acquireToken() header.set(ReqHeaderKey.AUTHORIZATION, bearer(token)) // TODO: need better solution diff --git a/src/infra/input-post-setting.ts b/src/infra/input-post-setting.ts index ec3cc012..3879d705 100644 --- a/src/infra/input-post-setting.ts +++ b/src/infra/input-post-setting.ts @@ -106,9 +106,9 @@ export const inputPostSetting = ( calculateStepNumber('categoryIds') let categories: PostCategory[] = [] try { - categories = await PostCategoryService.listCategories() - } catch (err) { - void Alert.err(err instanceof Error ? err.message : JSON.stringify(err)) + categories = await PostCategoryService.getAll() + } catch (e) { + void Alert.err(e) // 取消 throw InputFlowAction.cancel } diff --git a/src/infra/typed-keys.ts b/src/infra/typed-keys.ts deleted file mode 100644 index 7e238ef9..00000000 --- a/src/infra/typed-keys.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { keys } from 'lodash-es' - -export const typedKeys = (obj: T) => keys(obj) as (keyof T)[] diff --git a/src/infra/untildify.test.ts b/src/infra/untildify.test.ts deleted file mode 100644 index 6e3dda49..00000000 --- a/src/infra/untildify.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { untildify } from '@/infra/untildify' -import { homedir } from 'os' - -describe('untildify', () => { - it('should untildify', () => { - const pathWithTilde = '~/test' - const untildifiedPath = untildify(pathWithTilde) - - expect(untildifiedPath).toBe(homedir() + '/test') - }) -}) diff --git a/src/infra/untildify.ts b/src/infra/untildify.ts deleted file mode 100644 index 0e1d439c..00000000 --- a/src/infra/untildify.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { homedir } from 'os' - -const _tildePathRegex = /^~(?=$|\/|\\)/ -const _homeDir = homedir() - -export const untildify = (pathWithTilde: string) => - pathWithTilde ? pathWithTilde.replace(_tildePathRegex, _homeDir) : pathWithTilde diff --git a/src/markdown/mkd-img-extractor.ts b/src/markdown/mkd-img-extractor.ts deleted file mode 100644 index 0b65a81f..00000000 --- a/src/markdown/mkd-img-extractor.ts +++ /dev/null @@ -1,157 +0,0 @@ -import path from 'path' -import { isString } from 'lodash-es' -import fs from 'fs' -import { Uri, workspace } from 'vscode' -import { ImgService } from '@/service/img' -import { isErrorResponse } from '@/model/error-response' -import { promisify } from 'util' -import { Readable } from 'stream' -import { tmpdir } from 'os' - -export interface ImgInfo { - offset: number - data: string - src: ImgSrc -} - -export const enum ImgSrc { - web, - dataUrl, - fs, - any, -} - -enum ExtractorSt { - pending, - extracting, - extracted, -} - -export class MkdImgExtractor { - private _status = ExtractorSt.pending - private _errors: [imgLink: string, msg: string][] = [] - private readonly _workspaceDirs: string[] = [] - - constructor( - private readonly targetFileUri: Uri, - public onProgress?: (index: number, images: ImgInfo[]) => void - ) { - if (workspace.workspaceFolders !== undefined) - this._workspaceDirs = workspace.workspaceFolders.map(({ uri: { fsPath } }) => fsPath) - } - - get status() { - return this._status - } - - get errors() { - return this._errors - } - - async extract(imgInfoList: ImgInfo[]): Promise<[src: ImgInfo, dst: ImgInfo | null][]> { - this._status = ExtractorSt.extracting - - let count = 0 - - const result: ReturnType extends Promise ? U : never = [] - - for (const srcInfo of imgInfoList) { - if (this.onProgress) this.onProgress(count++, imgInfoList) - - // reuse resolved link - const resolvedLink = result.find(([src, dst]) => dst != null && src.data === srcInfo.data)?.[1]?.data - - const streamOrLink = resolvedLink ?? (await this.resolveImgInfo(srcInfo)) - - const dstInfo = await (async () => { - if (streamOrLink === undefined) return null - try { - const newLink = isString(streamOrLink) ? streamOrLink : await ImgService.upload(streamOrLink) - - return { - ...srcInfo, - data: newLink, - } - } catch (e) { - const errMsg = `上传图片失败, ${isErrorResponse(e) ? e.errors.join(',') : JSON.stringify(e)}` - this._errors.push([srcInfo.data, errMsg]) - return null - } - })() - - result.push([srcInfo, dstInfo]) - } - - this._status = ExtractorSt.extracted - - return result - } - - private async resolveWebImg(url: string) { - try { - return await ImgService.download(url) - } catch (e) { - this._errors.push([url, `无法下载网络图片: ${e}`]) - return - } - } - - private async resolveFsImg(imgPath: string) { - const checkReadAccess = (filePath: string) => - promisify(fs.access)(filePath).then( - () => true, - () => false - ) - - let iPath: string | undefined | null = imgPath - let iDir = 0 - let searchingDirs: string[] | undefined | null - let isEncodedPath = false - - while (iPath != null) { - if (await checkReadAccess(iPath)) return fs.createReadStream(iPath) - - if (!isEncodedPath) { - iPath = decodeURIComponent(iPath) - isEncodedPath = true - continue - } - - searchingDirs ??= [path.dirname(this.targetFileUri.fsPath), ...this._workspaceDirs] - iPath = iDir >= 0 && searchingDirs.length > iDir ? path.resolve(searchingDirs[iDir], imgPath) : undefined - iDir++ - isEncodedPath = false - } - } - - private resolveDataUrlImg(dataUrl: string) { - // reference for this impl: - // https://stackoverflow.com/questions/6850276/how-to-convert-dataurl-to-file-object-in-javascript/7261048#7261048 - - const regex = /data:image\/(.*?);.*?,([a-zA-Z0-9+/]*=?=?)/g - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const mg = Array.from(dataUrl.matchAll(regex)) - const buf = Buffer.from(mg[0][2], 'base64') - - const ext = mg[0][1] - const fileName = `${Date.now()}.${ext}` - const path = `${tmpdir()}/` + fileName - fs.writeFileSync(path, buf, 'utf8') - - return fs.createReadStream(path) - } - - private async resolveImgInfo(info: ImgInfo): Promise { - // for web img - // eslint-disable-next-line no-return-await - if (info.src === ImgSrc.web) return await this.resolveWebImg(info.data) - - // for fs img - // eslint-disable-next-line no-return-await - if (info.src === ImgSrc.fs) return await this.resolveFsImg(info.data) - - // for data url img - if (info.src === ImgSrc.dataUrl) return this.resolveDataUrlImg(info.data) - } -} diff --git a/src/model/blog-export.ts b/src/model/blog-export.ts index 38880144..c137e6ea 100644 --- a/src/model/blog-export.ts +++ b/src/model/blog-export.ts @@ -6,14 +6,14 @@ export enum BlogExportStatus { failed = 4, } -export interface BlogExportRecordList { +export type BlogExportRecordList = { items: BlogExportRecord[] pageIndex: number pageSize: number totalCount: number } -export interface BlogExportRecord { +export type BlogExportRecord = { id: number blogId: number fileName: string @@ -28,7 +28,7 @@ export interface BlogExportRecord { dateAdded: string } -export interface DownloadedBlogExport { +export type DownloadedBlogExport = { filePath: string id?: number | null } diff --git a/src/model/blog-post.ts b/src/model/blog-post.ts index 52508d65..acf89960 100644 --- a/src/model/blog-post.ts +++ b/src/model/blog-post.ts @@ -1,11 +1,6 @@ import { AccessPermission, PostType } from '@/model/post' export type BlogPost = { - id: number - postType: PostType - accessPermission: AccessPermission - title: string - url: string postBody: string categoryIds: [] collectionIds: [] @@ -13,22 +8,15 @@ export type BlogPost = { 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 @@ -40,4 +28,27 @@ export type BlogPost = { isContributeToImpressiveBugActivity: boolean usingEditorId: null sourceUrl: null + + // fields also in PostListRespItem + id: number + postType: PostType + accessPermission: AccessPermission + title: string + url: string + entryName: null + datePublished: string + dateUpdated: string + isMarkdown: boolean + isDraft: boolean + isPinned: boolean + isPublished: boolean + + // fields only in PostLispRespItem + aggCount: number + feedBackCount: number + isInSiteCandidate: boolean + isInSiteHome: boolean + postConfig: number + viewCount: number + webCount: number } diff --git a/src/model/blog-setting.ts b/src/model/blog-setting.ts index 8a46850f..16611221 100644 --- a/src/model/blog-setting.ts +++ b/src/model/blog-setting.ts @@ -35,7 +35,7 @@ export enum CodeHighlightEngineEnum { prismJs, } -export interface BlogSiteDto { +export type BlogSiteDto = { title: string subTitle: string application: string @@ -56,7 +56,7 @@ export interface BlogSiteDto { blogNewsUseMarkdown: boolean } -export interface BlogSiteExtendDto { +export type BlogSiteExtendDto = { blogId: number blogNews: string secondaryCss: string diff --git a/src/model/clipboard-img.ts b/src/model/clipboard-img.ts index a73f9b13..7f93ddd4 100644 --- a/src/model/clipboard-img.ts +++ b/src/model/clipboard-img.ts @@ -1,4 +1,4 @@ -export interface IClipboardImg { +export type IClipboardImg = { imgPath: string /** * if the path is generated by the extension -> false diff --git a/src/model/config.ts b/src/model/config.ts deleted file mode 100644 index 37a62d0b..00000000 --- a/src/model/config.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { env } from 'process' - -export type ExtConst = { - oauth: { - authority: string - tokenRoute: string - authRoute: string - userInfoRoute: string - clientId: string - clientSecret: string - responseType: string - scope: string - revokeRoute: string - } - apiBaseUrl: string - ingSite: string - openApiUrl: string -} - -export const isDevEnv = () => process.env.NODE_ENV === 'Development' - -declare const CNBLOGS_CLIENTID: string -declare const CNBLOGS_CLIENTSECRET: string - -export const defaultConfig: ExtConst = { - oauth: { - authority: 'https://oauth.cnblogs.com', - tokenRoute: '/connect/token', - authRoute: '/connect/authorize', - userInfoRoute: '/connect/userinfo', - clientId: CNBLOGS_CLIENTID, - clientSecret: CNBLOGS_CLIENTSECRET, - responseType: 'code', - scope: 'openid profile CnBlogsApi CnblogsAdminApi', - revokeRoute: '/connect/revocation', - }, - apiBaseUrl: 'https://i.cnblogs.com', - ingSite: 'https://ing.cnblogs.com', - openApiUrl: 'https://api.cnblogs.com', -} - -export const devConfig: ExtConst = { - ...defaultConfig, - oauth: { - ...defaultConfig.oauth, - authority: env.Authority ? env.Authority : 'https://my-oauth.cnblogs.com', - clientId: env.ClientId ? env.ClientId : 'vscode-cnb', - clientSecret: env.ClientSecret ? env.ClientSecret : '', - }, - apiBaseUrl: 'https://admin.cnblogs.com', - ingSite: 'https://my-ing.cnblogs.com', - openApiUrl: 'https://my-api.cnblogs.com', -} diff --git a/src/model/ing-view.ts b/src/model/ing-view.ts index 1ca0523a..0f1874f5 100644 --- a/src/model/ing-view.ts +++ b/src/model/ing-view.ts @@ -1,13 +1,13 @@ import { Ing, IngComment } from './ing' import { PartialTheme, Theme } from '@fluentui/react' -export interface IngAppState { +export type IngAppState = { ingList?: Ing[] theme: Theme | PartialTheme isRefreshing: boolean comments?: Record } -export interface IngItemState { +export type IngItemState = { comments?: Ing[] } diff --git a/src/model/my-config.ts b/src/model/my-config.ts index 190bd4c6..72c657e4 100644 --- a/src/model/my-config.ts +++ b/src/model/my-config.ts @@ -1,75 +1,3 @@ -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 diff --git a/src/model/webview-cmd.ts b/src/model/webview-cmd.ts index f249c1a9..5633edbe 100644 --- a/src/model/webview-cmd.ts +++ b/src/model/webview-cmd.ts @@ -19,11 +19,11 @@ export namespace Webview.Cmd { getChildCategories = 'getChildCategories', } - export type GetChildCategoriesPayload = { + export interface GetChildCategoriesPayload { parentId: number } - export type UpdateChildCategoriesPayload = { + export interface UpdateChildCategoriesPayload { parentId: number value: PostCategory[] } diff --git a/src/model/webview-msg.ts b/src/model/webview-msg.ts index 06706551..17cfb362 100644 --- a/src/model/webview-msg.ts +++ b/src/model/webview-msg.ts @@ -8,7 +8,7 @@ import { SiteCategory } from '@/model/site-category' import { PostCategory } from '@/model/post-category' export namespace WebviewMsg { - export interface Msg { + export type Msg = { command: Webview.Cmd.Ui | Webview.Cmd.Ext } diff --git a/src/service/blog-export/blog-export-post.store.ts b/src/service/blog-export/blog-export-post.store.ts index f4947a40..092d5533 100644 --- a/src/service/blog-export/blog-export-post.store.ts +++ b/src/service/blog-export/blog-export-post.store.ts @@ -3,7 +3,7 @@ import { ExportPostModel } from '@/model/blog-export/export-post' import { DataTypes, Op, Sequelize } from 'sequelize' import { Disposable } from 'vscode' import sqlite3 from 'sqlite3' -import { isDevEnv } from '@/model/config' +import { isDevEnv } from '@/ctx/app-const' export class ExportPostStore implements Disposable { private _sequelize = new Sequelize({ diff --git a/src/service/blog-export/blog-export.ts b/src/service/blog-export/blog-export.ts index 47541adc..46181c8f 100644 --- a/src/service/blog-export/blog-export.ts +++ b/src/service/blog-export/blog-export.ts @@ -1,11 +1,11 @@ import { BlogExportRecord, BlogExportRecordList } from '@/model/blog-export' -import { globalCtx } from '@/ctx/global-ctx' import got from '@/infra/http-client' import { AuthedReq } from '@/infra/http/authed-req' import { consHeader } from '@/infra/http/infra/header' import { consUrlPara } from '@/infra/http/infra/url-para' +import { AppConst } from '@/ctx/app-const' -const basePath = `${globalCtx.config.apiBaseUrl}/api/blogExports` +const basePath = `${AppConst.ApiBase.BLOG_BACKEND}/blogExports` const downloadOrigin = 'https://export.cnblogs.com' export namespace BlogExportApi { diff --git a/src/service/blog-setting.ts b/src/service/blog-setting.ts index 8efbd7f3..5d29d130 100644 --- a/src/service/blog-setting.ts +++ b/src/service/blog-setting.ts @@ -1,8 +1,8 @@ import { BlogSetting, BlogSiteDto, BlogSiteExtendDto } from '@/model/blog-setting' -import { globalCtx } from '@/ctx/global-ctx' import { AuthedReq } from '@/infra/http/authed-req' import { consHeader } from '@/infra/http/infra/header' import { Alert } from '@/infra/alert' +import { AppConst } from '@/ctx/app-const' let cache: BlogSetting | null = null @@ -10,7 +10,7 @@ export namespace BlogSettingService { export async function getBlogSetting(refresh = false) { if (cache != null && !refresh) return cache - const url = `${globalCtx.config.apiBaseUrl}/api/settings` + const url = `${AppConst.ApiBase.BLOG_BACKEND}/settings` try { const resp = await AuthedReq.get(url, consHeader()) diff --git a/src/service/downloaded-export.store.ts b/src/service/downloaded-export.store.ts index 3d008f63..cf5420a1 100644 --- a/src/service/downloaded-export.store.ts +++ b/src/service/downloaded-export.store.ts @@ -1,16 +1,16 @@ import { DownloadedBlogExport } from '@/model/blog-export' -import { globalCtx } from '@/ctx/global-ctx' import { exists, existsSync } from 'fs' import { take } from 'lodash-es' import { promisify } from 'util' +import { LocalState } from '@/ctx/local-state' const listKey = 'downloadExports' const metadataKey = 'downloadedExport-' -const updateList = (value?: DownloadedBlogExport[] | null) => globalCtx.storage.update(listKey, value) +const updateList = (value?: DownloadedBlogExport[] | null) => LocalState.setState(listKey, value) const updateExport = (id: number, value?: DownloadedBlogExport | null) => - globalCtx.storage.update(`${metadataKey}${id}`, value) + LocalState.setState(`${metadataKey}${id}`, value) export namespace DownloadedExportStore { export async function add(filePath: string, id?: number | null) { @@ -27,7 +27,7 @@ export namespace DownloadedExportStore { } export async function list({ prune = true } = {}) { - let items = globalCtx.storage.get(listKey) ?? [] + let items = LocalState.getState(listKey) ?? [] if (prune) { const prunedItems: DownloadedBlogExport[] = [] @@ -50,7 +50,7 @@ export namespace DownloadedExportStore { } } - return Promise.resolve(globalCtx.storage.get(listKey) ?? []) + return Promise.resolve(LocalState.getState(listKey) ?? []) } export async function remove(downloaded: DownloadedBlogExport, { shouldRemoveExportRecordMap = true } = {}) { @@ -66,7 +66,7 @@ export namespace DownloadedExportStore { export async function findById(id: number, { prune = true } = {}) { const key = `${metadataKey}${id}` - let item = globalCtx.storage.get(key) + let item = LocalState.getState(key) as DownloadedBlogExport | undefined if (prune && item) { const isExist = await promisify(exists)(item.filePath) diff --git a/src/service/img.ts b/src/service/img.ts index 06e64ef2..f23a0a5b 100644 --- a/src/service/img.ts +++ b/src/service/img.ts @@ -1,9 +1,9 @@ -import { globalCtx } from '@/ctx/global-ctx' import { Readable } from 'stream' import { isString, merge, pick } from 'lodash-es' import httpClient from '@/infra/http-client' import path from 'path' import { lookup, extension } from 'mime-types' +import { AppConst } from '@/ctx/app-const' export namespace ImgService { export async function upload< @@ -24,7 +24,7 @@ export namespace ImgService { const fd = new (await import('form-data')).default() fd.append('image', file, { filename: finalName, contentType: mimeType }) - const url = `${globalCtx.config.apiBaseUrl}/api/posts/body/images` + const url = `${AppConst.ApiBase.BLOG_BACKEND}/posts/body/images` const resp = await httpClient.post(url, { body: fd, diff --git a/src/service/ing/ing-list-webview-provider.ts b/src/service/ing/ing-list-webview-provider.ts index 3b1a3c7a..a1e8f8db 100644 --- a/src/service/ing/ing-list-webview-provider.ts +++ b/src/service/ing/ing-list-webview-provider.ts @@ -10,7 +10,7 @@ import { } from 'vscode' import { parseWebviewHtml } from '@/service/parse-webview-html' import { IngWebviewHostCmd, IngWebviewUiCmd, Webview } from '@/model/webview-cmd' -import { IngApi } from '@/service/ing/ing-api' +import { IngService } from '@/service/ing/ing' import { IngAppState } from '@/model/ing-view' import { IngType, IngTypesMetadata } from '@/model/ing' import { isNumber } from 'lodash-es' @@ -84,7 +84,7 @@ export class IngListWebviewProvider implements WebviewViewProvider { command: Webview.Cmd.Ing.Ui.setAppState, } as IngWebviewUiCmd>) .then(undefined, () => undefined) - const rawIngList = await IngApi.getList({ + const rawIngList = await IngService.getList({ type: ingType, pageIndex, pageSize: 30, @@ -94,7 +94,7 @@ export class IngListWebviewProvider implements WebviewViewProvider { if (UiCfg.isEnableTextIngStar()) ing.icons = ingStarToText(ing.icons) return ing }) - const comments = await IngApi.getCommentList(...ingList.map(x => x.id)) + const comments = await IngService.getCommentList(...ingList.map(x => x.id)) await this._view.webview .postMessage({ command: Webview.Cmd.Ing.Ui.setAppState, @@ -117,7 +117,7 @@ export class IngListWebviewProvider implements WebviewViewProvider { async updateComments(ingIds: number[]) { if (!this._view || !this._view.visible) return - const comments = await IngApi.getCommentList(...ingIds) + const comments = await IngService.getCommentList(...ingIds) await this._view.webview.postMessage({ command: Webview.Cmd.Ing.Ui.setAppState, payload: { diff --git a/src/service/ing/ing-api.ts b/src/service/ing/ing.ts similarity index 93% rename from src/service/ing/ing-api.ts rename to src/service/ing/ing.ts index 3fa983cd..9216c2f0 100644 --- a/src/service/ing/ing-api.ts +++ b/src/service/ing/ing.ts @@ -1,10 +1,10 @@ import { Ing, IngComment, IngType } from '@/model/ing' import { Alert } from '@/infra/alert' import { IngReq } from '@/wasm' -import { AccountManagerNg } from '@/auth/account-manager' +import { AuthManager } from '@/auth/auth-manager' async function getAuthedIngReq() { - const token = await AccountManagerNg.acquireToken() + const token = await AuthManager.acquireToken() // TODO: need better solution const isPatToken = token.length === 64 return new IngReq(token, isPatToken) @@ -17,7 +17,7 @@ async function getComment(id: number) { return list.map(IngComment.parse) } -export namespace IngApi { +export namespace IngService { export async function pub(content: string, isPrivate: boolean) { try { const req = await getAuthedIngReq() diff --git a/src/service/multi-step-input.ts b/src/service/multi-step-input.ts index 6a736f18..27f4125f 100644 --- a/src/service/multi-step-input.ts +++ b/src/service/multi-step-input.ts @@ -22,7 +22,7 @@ export class InputFlowAction { export type InputStep = (input: MultiStepInput) => Thenable -export interface QuickPickParameters { +export type QuickPickParameters = { title: string step: number totalSteps: number diff --git a/src/service/post/post-category.ts b/src/service/post/post-category.ts index 10ca5594..1bbea037 100644 --- a/src/service/post/post-category.ts +++ b/src/service/post/post-category.ts @@ -1,89 +1,96 @@ import { PostCategory, PostCategoryAddDto } from '@/model/post-category' -import { globalCtx } from '@/ctx/global-ctx' -import { AuthedReq } from '@/infra/http/authed-req' -import { consHeader, ReqHeaderKey } from '@/infra/http/infra/header' import { Alert } from '@/infra/alert' -import { consUrlPara } from '@/infra/http/infra/url-para' - -let cache: Map | null = null +import { SiteCategory } from '@/model/site-category' +import { AuthManager } from '@/auth/auth-manager' +import { PostCategoryReq } from '@/wasm' + +// TODO: need better cache impl +let siteCategoryCache: SiteCategory[] | null = null + +async function getAuthedPostCategoryReq() { + const token = await AuthManager.acquireToken() + // TODO: need better solution + const isPatToken = token.length === 64 + return new PostCategoryReq(token, isPatToken) +} export namespace PostCategoryService { - import ContentType = ReqHeaderKey.ContentType - - export async function findCategories(ids: number[], { useCache = true } = {}) { - ids = ids.filter(x => x > 0) - if (ids.length <= 0) return [] - - const categories = await listCategories(!useCache) - return categories.filter(({ categoryId }) => ids.includes(categoryId)) + export async function getAll() { + const req = await getAuthedPostCategoryReq() + try { + const resp = await req.getAll() + const { categories } = <{ categories: PostCategory[] }>JSON.parse(resp) + return categories.map(x => Object.assign(new PostCategory(), x)) + } catch (e) { + void Alert.err(`查询随笔分类失败: ${e}`) + throw e + } } - export async function listCategories(option: boolean | { forceRefresh?: boolean | null; parentId?: number } = {}) { - const parentId = typeof option === 'object' ? option.parentId ?? -1 : -1 - const shouldForceRefresh = - option === true || (typeof option === 'object' ? option.forceRefresh ?? false : false) - cache ??= new Map() - const map = cache - const cachedCategories = map.get(parentId) - if (cachedCategories && !shouldForceRefresh) return cachedCategories - - const para = consUrlPara(['parent', parentId <= 0 ? '' : `${parentId}`]) - const url = `${globalCtx.config.apiBaseUrl}/api/v2/blog-category-types/1/categories?${para}` + export async function getOne(categoryId: number) { + const req = await getAuthedPostCategoryReq() try { - const resp = await AuthedReq.get(url, consHeader()) - let { categories } = <{ categories: PostCategory[] }>JSON.parse(resp) - categories = categories.map(x => Object.assign(new PostCategory(), x)) - map.set(parentId, categories) - return categories + const resp = await req.getOne(categoryId) + const { parent } = <{ parent: PostCategory | null }>JSON.parse(resp) + return Object.assign(new PostCategory(), parent) } catch (e) { - void Alert.err(`获取随笔分类失败: ${e}`) - return [] + void Alert.err(`查询随笔分类失败: ${e}`) + throw e } } - export async function find(id: number) { - const para = consUrlPara(['parent', id <= 0 ? '' : id.toString()]) - const url = `${globalCtx.config.apiBaseUrl}/api/v2/blog-category-types/1/categories?${para}` - + export async function getAllUnder(parentId: number) { + const req = await getAuthedPostCategoryReq() try { - const resp = await AuthedReq.get(url, consHeader()) - const { parent } = <{ parent?: PostCategory | null }>JSON.parse(resp) - return Object.assign(new PostCategory(), parent) + const resp = await req.getOne(parentId) + const { categories } = <{ categories: PostCategory[] }>JSON.parse(resp) + return categories.map(x => Object.assign(new PostCategory(), x)) } catch (e) { void Alert.err(`查询随笔分类失败: ${e}`) - return new PostCategory() + throw e } } - export async function newCategory(dto: PostCategoryAddDto) { - const url = `${globalCtx.config.apiBaseUrl}/api/category/blog/1` - const header = consHeader([ReqHeaderKey.CONTENT_TYPE, ContentType.appJson]) + export async function create(dto: PostCategoryAddDto) { + const req = await getAuthedPostCategoryReq() const body = JSON.stringify(dto) - - await AuthedReq.post(url, header, body) + try { + await req.create(body) + } catch (e) { + void Alert.err(`创建分类失败: ${e}`) + } } - export async function updateCategory(category: PostCategory) { - const url = `${globalCtx.config.apiBaseUrl}/api/category/blog/${category.categoryId}` - const header = consHeader([ReqHeaderKey.CONTENT_TYPE, ContentType.appJson]) + export async function update(category: PostCategory) { + const req = await getAuthedPostCategoryReq() const body = JSON.stringify(category) - - await AuthedReq.put(url, header, body) + try { + await req.update(category.categoryId, body) + } catch (e) { + void Alert.err(`更新分类失败: ${e}`) + } } - export async function deleteCategory(categoryId: number) { - if (categoryId <= 0) throw Error('Invalid param categoryId') - - const url = `${globalCtx.config.apiBaseUrl}/api/category/blog/${categoryId}` - + export async function del(categoryId: number) { + const req = await getAuthedPostCategoryReq() try { - await AuthedReq.del(url, consHeader()) + await req.del(categoryId) } catch (e) { void Alert.err(`删除分类失败: ${e}`) } } - export function clearCache() { - cache = null + export async function getSitePresetList(forceRefresh = false) { + if (siteCategoryCache != null && !forceRefresh) return siteCategoryCache + const req = await getAuthedPostCategoryReq() + + try { + const resp = await req.getSitePresetList() + siteCategoryCache = JSON.parse(resp) + } catch (e) { + void Alert.err(`获取随笔分类失败: ${e}`) + } + + return siteCategoryCache } } diff --git a/src/service/post/post-cfg-panel.ts b/src/service/post/post-cfg-panel.ts index 53f57e19..3f233cc9 100644 --- a/src/service/post/post-cfg-panel.ts +++ b/src/service/post/post-cfg-panel.ts @@ -3,7 +3,6 @@ import vscode, { Uri } from 'vscode' import { Post } from '@/model/post' import { globalCtx } from '@/ctx/global-ctx' import { PostCategoryService } from './post-category' -import { SiteCategoryService } from '@/service/site-category' import { PostTagService } from './post-tag' import { PostService } from './post' import { isErrorResponse } from '@/model/error-response' @@ -51,24 +50,24 @@ export namespace PostCfgPanel { disposables.push( webview.onDidReceiveMessage(async ({ command }: WebviewMsg.Msg) => { - if (command === Webview.Cmd.Ext.refreshPost) { - await webview.postMessage({ - command: Webview.Cmd.Ui.setFluentIconBaseUrl, - baseUrl: webview.asWebviewUri(Uri.joinPath(resourceRootUri(), 'fonts')).toString() + '/', - } as WebviewMsg.SetFluentIconBaseUrlMsg) - await webview.postMessage({ - command: Webview.Cmd.Ui.editPostCfg, - post: cloneDeep(post), - activeTheme: vscode.window.activeColorTheme.kind, - personalCategories: cloneDeep(await PostCategoryService.listCategories()), - siteCategories: cloneDeep(await SiteCategoryService.fetchAll()), - tags: cloneDeep(await PostTagService.fetchTags()), - breadcrumbs, - fileName: localFileUri - ? path.basename(localFileUri.fsPath, path.extname(localFileUri?.fsPath)) - : '', - } as WebviewMsg.EditPostCfgMsg) - } + if (command !== Webview.Cmd.Ext.refreshPost) return + + await webview.postMessage({ + command: Webview.Cmd.Ui.setFluentIconBaseUrl, + baseUrl: webview.asWebviewUri(Uri.joinPath(resourceRootUri(), 'fonts')).toString() + '/', + } as WebviewMsg.SetFluentIconBaseUrlMsg) + await webview.postMessage({ + command: Webview.Cmd.Ui.editPostCfg, + post: cloneDeep(post), + activeTheme: vscode.window.activeColorTheme.kind, + personalCategories: cloneDeep(await PostCategoryService.getAll()), + siteCategories: cloneDeep(await PostCategoryService.getSitePresetList()), + tags: cloneDeep(await PostTagService.fetchTags()), + breadcrumbs, + fileName: localFileUri + ? path.basename(localFileUri.fsPath, path.extname(localFileUri?.fsPath)) + : '', + } as WebviewMsg.EditPostCfgMsg) }), observeWebviewMessages(panel, option), observeActiveColorSchemaChange(panel), @@ -208,9 +207,7 @@ export namespace PostCfgPanel { await webview.postMessage({ command: Webview.Cmd.Ui.updateChildCategories, payload: { - value: await PostCategoryService.listCategories({ parentId: payload.parentId }).catch( - () => [] - ), + value: await PostCategoryService.getAllUnder(payload.parentId).catch(() => []), parentId: payload.parentId, }, } as WebviewCommonCmd) diff --git a/src/service/post/post-file-map.ts b/src/service/post/post-file-map.ts index 4fc56934..2d97a692 100644 --- a/src/service/post/post-file-map.ts +++ b/src/service/post/post-file-map.ts @@ -1,6 +1,6 @@ import { postCategoryDataProvider } from '@/tree-view/provider/post-category-tree-data-provider' import { postDataProvider } from '@/tree-view/provider/post-data-provider' -import { globalCtx } from '@/ctx/global-ctx' +import { LocalState } from '@/ctx/local-state' const validatePostFileMap = (map: PostFileMap) => map[0] >= 0 && !!map[1] @@ -9,7 +9,7 @@ export type PostFileMap = [postId: number, filePath: string] const storageKey = 'postFileMaps' function getMaps(): PostFileMap[] { - return globalCtx.storage.get(storageKey) ?? [] + return LocalState.getState(storageKey) ?? [] } export namespace PostFileMapManager { @@ -44,7 +44,7 @@ export namespace PostFileMapManager { if (exist) exist[1] = filePath else maps.push([postId, filePath]) - await globalCtx.storage.update(storageKey, maps.filter(validatePostFileMap)) + await LocalState.setState(storageKey, maps.filter(validatePostFileMap)) if (emitEvent) { postDataProvider.fireTreeDataChangedEvent(postId) postCategoryDataProvider.onPostUpdated({ refreshPost: false, postIds: [postId] }) diff --git a/src/service/post/post-tag.ts b/src/service/post/post-tag.ts index e9ac3c91..0ced2330 100644 --- a/src/service/post/post-tag.ts +++ b/src/service/post/post-tag.ts @@ -1,7 +1,7 @@ import { PostTag } from '@/model/post-tag' -import { globalCtx } from '@/ctx/global-ctx' import { AuthedReq } from '@/infra/http/authed-req' import { consHeader } from '@/infra/http/infra/header' +import { AppConst } from '@/ctx/app-const' let cachedTags: PostTag[] | null = null @@ -9,7 +9,7 @@ export namespace PostTagService { export async function fetchTags(forceRefresh = false): Promise { if (cachedTags && !forceRefresh) return cachedTags - const url = `${globalCtx.config.apiBaseUrl}/api/tags/list` + const url = `${AppConst.ApiBase.BLOG_BACKEND}/tags/list` const resp = await AuthedReq.get(url, consHeader()) const list = JSON.parse(resp) diff --git a/src/service/post/post-title-sanitizer.ts b/src/service/post/post-title-sanitizer.ts index 4fae2a7d..ad0174c4 100644 --- a/src/service/post/post-title-sanitizer.ts +++ b/src/service/post/post-title-sanitizer.ts @@ -1,8 +1,8 @@ import path from 'path' import sanitizeFilename from 'sanitize-filename' import { Post } from '@/model/post' -import { globalCtx } from '@/ctx/global-ctx' import { PostFileMapManager } from './post-file-map' +import { LocalState } from '@/ctx/local-state' type InvalidPostFileNameMap = [postId: number, invalidName: string | undefined | null] @@ -18,13 +18,13 @@ export namespace InvalidPostTitleStore { export function store(map: InvalidPostFileNameMap): Thenable { const [postId, invalidName] = map const key = buildStorageKey(postId) - if (invalidName) return globalCtx.storage.update(key, invalidName) - else return globalCtx.storage.update(key, undefined) + if (invalidName) return LocalState.setState(key, invalidName) + else return LocalState.setState(key, undefined) } export function get(postId: number): string | undefined { const key = buildStorageKey(postId) - return globalCtx.storage.get(key) + return LocalState.getState(key) } } @@ -33,7 +33,7 @@ export namespace PostTitleSanitizer { const { id: postId } = post const localFilePath = PostFileMapManager.getFilePath(postId) const { title: postTitle } = post - if (!localFilePath) return postTitle + if (localFilePath === undefined) return postTitle const localFilename = path.basename(localFilePath, path.extname(localFilePath)) const { text: sanitizedTitle } = await sanitize(post) diff --git a/src/service/post/post.ts b/src/service/post/post.ts index c043bb4c..525e50a7 100644 --- a/src/service/post/post.ts +++ b/src/service/post/post.ts @@ -1,4 +1,3 @@ -import { globalCtx } from '@/ctx/global-ctx' import { PostEditDto } from '@/model/post-edit-dto' import { PostUpdatedResp } from '@/model/post-updated-response' import { ZzkSearchResult } from '@/model/zzk-search-result' @@ -7,20 +6,28 @@ import { rmYfm } from '@/infra/filter/rm-yfm' import { PostListState } from '@/model/post-list-state' import { Alert } from '@/infra/alert' import { consUrlPara } from '@/infra/http/infra/url-para' -import { consHeader, ReqHeaderKey } from '@/infra/http/infra/header' +import { consHeader } 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' +import { AuthManager } from '@/auth/auth-manager' +import { PostReq } from '@/wasm' +import { LocalState } from '@/ctx/local-state' +import { AppConst } from '@/ctx/app-const' let newPostTemplate: PostEditDto | undefined -const getBaseUrl = () => globalCtx.config.apiBaseUrl +async function getAuthedPostReq() { + const token = await AuthManager.acquireToken() + // TODO: need better solution + const isPatToken = token.length === 64 + return new PostReq(token, isPatToken) +} export namespace PostService { - import ContentType = ReqHeaderKey.ContentType - export const getPostListState = () => globalCtx.storage.get('postListState') + export const getPostListState = () => LocalState.getState('postListState') export async function fetchPostList({ search = '', pageIndex = 1, pageSize = 30, categoryId = <'' | number>'' }) { const para = consUrlPara( @@ -30,7 +37,7 @@ export namespace PostService { ['search', search], ['cid', categoryId.toString()] ) - const url = `${getBaseUrl()}/api/posts/list?${para}` + const url = `${AppConst.ApiBase.BLOG_BACKEND}/posts/list?${para}` const resp = await AuthedReq.get(url, consHeader()) const listModel = JSON.parse(resp) const page = { @@ -47,27 +54,45 @@ export namespace PostService { } } + export async function getPostList(pageIndex: number, pageCap: number) { + const req = await getAuthedPostReq() + try { + const resp = await req.getList(pageIndex, pageCap) + return JSON.parse(resp) + } catch (e) { + void Alert.err(`获取随笔列表失败: ${e}`) + return [] + } + } + + export async function getPostCount() { + const req = await getAuthedPostReq() + try { + return await req.getCount() + } catch (e) { + void Alert.err(`获取随笔列表失败: ${e}`) + return 0 + } + } + // TODO: need better impl export async function* allPostIter() { - const result = await PostService.fetchPostList({ pageSize: 1 }) - const postCount = result.matchedPostCount + const postCount = await getPostCount() 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) + const list = await PostService.getPostList(i + 1, 1) + const id = list[0].id + const dto = await PostService.getPostEditDto(id) yield dto.post } } - export async function fetchPostEditDto(postId: number) { - const url = `${getBaseUrl()}/api/posts/${postId}` - + export async function getPostEditDto(postId: number) { + const req = await getAuthedPostReq() try { - const resp = await AuthedReq.get(url, consHeader()) - - const { blogPost, myConfig } = <{ blogPost?: Post; myConfig?: MyConfig }>JSON.parse(resp) + const resp = await req.getOne(postId) // TODO: need better impl + const { blogPost, myConfig } = <{ blogPost?: Post; myConfig?: MyConfig }>JSON.parse(resp) if (blogPost === undefined) throw Error('博文不存在') return { @@ -81,35 +106,25 @@ export namespace PostService { } export async function delPost(...postIds: number[]) { - if (postIds.length === 1) { - const url = `${getBaseUrl()}/api/posts/${postIds[0]}` - try { - await AuthedReq.del(url, consHeader()) - } catch (e) { - void Alert.err(`删除博文失败: ${e}`) - } - } else { - const para = consUrlPara(...postIds.map(id => ['postIds', id.toString()] as [string, string])) - const url = `${getBaseUrl()}/api/bulk-operation/post?${para}` - try { - await AuthedReq.del(url, consHeader()) - } catch (e) { - void Alert.err(`删除博文失败: ${e}`) - } + const req = await getAuthedPostReq() + try { + if (postIds.length === 1) await req.delOne(postIds[0]) + else await req.delSome(new Uint32Array(postIds)) + } catch (e) { + void Alert.err(`删除博文失败: ${e}`) } } export async function updatePost(post: Post) { if (MarkdownCfg.isIgnoreYfmWhenUploadPost()) post.postBody = rmYfm(post.postBody) - const url = `${getBaseUrl()}/api/posts` const body = JSON.stringify(post) - const header = consHeader([ReqHeaderKey.CONTENT_TYPE, ContentType.appJson]) - const resp = await AuthedReq.post(url, header, body) + const req = await getAuthedPostReq() + const resp = await req.update(body) return JSON.parse(resp) } - export async function updatePostListStateNg( + export async function updatePostListState( pageIndex: number, pageCap: number, pageItemCount: number, @@ -126,11 +141,11 @@ export namespace PostService { hasPrev, hasNext, } as PostListState - await globalCtx.storage.update('postListState', finalState) + await LocalState.setState('postListState', finalState) } export async function fetchPostEditTemplate() { - newPostTemplate ??= await fetchPostEditDto(-1) + newPostTemplate ??= await getPostEditDto(-1) if (newPostTemplate === undefined) return undefined return { diff --git a/src/service/site-category.ts b/src/service/site-category.ts deleted file mode 100644 index 02209cbf..00000000 --- a/src/service/site-category.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SiteCategory } from '@/model/site-category' -import { globalCtx } from '@/ctx/global-ctx' -import { AuthedReq } from '@/infra/http/authed-req' -import { consHeader } from '@/infra/http/infra/header' -import { Alert } from '@/infra/alert' - -let cached: SiteCategory[] | null = null - -export namespace SiteCategoryService { - export async function fetchAll(forceRefresh = false) { - if (cached && !forceRefresh) return cached - - const url = `${globalCtx.config.apiBaseUrl}/api/category/site` - try { - const resp = await AuthedReq.get(url, consHeader()) - const list = JSON.parse(resp) - cached = list - return list - } catch (e) { - void Alert.err(`获取随笔分类失败: ${e}`) - return [] - } - } -} diff --git a/src/setup/setup-cmd.ts b/src/setup/setup-cmd.ts index 6ba1dbfd..5c0808e0 100644 --- a/src/setup/setup-cmd.ts +++ b/src/setup/setup-cmd.ts @@ -8,7 +8,6 @@ import { refreshPostCategoryList } from '@/cmd/post-category/refresh-post-catego import { handleUpdatePostCategory } from '@/cmd/post-category/update-post-category' import { openPostInBlogAdmin } from '@/cmd/open/open-post-in-blog-admin' import { viewPostOnline } from '@/cmd/view-post-online' -import { extractImg } from '@/cmd/extract-img' import { handleDeletePostCategories } from '@/cmd/post-category/del-selected-category' import { regCmd } from '@/infra/cmd' import { exportPostToPdf } from '@/cmd/pdf/export-pdf' @@ -19,7 +18,7 @@ import { viewPostBlogExport } from '@/cmd/blog-export/view-post' import { deleteBlogExport } from '@/cmd/blog-export/delete' import { openLocalExport } from '@/cmd/blog-export/open-local' import { refreshExportRecord } from '@/cmd/blog-export/refresh' -import { AccountManagerNg } from '@/auth/account-manager' +import { AuthManager } from '@/auth/auth-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' @@ -38,6 +37,7 @@ 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' +import { extractImg } from '@/cmd/extract-img/extract-img' function withPrefix(prefix: string) { return (rest: string) => `${prefix}${rest}` @@ -49,9 +49,9 @@ export function setupExtCmd() { const tokens = [ // auth - regCmd(withAppName('.login.web'), AccountManagerNg.webLogin), - regCmd(withAppName('.login.pat'), AccountManagerNg.patLogin), - regCmd(withAppName('.logout'), AccountManagerNg.logout), + regCmd(withAppName('.login.web'), AuthManager.webLogin), + regCmd(withAppName('.login.pat'), AuthManager.patLogin), + regCmd(withAppName('.logout'), AuthManager.logout), // post.list-view regCmd(withAppName('.post.list-view.refresh'), PostListView.refresh), regCmd(withAppName('.post.list-view.prev'), PostListView.goPrev), diff --git a/src/tree-view/model/base-entry-tree-item.ts b/src/tree-view/model/base-entry-tree-item.ts index 7794e1b7..deced3c6 100644 --- a/src/tree-view/model/base-entry-tree-item.ts +++ b/src/tree-view/model/base-entry-tree-item.ts @@ -1,4 +1,4 @@ -export interface BaseEntryTreeItem { +export type BaseEntryTreeItem = { readonly getChildren: () => TChildren[] readonly getChildrenAsync: () => Promise } diff --git a/src/tree-view/model/post-category-tree-item.ts b/src/tree-view/model/post-category-tree-item.ts index 590e3532..e977f5be 100644 --- a/src/tree-view/model/post-category-tree-item.ts +++ b/src/tree-view/model/post-category-tree-item.ts @@ -1,4 +1,3 @@ -import { TreeItem } from 'vscode' import { PostCategory } from '@/model/post-category' import { toTreeItem } from '@/tree-view/convert' import { BaseTreeItemSource } from './base-tree-item-source' @@ -7,10 +6,10 @@ import { PostTreeItem } from './post-tree-item' export class PostCategoryTreeItem extends BaseTreeItemSource { constructor( public readonly category: PostCategory, - public children?: (PostCategoryTreeItem | PostTreeItem)[] + public children: (PostCategoryTreeItem | PostTreeItem)[] = [] ) { super() } - toTreeItem = (): TreeItem | Promise => toTreeItem(this.category) + toTreeItem = () => toTreeItem(this.category) } diff --git a/src/tree-view/model/post-metadata.ts b/src/tree-view/model/post-metadata.ts index 1e32431f..15a6f716 100644 --- a/src/tree-view/model/post-metadata.ts +++ b/src/tree-view/model/post-metadata.ts @@ -62,7 +62,7 @@ export abstract class PostMetadata extends BaseTreeItemSource { exclude?: RootPostMetadataType[] }): Promise { let parsedPost = post instanceof PostTreeItem ? post.post : post - const postEditDto = await PostService.fetchPostEditDto(parsedPost.id) + const postEditDto = await PostService.getPostEditDto(parsedPost.id) parsedPost = postEditDto?.post || parsedPost return Promise.all( rootMetadataMap(parsedPost, postEditDto) @@ -132,11 +132,11 @@ export class PostCategoryMetadata extends PostMetadata { } static async parse(parent: Post, editDto?: PostEditDto): Promise { - editDto = editDto ? editDto : await PostService.fetchPostEditDto(parent.id) + editDto = editDto ? editDto : await PostService.getPostEditDto(parent.id) if (editDto == null) return [] const categoryIds = editDto.post.categoryIds ?? [] - const futList = categoryIds.map(PostCategoryService.find) + const futList = categoryIds.map(PostCategoryService.getOne) const categoryList = await Promise.all(futList) return categoryList @@ -172,7 +172,7 @@ export class PostTagMetadata extends PostMetadata { } static async parse(parent: Post, editDto?: PostEditDto): Promise { - editDto = editDto ? editDto : await PostService.fetchPostEditDto(parent.id) + editDto = editDto ? editDto : await PostService.getPostEditDto(parent.id) if (editDto == null) return [] const { diff --git a/src/tree-view/provider/account-view-data-provider.ts b/src/tree-view/provider/account-view-data-provider.ts index 2fe7cd75..acd6fbb8 100644 --- a/src/tree-view/provider/account-view-data-provider.ts +++ b/src/tree-view/provider/account-view-data-provider.ts @@ -1,4 +1,4 @@ -import { accountManager } from '@/auth/account-manager' +import { AuthManager } from '@/auth/auth-manager' import { EventEmitter, ProviderResult, ThemeIcon, TreeDataProvider, TreeItem } from 'vscode' export class AccountViewDataProvider implements TreeDataProvider { @@ -13,9 +13,9 @@ export class AccountViewDataProvider implements TreeDataProvider { } getChildren(element?: TreeItem): ProviderResult { - if (!accountManager.isAuthorized || element) return [] + if (!AuthManager.isAuthed || element) return [] - const userName = accountManager.currentUser?.userInfo.DisplayName + const userName = AuthManager.getUserInfo()?.DisplayName return [ { label: userName, tooltip: '用户名', iconPath: new ThemeIcon('account') }, { diff --git a/src/tree-view/provider/post-category-tree-data-provider.ts b/src/tree-view/provider/post-category-tree-data-provider.ts index 3941847d..23385b4f 100644 --- a/src/tree-view/provider/post-category-tree-data-provider.ts +++ b/src/tree-view/provider/post-category-tree-data-provider.ts @@ -10,7 +10,6 @@ import { PostEntryMetadata, PostMetadata, RootPostMetadataType } from '@/tree-vi import { PostTreeItem } from '@/tree-view/model/post-tree-item' import { Alert } from '@/infra/alert' import { execCmd } from '@/infra/cmd' -import { PostCategory } from '@/model/post-category' export class PostCategoryTreeDataProvider implements TreeDataProvider { private _treeDataChanged = new EventEmitter() @@ -51,19 +50,20 @@ export class PostCategoryTreeDataProvider implements TreeDataProvider { - if (!this.isRefreshing) { - if (parent == null) { - return this.getCategories() - } else if (parent instanceof PostCategoryTreeItem) { - return Promise.all([this.getCategories(parent.category.categoryId), this.getPost(parent)]).then( - ([childCategories, childPost]) => (parent.children = [...childCategories, ...childPost]) - ) - } else if (parent instanceof PostTreeItem) { - return this.getPostMetadataChildren(parent) - } else if (parent instanceof PostEntryMetadata) { - return parent.getChildrenAsync() - } + getChildren(item?: PostCategoriesListTreeItem): ProviderResult { + if (this.isRefreshing) return Promise.resolve([]) + + if (item === undefined) { + return PostCategoryService.getAll().then(list => list.map(c => new PostCategoryTreeItem(c))) + } else if (item instanceof PostCategoryTreeItem) { + const categoryId = item.category.categoryId + return Promise.all([this.getCategories(categoryId), this.getPost(item)]).then( + ([childCategories, childPost]) => (item.children = [...childCategories, ...childPost]) + ) + } else if (item instanceof PostTreeItem) { + return this.getPostMetadataChildren(item) + } else if (item instanceof PostEntryMetadata) { + return item.getChildrenAsync() } return Promise.resolve([]) @@ -84,14 +84,14 @@ export class PostCategoryTreeDataProvider implements TreeDataProvider postIds.includes(x.post.id)) const categories = new Set() postTreeItems.forEach(treeItem => { - if (treeItem.parent) { - if (refreshPost) treeItem.parent.children = undefined - else this.fireTreeDataChangedEvent(treeItem) - - if (!categories.has(treeItem.parent)) { - categories.add(treeItem.parent) - this.fireTreeDataChangedEvent(treeItem.parent) - } + if (treeItem.parent === undefined) return + + if (refreshPost) treeItem.parent.children = [] + else this.fireTreeDataChangedEvent(treeItem) + + if (!categories.has(treeItem.parent)) { + categories.add(treeItem.parent) + this.fireTreeDataChangedEvent(treeItem.parent) } }) } @@ -116,21 +116,17 @@ export class PostCategoryTreeDataProvider implements TreeDataProvider new PostCategoryTreeItem(x)) } catch (e) { - void Alert.err(`获取博文分类失败: ${(e).message}`) - } finally { + void Alert.err(`获取博文分类失败: ${e}`) await this.setIsRefreshing(false) + throw e } - - return categories.map(x => new PostCategoryTreeItem(x)) } } diff --git a/src/tree-view/provider/post-data-provider.ts b/src/tree-view/provider/post-data-provider.ts index 6dc60f32..78f5c994 100644 --- a/src/tree-view/provider/post-data-provider.ts +++ b/src/tree-view/provider/post-data-provider.ts @@ -53,19 +53,23 @@ export class PostDataProvider implements TreeDataProvider { } async loadPost() { - const { pageIndex } = PostService.getPostListState() ?? {} - const pageSize = PostListCfg.getPostListPageSize() + const { pageIndex } = PostService.getPostListState() + const pageCap = PostListCfg.getPostListPageSize() try { - const result = await PostService.fetchPostList({ pageIndex, pageSize }) - // TODO: need better design - this.page = result.page + const result = await PostService.getPostList(pageIndex, pageCap) + this.page = { + index: pageIndex, + cap: pageCap, + // TODO: need better design + items: result.map(it => Object.assign(new Post(), it)), + } this.fireTreeDataChangedEvent(undefined) - return result + return this.page } catch (e) { - void Alert.err(`加载博文失败\n${JSON.stringify(e)}`) + void Alert.err(`加载博文失败: ${e}`) throw e } } diff --git a/ui/global.d.ts b/ui/global.d.ts index 1e24e4ed..318042c5 100644 --- a/ui/global.d.ts +++ b/ui/global.d.ts @@ -1,14 +1,7 @@ type WebviewCommonCmd = import('@/model/webview-cmd').WebviewCommonCmd -declare interface VsCodeApi { +declare type VsCodeApi = { postMessage = WebviewCommonCmd<{}>>(message: Object | T): any } -declare interface Window { - addEventListener>( - type: 'message', - callback: (event: { data: TCmd }) => unknown - ): void -} - declare function acquireVsCodeApi(): VsCodeApi diff --git a/ui/ing/App.tsx b/ui/ing/App.tsx index 4eca81ea..045ebcc2 100644 --- a/ui/ing/App.tsx +++ b/ui/ing/App.tsx @@ -4,7 +4,7 @@ import { IngList } from 'ing/IngList' import { getVsCodeApiSingleton } from 'share/vscode-api' import { IngAppState } from '@/model/ing-view' import { Ing, IngComment } from '@/model/ing' -import { activeThemeProvider } from 'share/active-theme-provider' +import { ActiveThemeProvider } from 'share/active-theme-provider' import { ThemeProvider } from '@fluentui/react/lib/Theme' import { Spinner, Stack } from '@fluentui/react' import { cloneWith } from 'lodash-es' @@ -14,7 +14,7 @@ export class App extends Component { super(props) this.state = { - theme: activeThemeProvider.activeTheme(), + theme: ActiveThemeProvider.activeTheme(), isRefreshing: false, } @@ -24,7 +24,7 @@ export class App extends Component { override componentDidMount(): void { console.debug('IngApp mounted') - this.setState({ theme: activeThemeProvider.activeTheme() }) + this.setState({ theme: ActiveThemeProvider.activeTheme() }) } render(): ReactNode { @@ -67,14 +67,15 @@ export class App extends Component { return } if (command === Webview.Cmd.Ing.Ui.updateTheme) { - this.setState({ theme: activeThemeProvider.activeTheme() }) + this.setState({ theme: ActiveThemeProvider.activeTheme() }) return } }) } private refresh() { - getVsCodeApiSingleton().postMessage({ + const vscodeApi = getVsCodeApiSingleton() + vscodeApi.postMessage({ command: Webview.Cmd.Ing.Ext.refreshingList, payload: {}, }) diff --git a/ui/ing/IngItem.tsx b/ui/ing/IngItem.tsx index 59c51f7d..6c330969 100644 --- a/ui/ing/IngItem.tsx +++ b/ui/ing/IngItem.tsx @@ -13,7 +13,7 @@ interface IngItemProps { comments?: IngComment[] } -class IngItem extends Component { +export class IngItem extends Component { readonly icons = { vscodeLogo: ( @@ -189,12 +189,8 @@ class IngItem extends Component { ) private renderSendFromIcon(value: IngSendFromType) { - switch (value) { - case IngSendFromType.code: - return this.icons.vscodeLogo - case IngSendFromType.cellPhone: - return this.icons.mobile - } + if (value === IngSendFromType.code) return this.icons.vscodeLogo + if (value === IngSendFromType.cellPhone) return this.icons.mobile } private comment(payload: Webview.Cmd.Ing.CommentCmdPayload) { @@ -204,5 +200,3 @@ class IngItem extends Component { } as IngWebviewHostCmd) } } - -export { IngItem } diff --git a/ui/ing/IngList.tsx b/ui/ing/IngList.tsx index 312b9db8..f39b1030 100644 --- a/ui/ing/IngList.tsx +++ b/ui/ing/IngList.tsx @@ -8,7 +8,7 @@ interface IngListProps { comments: Record } -class IngList extends Component { +export class IngList extends Component { constructor(props: IngListProps) { super(props) } @@ -24,7 +24,7 @@ class IngList extends Component { } private renderItems() { - const { comments } = this.props + const comments = this.props.comments return this.props.ingList.map(ing => ( @@ -32,5 +32,3 @@ class IngList extends Component { )) } } - -export { IngList } diff --git a/ui/ing/index.html b/ui/ing/index.html index 517c892f..e923da9e 100644 --- a/ui/ing/index.html +++ b/ui/ing/index.html @@ -9,6 +9,7 @@ content="width=device-width, initial-scale=1.0"> + diff --git a/ui/post-cfg/App.tsx b/ui/post-cfg/App.tsx index 245db6e2..23144767 100644 --- a/ui/post-cfg/App.tsx +++ b/ui/post-cfg/App.tsx @@ -3,13 +3,13 @@ import { ThemeProvider } from '@fluentui/react/lib/Theme' import { Breadcrumb, IBreadcrumbItem, initializeIcons, PartialTheme, Spinner, Stack, Theme } from '@fluentui/react' import { PostForm } from './components/PostForm' import { Post } from '@/model/post' -import { personalCategoriesStore } from './service/personal-category-store' -import { siteCategoriesStore } from './service/site-category-store' -import { tagsStore } from './service/tags-store' +import { PersonalCategoryStore } from './service/personal-category-store' +import { SiteCategoryStore } from './service/site-category-store' +import { TagStore } from './service/tag-store' import { WebviewMsg } from '@/model/webview-msg' import { Webview } from '@/model/webview-cmd' import { PostFormContextProvider } from './components/PostFormContextProvider' -import { activeThemeProvider } from 'share/active-theme-provider' +import { ActiveThemeProvider } from 'share/active-theme-provider' import { darkTheme, lightTheme } from 'share/theme' import { getVsCodeApiSingleton } from 'share/vscode-api' @@ -23,10 +23,10 @@ interface AppState { export interface AppProps extends Record {} -class App extends Component { +export class App extends Component { constructor(props: AppProps) { super(props) - this.state = { theme: activeThemeProvider.activeTheme(), fileName: '', useNestCategoriesSelect: false } + this.state = { theme: ActiveThemeProvider.activeTheme(), fileName: '', useNestCategoriesSelect: false } this.observerMessages() getVsCodeApiSingleton().postMessage({ command: Webview.Cmd.Ext.refreshPost }) } @@ -73,7 +73,7 @@ class App extends Component { } private renderBreadcrumbs() { - const { breadcrumbs } = this.state + const breadcrumbs = this.state.breadcrumbs if (!breadcrumbs || breadcrumbs.length <= 0) return <> const items = breadcrumbs.map(breadcrumb => ({ text: breadcrumb, key: breadcrumb }) as IBreadcrumbItem) @@ -83,16 +83,16 @@ class App extends Component { private observerMessages() { window.addEventListener('message', ev => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { command } = ev.data + const command = ev.data.command // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const message = ev.data as any + const message = ev.data if (command === Webview.Cmd.Ui.editPostCfg) { const { post, activeTheme, personalCategories, siteCategories, tags, breadcrumbs, fileName } = message as WebviewMsg.EditPostCfgMsg - personalCategoriesStore.set(personalCategories) - siteCategoriesStore.set(siteCategories) - tagsStore.set(tags) + PersonalCategoryStore.set(personalCategories) + SiteCategoryStore.set(siteCategories) + TagStore.set(tags) this.setState({ theme: (activeTheme as number) === 2 ? darkTheme : lightTheme, @@ -108,10 +108,8 @@ class App extends Component { const { baseUrl } = message as WebviewMsg.SetFluentIconBaseUrlMsg initializeIcons(baseUrl) } else if (command === Webview.Cmd.Ui.updateTheme) { - this.setState({ theme: activeThemeProvider.activeTheme() }) + this.setState({ theme: ActiveThemeProvider.activeTheme() }) } }) } } - -export { App } diff --git a/ui/post-cfg/components/AccessPermissionSelector.tsx b/ui/post-cfg/components/AccessPermissionSelector.tsx index c3f84394..5fc6a8f7 100644 --- a/ui/post-cfg/components/AccessPermissionSelector.tsx +++ b/ui/post-cfg/components/AccessPermissionSelector.tsx @@ -2,35 +2,35 @@ import { ChoiceGroup, IChoiceGroupOption, Label, Stack } from '@fluentui/react' import { AccessPermission, formatAccessPermission } from '@/model/post' import React from 'react' -export interface IAccessPermissionSelectorProps { +export type IAccessPermissionSelectorProps = { accessPermission?: AccessPermission onChange?: (accessPermission: AccessPermission) => void } export interface IAccessPermissionSelectorState extends Record {} +const options: IChoiceGroupOption[] = [ + { + text: formatAccessPermission(AccessPermission.undeclared), + value: AccessPermission.undeclared, + key: AccessPermission.undeclared.toString(), + }, + { + text: formatAccessPermission(AccessPermission.authenticated), + value: AccessPermission.authenticated, + key: AccessPermission.authenticated.toString(), + }, + { + text: formatAccessPermission(AccessPermission.owner), + value: AccessPermission.owner, + key: AccessPermission.owner.toString(), + }, +] + export class AccessPermissionSelector extends React.Component< IAccessPermissionSelectorProps, IAccessPermissionSelectorState > { - private options: IChoiceGroupOption[] = [ - { - text: formatAccessPermission(AccessPermission.undeclared), - value: AccessPermission.undeclared, - key: AccessPermission.undeclared.toString(), - }, - { - text: formatAccessPermission(AccessPermission.authenticated), - value: AccessPermission.authenticated, - key: AccessPermission.authenticated.toString(), - }, - { - text: formatAccessPermission(AccessPermission.owner), - value: AccessPermission.owner, - key: AccessPermission.owner.toString(), - }, - ] - constructor(props: IAccessPermissionSelectorProps) { props.accessPermission ??= AccessPermission.undeclared super(props) @@ -43,7 +43,7 @@ export class AccessPermissionSelector extends React.Component< this.props.onChange?.apply(this, [option?.value ?? AccessPermission.undeclared]) } diff --git a/ui/post-cfg/components/CategorySelect.tsx b/ui/post-cfg/components/CategorySelect.tsx index 90ac2c5c..19e5b8bf 100644 --- a/ui/post-cfg/components/CategorySelect.tsx +++ b/ui/post-cfg/components/CategorySelect.tsx @@ -1,7 +1,8 @@ import { Checkbox, Stack } from '@fluentui/react' import { Component } from 'react' -import { personalCategoriesStore } from '../service/personal-category-store' +import { PersonalCategoryStore } from '../service/personal-category-store' import { PostCategory } from '@/model/post-category' +import { eq } from '../../../src/infra/fp/ord' interface CategoriesSelectorProps { categoryIds: number[] | undefined @@ -16,11 +17,12 @@ interface CategoriesSelectorState { class CategorySelect extends Component { constructor(props: CategoriesSelectorProps) { super(props) - this.state = { categories: personalCategoriesStore.get(), categoryIds: props.categoryIds ?? [] } + this.state = { categories: PersonalCategoryStore.get(), categoryIds: props.categoryIds ?? [] } } render() { - const { categories, categoryIds } = this.state + const categories = this.state.categories + const categoryIds = this.state.categoryIds const items = categories.map(category => ( x === categoryId) + const position = categoryIds.findIndex(eq(categoryId)) const isInclude = position >= 0 - switch (isChecked) { - case true: - if (!isInclude) categoryIds.push(categoryId) - - break - default: - if (isInclude) categoryIds.splice(position, 1) - } + + if (isChecked && !isInclude) categoryIds.push(categoryId) + else if (isInclude) categoryIds.splice(position, 1) + this.props.onChange?.apply(this, [categoryIds]) } } diff --git a/ui/post-cfg/components/CommonOptions.tsx b/ui/post-cfg/components/CommonOptions.tsx index 1ff9499a..f6087c72 100644 --- a/ui/post-cfg/components/CommonOptions.tsx +++ b/ui/post-cfg/components/CommonOptions.tsx @@ -3,7 +3,7 @@ import * as React from 'react' type Option = { [key: string]: { label: string; checked: boolean } } -export interface ICommonOptionsProps { +export type ICommonOptionsProps = { options: TOption onChange?: (optionKey: keyof TOption, checked: boolean, stateObj: { [p in typeof optionKey]: boolean }) => void } @@ -18,7 +18,7 @@ export class CommonOptions extends React.Compon render() { return ( - + {this.renderOptions()} diff --git a/ui/post-cfg/components/ErrorResponse.tsx b/ui/post-cfg/components/ErrorResponse.tsx index c6b74efa..2ef993fa 100644 --- a/ui/post-cfg/components/ErrorResponse.tsx +++ b/ui/post-cfg/components/ErrorResponse.tsx @@ -7,7 +7,7 @@ import { PostFormContext } from './PostFormContext' export interface IErrorResponseProps extends Record {} -export interface IErrorResponseState { +export type IErrorResponseState = { errors: string[] } @@ -22,7 +22,9 @@ export class ErrorResponse extends React.Component { - const { command, errorResponse } = (msg.data ?? {}) as any as Optional + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const data = msg.data ?? {} + const { command, errorResponse } = data as Optional if (command === Webview.Cmd.Ui.showErrorResponse) { this.setState({ errors: errorResponse.errors ?? [] }, () => this.reveal()) this.context.set({ disabled: false, status: '' }) @@ -35,7 +37,7 @@ export class ErrorResponse extends React.Component this.elementId = `errorResponse ${Date.now()}` diff --git a/ui/post-cfg/components/InputSummary.tsx b/ui/post-cfg/components/InputSummary.tsx index fbf87110..f3e8cb7b 100644 --- a/ui/post-cfg/components/InputSummary.tsx +++ b/ui/post-cfg/components/InputSummary.tsx @@ -5,14 +5,14 @@ import { WebviewMsg } from '@/model/webview-msg' import React from 'react' import { getVsCodeApiSingleton } from 'share/vscode-api' -export interface IInputSummaryProps { +export type IInputSummaryProps = { summary?: string featureImageUrl?: string onChange?: (summary: string) => void onFeatureImageChange?: (imageUrl: string) => void } -export interface IInputSummaryState { +export type IInputSummaryState = { isCollapse: boolean disabled: boolean errors?: string[] @@ -20,6 +20,7 @@ export interface IInputSummaryState { export class InputSummary extends React.Component { private uploadingImageId = '' + constructor(props: IInputSummaryProps) { super(props) const { featureImageUrl, summary } = props @@ -29,8 +30,8 @@ export class InputSummary extends React.Component @@ -103,18 +104,13 @@ export class InputSummary extends React.Component) => { const data = ev.data as WebviewMsg.Msg - const { command } = data - switch (command) { - case Webview.Cmd.Ui.updateImageUploadStatus: - { - const { imageId, status } = data as WebviewMsg.UpdateImgUpdateStatusMsg - if (imageId === this.uploadingImageId) { - this.setState({ disabled: status.id === ImgUploadStatusId.uploading }) - if (status.id === ImgUploadStatusId.uploaded) - this.props.onFeatureImageChange?.apply(this, [status.imageUrl ?? '']) - } - } - break + if (data.command === Webview.Cmd.Ui.updateImageUploadStatus) { + const { imageId, status } = data as WebviewMsg.UpdateImgUpdateStatusMsg + if (imageId === this.uploadingImageId) { + this.setState({ disabled: status.id === ImgUploadStatusId.uploading }) + if (status.id === ImgUploadStatusId.uploaded) + this.props.onFeatureImageChange?.apply(this, [status.imageUrl ?? '']) + } } } @@ -128,7 +124,7 @@ export class InputSummary extends React.Component this.uploadFeatureImage()} diff --git a/ui/post-cfg/components/NestCategorySelect.tsx b/ui/post-cfg/components/NestCategorySelect.tsx index 7f2138f7..19caef61 100644 --- a/ui/post-cfg/components/NestCategorySelect.tsx +++ b/ui/post-cfg/components/NestCategorySelect.tsx @@ -1,17 +1,17 @@ import { ActionButton, Checkbox, Icon, Link, Spinner, Stack } from '@fluentui/react' import { PostCategory } from '@/model/post-category' import { take } from 'lodash-es' -import { personalCategoriesStore } from 'post-cfg/service/personal-category-store' +import { PersonalCategoryStore } from 'post-cfg/service/personal-category-store' import React from 'react' -export interface INestCategoriesSelectProps { +export type INestCategoriesSelectProps = { selected?: number[] parent?: number | null onSelect?: (value: number[]) => void level?: number } -export interface INestCategoriesSelectState { +export type INestCategoriesSelectState = { expanded?: Set | null children?: PostCategory[] showAll?: boolean @@ -36,14 +36,13 @@ export default class NestCategorySelect extends React.Component< render() { if (this.props.parent && !this.state.children) { - personalCategoriesStore - .getByParent(this.props.parent) + PersonalCategoryStore.getByParent(this.props.parent) .then(v => this.setState({ children: v })) .catch(console.warn) return } - const categories = this.isRoot ? personalCategoriesStore.get() : this.state.children ?? [] + const categories = this.isRoot ? PersonalCategoryStore.get() : this.state.children ?? [] return ( {(this.state.showAll ? categories : take(categories, this.state.limit)).map(c => ( @@ -137,7 +136,7 @@ export default class NestCategorySelect extends React.Component< } } -export interface ICategoryItemProps { +export type ICategoryItemProps = { category: PostCategory onChange: (isChecked: boolean) => void isChecked: boolean diff --git a/ui/post-cfg/components/PasswordInput.tsx b/ui/post-cfg/components/PasswordInput.tsx index 8a381723..dc81d23a 100644 --- a/ui/post-cfg/components/PasswordInput.tsx +++ b/ui/post-cfg/components/PasswordInput.tsx @@ -1,7 +1,7 @@ import { Label, Stack, TextField } from '@fluentui/react' import React from 'react' -export interface IPasswordInputProps { +export type IPasswordInputProps = { password?: string onChange?: (password: string) => void } diff --git a/ui/post-cfg/components/PostEntryNameInput.tsx b/ui/post-cfg/components/PostEntryNameInput.tsx index 8aac5401..7d2ee532 100644 --- a/ui/post-cfg/components/PostEntryNameInput.tsx +++ b/ui/post-cfg/components/PostEntryNameInput.tsx @@ -1,17 +1,18 @@ import { ActionButton, ITextField, Label, Stack, TextField } from '@fluentui/react' import * as React from 'react' -export interface IPostEntryNameInputProps { +export type IPostEntryNameInputProps = { entryName?: string onChange?: (value: string) => void } -export interface IPostEntryNameInputState { +export type IPostEntryNameInputState = { isCollapsed: boolean } export default class PostEntryNameInput extends React.Component { textFieldComp?: ITextField | null + constructor(props: IPostEntryNameInputProps) { super(props) @@ -61,7 +62,7 @@ export default class PostEntryNameInput extends React.Component { this.textFieldComp = input - if (this.state.isCollapsed === false) this.textFieldComp?.focus() + if (!this.state.isCollapsed) this.textFieldComp?.focus() }} value={this.props.entryName} onChange={(_, value) => this.props.onChange?.call(this, value)} diff --git a/ui/post-cfg/components/PostForm.tsx b/ui/post-cfg/components/PostForm.tsx index 0050c159..d6b7b2ea 100644 --- a/ui/post-cfg/components/PostForm.tsx +++ b/ui/post-cfg/components/PostForm.tsx @@ -21,7 +21,7 @@ import PostTitleInput from 'post-cfg/components/PostTitleInput' import NestCategorySelect from './NestCategorySelect' import { Post } from '@/model/post' -export interface IPostFormProps { +export type IPostFormProps = { post?: Post fileName?: string useNestCategoriesSelect: boolean @@ -72,6 +72,7 @@ export class PostForm extends React.Component { this.setState({ tags })} /> + this.setState({ tags })} /> { diff --git a/ui/post-cfg/components/PostFormContext.ts b/ui/post-cfg/components/PostFormContext.ts index c06c0b2f..35641b41 100644 --- a/ui/post-cfg/components/PostFormContext.ts +++ b/ui/post-cfg/components/PostFormContext.ts @@ -1,6 +1,6 @@ import React from 'react' -export interface IPostFormContext { +export type IPostFormContext = { disabled: boolean status: 'loading' | 'submitting' | '' set(v: Omit): void diff --git a/ui/post-cfg/components/PostFormContextProvider.tsx b/ui/post-cfg/components/PostFormContextProvider.tsx index 989c615f..cb6f99b6 100644 --- a/ui/post-cfg/components/PostFormContextProvider.tsx +++ b/ui/post-cfg/components/PostFormContextProvider.tsx @@ -2,12 +2,12 @@ import * as React from 'react' import { PostFormContext, IPostFormContext, defaultPostFormContext } from './PostFormContext' import { ReactNode } from 'react' -export interface IPostFormContextProviderProps { +export type IPostFormContextProviderProps = { value?: Partial children: ReactNode } -export interface IPostFormContextProviderState { +export type IPostFormContextProviderState = { value: IPostFormContext } diff --git a/ui/post-cfg/components/PostTitleInput.tsx b/ui/post-cfg/components/PostTitleInput.tsx index d25f5147..f5c45f15 100644 --- a/ui/post-cfg/components/PostTitleInput.tsx +++ b/ui/post-cfg/components/PostTitleInput.tsx @@ -1,13 +1,13 @@ import { ActionButton, Label, Stack, Text, TextField } from '@fluentui/react' import React from 'react' -export interface IPostTitleInputProps { +export type IPostTitleInputProps = { value: string fileName: string onChange: (value: string | null | undefined) => unknown } -export interface IPostTitleInputState { +export type IPostTitleInputState = { value: IPostTitleInputProps['value'] } diff --git a/ui/post-cfg/components/SiteCategorySelector.tsx b/ui/post-cfg/components/SiteCategorySelector.tsx index d2775359..14745a0e 100644 --- a/ui/post-cfg/components/SiteCategorySelector.tsx +++ b/ui/post-cfg/components/SiteCategorySelector.tsx @@ -1,14 +1,14 @@ import { ActionButton, Checkbox, Label, Stack } from '@fluentui/react' import { SiteCategory } from '@/model/site-category' import React from 'react' -import { siteCategoriesStore } from '../service/site-category-store' +import { SiteCategoryStore } from '../service/site-category-store' -export interface ISiteCategoriesSelectorProps { +export type ISiteCategoriesSelectorProps = { categoryIds?: number[] onChange?: (siteCategoryId: number) => void } -export interface ISiteCategoriesSelectorState { +export type ISiteCategoriesSelectorState = { siteCategories: SiteCategory[] isCollapsed: boolean categoryIds: number[] @@ -19,7 +19,7 @@ export class SiteCategorySelector extends React.Component 0) { @@ -37,7 +37,8 @@ export class SiteCategorySelector extends React.Component { const { children, id: parentId } = parent const { categoryExpandState } = this.state diff --git a/ui/post-cfg/components/SiteHomeContributionOptionsSelector.tsx b/ui/post-cfg/components/SiteHomeContributionOptionsSelector.tsx index 565d0abb..1ff2a824 100644 --- a/ui/post-cfg/components/SiteHomeContributionOptionsSelector.tsx +++ b/ui/post-cfg/components/SiteHomeContributionOptionsSelector.tsx @@ -9,7 +9,7 @@ export interface ISiteHomeContributionOptionsSelectorProps extends ISiteHomeCont onInSiteCandidateChange: (value: boolean) => void } -export interface ISiteHomeContributionOptionsSelector { +export type ISiteHomeContributionOptionsSelector = { isCollapse: boolean } diff --git a/ui/post-cfg/components/TagsInput.tsx b/ui/post-cfg/components/TagsInput.tsx index 4b8b5b9e..d4016739 100644 --- a/ui/post-cfg/components/TagsInput.tsx +++ b/ui/post-cfg/components/TagsInput.tsx @@ -11,15 +11,15 @@ import { Text, } from '@fluentui/react' import React from 'react' -import { tagsStore } from '../service/tags-store' +import { TagStore } from '../service/tag-store' import { PostTags, PostTag } from '@/model/post-tag' -export interface ITagsInputProps { +export type ITagsInputProps = { selectedTagNames?: string[] onChange?: (tagNames: string[]) => void } -export interface ITagsInputState { +export type ITagsInputState = { tags: PostTags selectedTags: ITag[] } @@ -32,7 +32,7 @@ export class TagsInput extends React.Component constructor(props: ITagsInputProps) { super(props) - const tags = tagsStore.get() + const tags = TagStore.get() this.state = { tags, selectedTags: @@ -58,7 +58,7 @@ export class TagsInput extends React.Component loadingText: '加载中', }} onRenderSuggestionsItem={tag => { - const isNewTag = (tag as INewTag).isNew === true + const isNewTag = (tag as INewTag).isNew const tagEl = ( diff --git a/ui/post-cfg/model/site-home-contribution-options.ts b/ui/post-cfg/model/site-home-contribution-options.ts index d9c38d95..bb00b61a 100644 --- a/ui/post-cfg/model/site-home-contribution-options.ts +++ b/ui/post-cfg/model/site-home-contribution-options.ts @@ -1,6 +1,4 @@ -interface SiteHomeContributionOptions { +export type SiteHomeContributionOptions = { inSiteCandidate: boolean inSiteHome: boolean } - -export { SiteHomeContributionOptions } diff --git a/ui/post-cfg/service/personal-category-store.ts b/ui/post-cfg/service/personal-category-store.ts index f45f8986..6065af77 100644 --- a/ui/post-cfg/service/personal-category-store.ts +++ b/ui/post-cfg/service/personal-category-store.ts @@ -4,24 +4,28 @@ import { PostCategory } from '@/model/post-category' let children: Map let pendingChildrenQuery: Map> | undefined | null +let items: PostCategory[] = [] -export namespace personalCategoriesStore { - let items: PostCategory[] = [] - export const get = (): PostCategory[] => items ?? [] +export namespace PersonalCategoryStore { + export const get = () => items + + export function set(value: PostCategory[]) { + items = value + } export const getByParent = async (parent: number): Promise => { children ??= new Map() let result = children.get(parent) const vscode = getVsCodeApiSingleton() - if (!result) { + if (result === undefined) { let promise = pendingChildrenQuery?.get(parent) if (promise == null) { promise = new Promise(resolve => { const timeoutId = setTimeout(() => { clearTimeout(timeoutId) window.removeEventListener('message', onUpdate) - console.warn(`timeout: personalCategoriesStore.getByParent: parent: ${parent}`) + console.warn(`timeout: PersonalCategoryStore.getByParent: parent: ${parent}`) resolve([]) }, 30 * 1000) @@ -40,10 +44,7 @@ export namespace personalCategoriesStore { } } - window.addEventListener>( - 'message', - onUpdate - ) + window.addEventListener('message', onUpdate) }).finally(() => pendingChildrenQuery?.delete(parent)) vscode.postMessage>({ @@ -57,6 +58,4 @@ export namespace personalCategoriesStore { return result } - - export const set = (value: PostCategory[]) => (items = value ?? []) } diff --git a/ui/post-cfg/service/site-category-store.ts b/ui/post-cfg/service/site-category-store.ts index 77446c92..007bd6fe 100644 --- a/ui/post-cfg/service/site-category-store.ts +++ b/ui/post-cfg/service/site-category-store.ts @@ -1,7 +1,11 @@ import { SiteCategory } from '@/model/site-category' -export namespace siteCategoriesStore { - let items: SiteCategory[] = [] - export const get = (): SiteCategory[] => items ?? [] - export const set = (value: SiteCategory[]) => (items = value ?? []) +let items: SiteCategory[] = [] + +export namespace SiteCategoryStore { + export const get = () => items + + export function set(value: SiteCategory[]) { + items = value + } } diff --git a/ui/post-cfg/service/tag-store.ts b/ui/post-cfg/service/tag-store.ts new file mode 100644 index 00000000..14a42631 --- /dev/null +++ b/ui/post-cfg/service/tag-store.ts @@ -0,0 +1,11 @@ +import { PostTag } from '@/model/post-tag' + +let tags: PostTag[] = [] + +export namespace TagStore { + export const get = () => tags + + export function set(value: PostTag[]) { + tags = value + } +} diff --git a/ui/post-cfg/service/tags-store.ts b/ui/post-cfg/service/tags-store.ts deleted file mode 100644 index 71e45909..00000000 --- a/ui/post-cfg/service/tags-store.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { PostTag } from '@/model/post-tag' - -export namespace tagsStore { - let tags: PostTag[] = [] - - export const set = (value: PostTag[]) => (tags = value ?? []) - export const get = (): PostTag[] => tags ?? [] -} diff --git a/ui/share/active-theme-provider.ts b/ui/share/active-theme-provider.ts index bdc8dce5..78552eed 100644 --- a/ui/share/active-theme-provider.ts +++ b/ui/share/active-theme-provider.ts @@ -1,5 +1,8 @@ import { darkTheme, lightTheme } from 'share/theme' -export namespace activeThemeProvider { - export const activeTheme = () => (document.body.classList.contains('vscode-dark') ? darkTheme : lightTheme) +export namespace ActiveThemeProvider { + export function activeTheme() { + if (document.body.classList.contains('vscode-dark')) return darkTheme + else return lightTheme + } } diff --git a/ui/share/theme.ts b/ui/share/theme.ts index 5dabd3e1..fb6df1ab 100644 --- a/ui/share/theme.ts +++ b/ui/share/theme.ts @@ -26,9 +26,9 @@ const accent: PartialTheme = { }, } -const lightTheme: PartialTheme = accent +export const lightTheme: PartialTheme = accent -const darkTheme: PartialTheme = { +export const darkTheme: PartialTheme = { palette: Object.assign( { neutralLighterAlt: '#282828', @@ -60,5 +60,3 @@ const darkTheme: PartialTheme = { semanticColors: accent.semanticColors, defaultFontStyle: accent.defaultFontStyle, } - -export { lightTheme, darkTheme }