diff --git a/.eslintrc.json b/.eslintrc.json index d6f61e00..6e160168 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -15,6 +15,15 @@ "endOfLine": "auto", "semi": false } + ], + "@typescript-eslint/strict-boolean-expressions": [ + "error", + { + "allowString": false, + "allowNumber": false, + "allowNullableObject": false, + "allowNullableBoolean": false + } ] }, "ignorePatterns": [ diff --git a/package.json b/package.json index 59a10a83..ea5a1fb8 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ "enablement": "vscode-cnb.isAuthed" }, { - "command": "vscode-cnb.post.create-local", + "command": "vscode-cnb.post.create", "title": "新建博文", "icon": "$(new-file)", "category": "Cnblogs Local Draft" @@ -855,7 +855,7 @@ "group": "navigation@6" }, { - "command": "vscode-cnb.post.create-local", + "command": "vscode-cnb.post.create", "when": "view == cnblogs-post-list || view == cnblogs-post-list-another", "group": "navigation@7" }, @@ -1059,7 +1059,7 @@ "when": "viewItem =~ /^cnb-post/ && viewItem != cnb-post-category" }, { - "command": "vscode-cnb.post.create-local", + "command": "vscode-cnb.post.create", "when": "viewItem == cnb-local-posts-folder" }, { diff --git a/rs/Cargo.lock b/rs/Cargo.lock index 00038553..0077b2f7 100644 --- a/rs/Cargo.lock +++ b/rs/Cargo.lock @@ -826,6 +826,7 @@ dependencies = [ "base64url", "console_error_panic_hook", "getrandom", + "lazy_static", "rand", "regex", "reqwest", diff --git a/rs/Cargo.toml b/rs/Cargo.toml index 7adb9c45..bf377afd 100644 --- a/rs/Cargo.toml +++ b/rs/Cargo.toml @@ -30,3 +30,5 @@ serde-wasm-bindgen = "0.5.0" serde_with = "3.1.0" reqwest = { version = "0.11.16", features = ["json"] } + +lazy_static = "1.4.0" diff --git a/rs/src/cnb/ing/mod.rs b/rs/src/cnb/ing/mod.rs index 992bf2c2..0aadae7b 100644 --- a/rs/src/cnb/ing/mod.rs +++ b/rs/src/cnb/ing/mod.rs @@ -1,10 +1,12 @@ mod comment; mod get_comment; mod get_list; -mod r#pub; +mod publish; use crate::panic_hook; use alloc::string::{String, ToString}; +use lazy_static::lazy_static; +use regex::Regex; use wasm_bindgen::prelude::*; #[wasm_bindgen(js_name = IngReq)] @@ -24,3 +26,13 @@ impl IngReq { } } } + +#[wasm_bindgen(js_name = ingStarIconToText)] +pub fn ing_star_tag_to_text(icon: &str) -> String { + lazy_static! { + static ref REGEX: Regex = Regex::new(r#""#).unwrap(); + } + let caps = REGEX.captures(icon).unwrap(); + let star_text = caps.get(1).unwrap().as_str(); + star_text.to_string() +} diff --git a/rs/src/cnb/ing/pub.rs b/rs/src/cnb/ing/publish.rs similarity index 87% rename from rs/src/cnb/ing/pub.rs rename to rs/src/cnb/ing/publish.rs index 7b8852db..b5d13992 100644 --- a/rs/src/cnb/ing/pub.rs +++ b/rs/src/cnb/ing/publish.rs @@ -11,8 +11,8 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen(js_class = IngReq)] impl IngReq { - #[wasm_bindgen(js_name = pub)] - pub async fn export_pub(&self, content: &str, is_private: bool) -> Result<(), String> { + #[wasm_bindgen(js_name = publish)] + pub async fn export_publish(&self, content: &str, is_private: bool) -> Result<(), String> { panic_hook!(); let url = openapi!("/statuses"); diff --git a/src/auth/auth-manager.ts b/src/auth/auth-manager.ts index 108da99e..77755dfe 100644 --- a/src/auth/auth-manager.ts +++ b/src/auth/auth-manager.ts @@ -19,10 +19,10 @@ authProvider.onDidChangeSessions(async ({ added }) => { await AuthManager.updateAuthStatus() accountViewDataProvider.fireTreeDataChangedEvent() - postDataProvider.fireTreeDataChangedEvent(undefined) + postDataProvider.fireTreeDataChangedEvent() postCategoryDataProvider.fireTreeDataChangedEvent() - BlogExportProvider.optionalInstance?.refreshRecords({ force: false, clearCache: true }).catch(console.warn) + await BlogExportProvider.optionalInstance?.refreshRecords({ force: false, clearCache: true }) }) export namespace AuthManager { diff --git a/src/auth/auth-session.ts b/src/auth/auth-session.ts index 0bdafbcd..e9a2d12f 100644 --- a/src/auth/auth-session.ts +++ b/src/auth/auth-session.ts @@ -15,6 +15,7 @@ export class AuthSession implements CodeAuthSession { const accessTokenPart2 = this.accessToken.split('.')[1] const buf = Buffer.from(accessTokenPart2, 'base64') - return (<{ exp: number }>JSON.parse(buf.toString())).exp + const exp = JSON.parse(buf.toString()).exp + return exp * 1000 < Date.now() } } diff --git a/src/cmd/blog-export/delete.ts b/src/cmd/blog-export/delete.ts index b6a2afc1..56eb49c1 100644 --- a/src/cmd/blog-export/delete.ts +++ b/src/cmd/blog-export/delete.ts @@ -21,17 +21,13 @@ export async function deleteBlogExport(input: unknown): Promise { function confirm( itemName: string, hasLocalFile = true, - detail: string | undefined | null = '数据可能无法恢复, 请谨慎操作!' + detail: string ): Thenable { const options = [ { title: '确定' + (hasLocalFile ? '(保留本地文件)' : ''), result: { shouldDeleteLocal: false } }, ...(hasLocalFile ? [{ title: '确定(同时删除本地文件)', result: { shouldDeleteLocal: true } }] : []), ] - return Alert.info( - `确定要删除 ${itemName} 吗?`, - { modal: true, detail: detail ? detail : undefined }, - ...options - ).then( + return Alert.info(`确定要删除 ${itemName} 吗?`, { modal: true, detail }, ...options).then( x => x?.result, () => undefined ) @@ -66,14 +62,12 @@ async function deleteExportRecordItem(item: BlogExportRecordTreeItem) { void Alert.err(`删除博客备份失败: ${e}`) return false }) - if (hasDeleted) if (downloaded) await removeDownloadedBlogExport(downloaded, { shouldDeleteLocal }) + if (hasDeleted) if (downloaded !== undefined) await removeDownloadedBlogExport(downloaded, { shouldDeleteLocal }) await BlogExportProvider.optionalInstance?.refreshRecords() } async function removeDownloadedBlogExport(downloaded: DownloadedBlogExport, { shouldDeleteLocal = false }) { - await DownloadedExportStore.remove(downloaded, { shouldRemoveExportRecordMap: shouldDeleteLocal }).catch( - console.warn - ) - if (shouldDeleteLocal) await promisify(fs.rm)(downloaded.filePath).catch(console.warn) + await DownloadedExportStore.remove(downloaded, { shouldRemoveExportRecordMap: shouldDeleteLocal }) + if (shouldDeleteLocal) await promisify(fs.rm)(downloaded.filePath) } diff --git a/src/cmd/blog-export/download.ts b/src/cmd/blog-export/download.ts index 4b5ce91c..84426ad4 100644 --- a/src/cmd/blog-export/download.ts +++ b/src/cmd/blog-export/download.ts @@ -11,6 +11,7 @@ import path from 'path' import { promisify } from 'util' import { execCmd } from '@/infra/cmd' import { WorkspaceCfg } from '@/ctx/cfg/workspace' +import AdmZip from 'adm-zip' function parseInput(input: unknown): BlogExportRecordTreeItem | null | undefined { return input instanceof BlogExportRecordTreeItem ? input : null @@ -37,8 +38,8 @@ export async function downloadBlogExport(input: unknown) { const { optionalInstance: blogExportProvider } = BlogExportProvider await setIsDownloading(true) - const onError = (msg?: string | null) => { - if (msg) void Alert.warn(msg) + const onError = (msg: string) => { + void Alert.warn(msg) if (!isFileExist) fs.rmSync(zipFilePath) blogExportProvider?.refreshItem(treeItem) setIsDownloading(false).then(undefined, console.warn) @@ -70,26 +71,19 @@ export async function downloadBlogExport(input: unknown) { treeItem.reportDownloadingProgress({ percentage: 100, message: '解压中' }) blogExportProvider?.refreshItem(treeItem) - import('adm-zip') - // eslint-disable-next-line @typescript-eslint/naming-convention - .then(({ default: AdmZip }) => { + void (async () => { + try { const entry = new AdmZip(zipFilePath) - return promisify(entry.extractAllToAsync.bind(entry))( - targetDir, - true, - undefined - ).then(() => promisify(fs.rm)(zipFilePath)) - }) - .then(() => { - DownloadedExportStore.add(nonZipFilePath, exportId) - .then(() => treeItem.reportDownloadingProgress(null)) - .then(() => blogExportProvider?.refreshItem(treeItem)) - .then(() => blogExportProvider?.refreshDownloadedExports()) - .catch(console.warn) - }, console.warn) - .finally(() => { + await promisify(entry.extractAllToAsync.bind(entry))(targetDir, true, undefined) + await promisify(fs.rm)(zipFilePath) + await DownloadedExportStore.add(nonZipFilePath, exportId) + treeItem.reportDownloadingProgress(null) + blogExportProvider?.refreshItem(treeItem) + await blogExportProvider?.refreshDownloadedExports() + } finally { setIsDownloading(false).then(undefined, console.warn) - }) + } + })() }) ) } else { diff --git a/src/cmd/blog-export/edit.ts b/src/cmd/blog-export/edit.ts index ba3643b1..f9af7503 100644 --- a/src/cmd/blog-export/edit.ts +++ b/src/cmd/blog-export/edit.ts @@ -13,7 +13,7 @@ function parseInput(input: unknown): ExportPostTreeItem | null | undefined { export async function editExportPost(input: unknown): Promise { const target = parseInput(input) - if (!target) return void Alert.warn('不支持的参数输入') + if (target === undefined || target === null) return void Alert.warn('不支持的参数输入') const { post: { title, isMarkdown, id: postId }, diff --git a/src/cmd/blog-export/open-local.ts b/src/cmd/blog-export/open-local.ts index b6d1c036..8375c721 100644 --- a/src/cmd/blog-export/open-local.ts +++ b/src/cmd/blog-export/open-local.ts @@ -61,6 +61,6 @@ export async function openLocalExport(opts: Partial = def ) await DownloadedExportStore.add(dbFilePath, exportRecord?.id) - if (exportRecord) await treeProvider?.refreshRecords({ force: false }) + if (exportRecord !== undefined) await treeProvider?.refreshRecords({ force: false }) else await treeProvider?.refreshDownloadedExports() } diff --git a/src/cmd/blog-export/view-post.ts b/src/cmd/blog-export/view-post.ts index 9ac77dd7..18fc990b 100644 --- a/src/cmd/blog-export/view-post.ts +++ b/src/cmd/blog-export/view-post.ts @@ -29,7 +29,7 @@ async function provide(downloadedExport: DownloadedBlogExport, { id: postId, tit return false }) - if (matchedEditor) { + if (matchedEditor !== undefined) { await window.showTextDocument(matchedEditor.document, { preview: false, preserveFocus: true }) return } @@ -45,15 +45,11 @@ async function provide(downloadedExport: DownloadedBlogExport, { id: postId, tit })() ) - const document = await workspace - .openTextDocument(Uri.parse(`${schemaWithId}:(只读) ${title}.${isMarkdown ? 'md' : 'html'}?postId=${postId}`)) - .then(x => x, console.warn) - if (document) { - await window.showTextDocument(document).then(undefined, console.warn) - await languages - .setTextDocumentLanguage(document, isMarkdown ? 'markdown' : 'html') - .then(undefined, console.warn) - } + const uri = Uri.parse(`${schemaWithId}:(只读) ${title}.${isMarkdown ? 'md' : 'html'}?postId=${postId}`) + const document = await workspace.openTextDocument(uri) + + await window.showTextDocument(document) + await languages.setTextDocumentLanguage(document, isMarkdown ? 'markdown' : 'html') disposable.dispose() } diff --git a/src/cmd/extract-img/extract-img.ts b/src/cmd/extract-img/extract-img.ts index 61437e20..cedccb6a 100644 --- a/src/cmd/extract-img/extract-img.ts +++ b/src/cmd/extract-img/extract-img.ts @@ -6,8 +6,9 @@ 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 +export async function extractImg(arg?: Uri, inputImgSrc?: ImgSrc) { + if (arg === undefined) return + if (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) diff --git a/src/cmd/ing/comment-ing.ts b/src/cmd/ing/comment-ing.ts index 93b69c6f..4252d000 100644 --- a/src/cmd/ing/comment-ing.ts +++ b/src/cmd/ing/comment-ing.ts @@ -15,19 +15,19 @@ export class CommentIngCmdHandler implements CmdHandler { async handle(): Promise { const maxIngContentLength = 50 - const baseTitle = this._parentCommentId || 0 > 0 ? `回复@${this._atUser?.displayName}` : '评论闪存' + const baseTitle = this._parentCommentId !== undefined ? `回复@${this._atUser?.displayName}` : '评论闪存' const input = await window.showInputBox({ title: `${baseTitle}: ${this._ingContent.substring(0, maxIngContentLength)}${ this._ingContent.length > maxIngContentLength ? '...' : '' }`, - prompt: this._atUser ? `@${this._atUser.displayName}` : '', + prompt: this._atUser !== undefined ? `@${this._atUser.displayName}` : '', ignoreFocusOut: true, }) - this._content = input || '' + this._content = input ?? '' const { id: atUserId, displayName: atUserAlias } = this._atUser ?? {} - const atContent = atUserAlias ? `@${atUserAlias} ` : '' + const atContent = atUserAlias !== undefined ? `@${atUserAlias} ` : '' - if (this._content) { + if (this._content !== '') { return window.withProgress({ location: ProgressLocation.Notification, title: '正在请求...' }, async p => { p.report({ increment: 30 }) const isSuccess = await IngService.comment( diff --git a/src/cmd/ing/ing-page-list.ts b/src/cmd/ing/ing-page-list.ts index 55c5c665..463153f9 100644 --- a/src/cmd/ing/ing-page-list.ts +++ b/src/cmd/ing/ing-page-list.ts @@ -44,13 +44,11 @@ export namespace Ing.ListView { quickPick.onDidChangeSelection( ([selectedItem]) => { - if (selectedItem) { - quickPick.hide() - return getIngListWebviewProvider().refreshingList({ - pageIndex: 1, - ingType: selectedItem.ingType, - }) - } + quickPick.hide() + return getIngListWebviewProvider().refreshingList({ + pageIndex: 1, + ingType: selectedItem.ingType, + }) }, undefined, disposables diff --git a/src/cmd/ing/pub-ing.ts b/src/cmd/ing/pub-ing.ts index 6ec0c74c..8cad16b7 100644 --- a/src/cmd/ing/pub-ing.ts +++ b/src/cmd/ing/pub-ing.ts @@ -24,7 +24,7 @@ async function afterPub(ingIsPrivate: boolean) { const selected = await Alert.info('闪存已发布, 快去看看吧', ...options.map(v => ({ title: v[0], id: v[0] }))) - if (selected) return options.find(x => x[0] === selected.id)?.[1]() + if (selected !== undefined) return options.find(x => x[0] === selected.id)?.[1]() } export function pubIng(content: string, isPrivate: boolean) { diff --git a/src/cmd/open/open-post-in-blog-admin.ts b/src/cmd/open/open-post-in-blog-admin.ts index a428f1b9..cf076eaf 100644 --- a/src/cmd/open/open-post-in-blog-admin.ts +++ b/src/cmd/open/open-post-in-blog-admin.ts @@ -4,8 +4,8 @@ import { Post } from '@/model/post' import { PostFileMapManager } from '@/service/post/post-file-map' import { PostTreeItem } from '@/tree-view/model/post-tree-item' -export const openPostInBlogAdmin = (item: Post | PostTreeItem | Uri) => { - if (!item) return +export const openPostInBlogAdmin = (item?: Post | PostTreeItem | Uri) => { + if (item === undefined) return item = item instanceof PostTreeItem ? item.post : item const postId = item instanceof Post ? item.id : PostFileMapManager.getPostId(item.fsPath) ?? -1 diff --git a/src/cmd/open/os-open-local-post-file.ts b/src/cmd/open/os-open-local-post-file.ts index 65650c02..e33a56b5 100644 --- a/src/cmd/open/os-open-local-post-file.ts +++ b/src/cmd/open/os-open-local-post-file.ts @@ -3,7 +3,7 @@ import { execCmd } from '@/infra/cmd' import { Post } from '@/model/post' import { PostFileMapManager } from '@/service/post/post-file-map' -export function osOpenLocalPostFile(post: Post | undefined) { +export function osOpenLocalPostFile(post?: Post) { if (post === undefined) return const postFilePath = PostFileMapManager.getFilePath(post.id) diff --git a/src/cmd/pdf/export-pdf.ts b/src/cmd/pdf/export-pdf.ts index adb3c35b..82e511f9 100644 --- a/src/cmd/pdf/export-pdf.ts +++ b/src/cmd/pdf/export-pdf.ts @@ -2,7 +2,7 @@ import type puppeteer from 'puppeteer-core' import fs from 'fs' import path from 'path' import os from 'os' -import { MessageOptions, Progress, ProgressLocation, Uri, window, workspace } from 'vscode' +import { Progress, ProgressLocation, Uri, window, workspace } from 'vscode' import { Post } from '@/model/post' import { PostFileMapManager } from '@/service/post/post-file-map' import { PostService } from '@/service/post/post' @@ -91,7 +91,7 @@ const writePdfToFile = (dir: Uri, post: Post, buffer: Buffer) => const retrieveChromiumPath = async (): Promise => { let path: string | undefined = ChromiumPathProvider.lookupExecutableFromMacApp(ChromiumCfg.getChromiumPath()) - if (path && fs.existsSync(path)) return path + if (path !== undefined && fs.existsSync(path)) return path const platform = os.platform() const { defaultChromiumPath } = ChromiumPathProvider @@ -104,17 +104,17 @@ const retrieveChromiumPath = async (): Promise => { path = defaultChromiumPath.win.find(x => fs.existsSync(x)) ?? '' } - if (!path) { + if (path === undefined) { const { Options: options } = ChromiumPathProvider const input = await Alert.warn( - '未找到Chromium可执行文件', + '未找到 Chromium', { modal: true, }, ...options.map(x => x[0]) ) const op = options.find(x => x[0] === input) - path = op ? await op[1]() : undefined + path = op !== undefined ? await op[1]() : undefined } if (path !== undefined && path !== ChromiumCfg.getChromiumPath()) await ChromiumCfg.setChromiumPath(path) @@ -143,14 +143,12 @@ function handlePostInput(post: Post | PostTreeItem) { } async function handleUriInput(uri: Uri) { - const postList: Post[] = [] const { fsPath } = uri const postId = PostFileMapManager.getPostId(fsPath) - const { post: inputPost } = (await PostService.getPostEditDto(postId && postId > 0 ? postId : -1)) ?? {} + if (postId === undefined) return [] + const { post: inputPost } = await PostService.getPostEditDto(postId) - if (!inputPost) { - return [] - } else if (inputPost.id <= 0) { + if (inputPost.id <= 0) { Object.assign(inputPost, { id: -1, title: path.basename(fsPath, path.extname(fsPath)), @@ -158,9 +156,7 @@ async function handleUriInput(uri: Uri) { } as Post) } - postList.push(inputPost) - - return postList + return [inputPost] } const mapToPostEditDto = async (postList: Post[]) => @@ -168,15 +164,6 @@ const mapToPostEditDto = async (postList: Post[]) => .filter((x): x is PostEditDto => x != null) .map(x => x?.post) -const reportErrors = (errors: string[] | undefined) => { - if (errors && errors.length > 0) { - void Alert.err('导出 PDF 时遇到错误', { - modal: true, - detail: errors.join('\n'), - } as MessageOptions) - } -} - export async function exportPostToPdf(input?: Post | PostTreeItem | Uri): Promise { if (!(input instanceof Post) && !(input instanceof PostTreeItem) && !(input instanceof Uri)) return @@ -186,41 +173,37 @@ export async function exportPostToPdf(input?: Post | PostTreeItem | Uri): Promis const blogApp = AuthManager.getUserInfo()?.BlogApp if (blogApp === undefined) return void Alert.warn('无法获取博客地址, 请检查登录状态') - reportErrors( - await window.withProgress( - { - location: ProgressLocation.Notification, - }, - async progress => { - const errors: string[] = [] - progress.report({ message: '导出 PDF - 处理博文数据' }) - let selectedPost = await (input instanceof Post || input instanceof PostTreeItem - ? handlePostInput(input) - : handleUriInput(input)) - if (selectedPost.length <= 0) return - - selectedPost = input instanceof Post ? await mapToPostEditDto(selectedPost) : selectedPost - progress.report({ message: '选择输出文件夹' }) - const dir = await inputTargetFolder() - if (!dir || !chromiumPath) return - - progress.report({ message: '启动 Chromium' }) - const { browser, page } = (await launchBrowser(chromiumPath)) ?? {} - if (!browser || !page) return ['启动 Chromium 失败'] - - let idx = 0 - const { length: total } = selectedPost - for (const post of selectedPost) { - try { - await exportOne(idx++, total, post, page, dir, progress, blogApp) - } catch (err) { - errors.push(`导出"${post.title}失败", ${JSON.stringify(err)}`) - } + await window.withProgress( + { + location: ProgressLocation.Notification, + }, + async progress => { + progress.report({ message: '导出 PDF - 处理博文数据' }) + let selectedPost = await (input instanceof Post || input instanceof PostTreeItem + ? handlePostInput(input) + : handleUriInput(input)) + if (selectedPost.length <= 0) return + + selectedPost = input instanceof Post ? await mapToPostEditDto(selectedPost) : selectedPost + progress.report({ message: '选择输出文件夹' }) + const dir = await inputTargetFolder() + if (dir === undefined || chromiumPath === undefined) return + + progress.report({ message: '启动 Chromium' }) + const { browser, page } = (await launchBrowser(chromiumPath)) ?? {} + if (browser === undefined || page === undefined) return ['启动 Chromium 失败'] + + let idx = 0 + const { length: total } = selectedPost + for (const post of selectedPost) { + try { + await exportOne(idx++, total, post, page, dir, progress, blogApp) + } catch (e) { + void Alert.err(`导出 ${post.title} 失败: ${e}`) } - await page.close() - await browser.close() - return errors } - ) + await page.close() + await browser.close() + } ) } diff --git a/src/cmd/pdf/post-pdf-template-builder.ts b/src/cmd/pdf/post-pdf-template-builder.ts index 27cf2d92..44eceb86 100644 --- a/src/cmd/pdf/post-pdf-template-builder.ts +++ b/src/cmd/pdf/post-pdf-template-builder.ts @@ -15,7 +15,7 @@ export namespace PostPdfTemplateBuilder { const { isMarkdown, id: postId } = post const localFilePath = PostFileMapManager.getFilePath(postId) - postBody = localFilePath ? fs.readFileSync(localFilePath).toString('utf-8') : postBody + postBody = localFilePath !== undefined ? fs.readFileSync(localFilePath).toString('utf-8') : postBody const html = isMarkdown ? markdownItFactory({ @@ -28,10 +28,10 @@ export namespace PostPdfTemplateBuilder { const buildTagHtml = (): Promise => { let html = - post.tags && post.tags.length > 0 + post.tags !== undefined && post.tags.length > 0 ? post.tags.map(t => `${t}`).join(', ') : '' - html = html ? `
标签: ${html}
` : '' + html = html !== '' ? `
标签: ${html}
` : '' return Promise.resolve(html) } @@ -50,7 +50,7 @@ export namespace PostPdfTemplateBuilder { ) .join(', ') : '' - html = html ? `
分类: ${html}
` : '' + html = html !== '' ? `
分类: ${html}
` : '' return html } diff --git a/src/cmd/post-category/base-tree-view-cmd-handler.ts b/src/cmd/post-category/base-tree-view-cmd-handler.ts index 8f812176..79ed943f 100644 --- a/src/cmd/post-category/base-tree-view-cmd-handler.ts +++ b/src/cmd/post-category/base-tree-view-cmd-handler.ts @@ -42,7 +42,7 @@ export abstract class BaseMultiSelectablePostCategoryTreeViewCmdHandler extends : this.input instanceof PostCategory ? this.input : null - if (inputCategory && !categories.find(x => x.categoryId === inputCategory.categoryId)) + if (inputCategory !== null && categories.find(x => x.categoryId === inputCategory.categoryId) === undefined) categories.unshift(inputCategory) return categories diff --git a/src/cmd/post-category/del-selected-category.ts b/src/cmd/post-category/del-selected-category.ts index 49e82831..db9b2608 100644 --- a/src/cmd/post-category/del-selected-category.ts +++ b/src/cmd/post-category/del-selected-category.ts @@ -1,4 +1,4 @@ -import { MessageOptions, ProgressLocation, window } from 'vscode' +import { ProgressLocation, window } from 'vscode' import { PostCategory } from '@/model/post-category' import { PostCategoryService } from '@/service/post/post-category' import { PostCategoriesListTreeItem } from '@/tree-view/model/category-list-tree-item' @@ -54,7 +54,7 @@ export class DeletePostCategoriesHandler extends BaseMultiSelectablePostCategory }` ) .join('\n'), - } as MessageOptions) + }) } if (errs.length < selectedCategories.length) refreshPostCategoryList() } @@ -70,7 +70,7 @@ export class DeletePostCategoriesHandler extends BaseMultiSelectablePostCategory { detail: info, modal: true, - } as MessageOptions, + }, ...options ) diff --git a/src/cmd/post-category/input-post-category.ts b/src/cmd/post-category/input-post-category.ts index adee0e5e..e179a69d 100644 --- a/src/cmd/post-category/input-post-category.ts +++ b/src/cmd/post-category/input-post-category.ts @@ -13,11 +13,10 @@ const defaultSteps: PostCategoryInputStep[] = ['title', 'description', 'visible' export type PostCategoryInputStep = keyof PostCategoryAddDto export const inputPostCategory = ({ - title, + title = '编辑分类', category, steps = defaultSteps, }: Partial): Promise => { - title = title ? title : '编辑分类' const result: PostCategoryAddDto = { title: '', visible: false, @@ -41,7 +40,7 @@ export const inputPostCategory = ({ step: state.step++, totalSteps: state.totalSteps, placeHolder: '<必填>请输入分类标题', - validateInput: value => Promise.resolve(value ? undefined : '请输入分类标题'), + validateInput: value => Promise.resolve(value === '' ? '请输入分类标题' : undefined), shouldResume: () => Promise.resolve(false), }) result.title = categoryTitle ?? '' @@ -97,7 +96,7 @@ export const inputPostCategory = ({ ] return Promise.resolve(calculateNextStep()) - .then(nextStep => (nextStep ? MultiStepInput.run(nextStep) : Promise.reject())) + .then(nextStep => (nextStep !== undefined ? MultiStepInput.run(nextStep) : Promise.reject())) .catch() .then(() => (state.step - 1 === state.totalSteps ? result : undefined)) } diff --git a/src/cmd/post-category/update-post-category.ts b/src/cmd/post-category/update-post-category.ts index 13fd303c..56760d87 100644 --- a/src/cmd/post-category/update-post-category.ts +++ b/src/cmd/post-category/update-post-category.ts @@ -21,7 +21,6 @@ class UpdatePostCategoryTreeViewCmdHandler extends BasePostCategoryTreeViewCmdHa if (addDto === undefined) return const updateDto = Object.assign(new PostCategory(), category, addDto) - if (!updateDto) return await window.withProgress( { diff --git a/src/cmd/post-list/create-local.ts b/src/cmd/post-list/create-local.ts deleted file mode 100644 index efb04ba5..00000000 --- a/src/cmd/post-list/create-local.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { homedir } from 'os' -import path from 'path' -import { Uri, window, workspace } from 'vscode' -import { osOpenActiveFile } from '@/cmd/open/os-open-active-file' -import { openPostFile } from './open-post-file' -import { WorkspaceCfg } from '@/ctx/cfg/workspace' - -export async function createLocal() { - const dir = WorkspaceCfg.getWorkspaceUri().fsPath.replace(homedir(), '~') - let title = await window.showInputBox({ - placeHolder: '请输入标题', - prompt: `文件将会保存到 ${dir}`, - title: '新建博文', - validateInput: input => { - if (!input) return '标题不能为空' - - return undefined - }, - }) - if (!title) return - - const { fsPath: workspacePath } = WorkspaceCfg.getWorkspaceUri() - title = ['.md', '.html'].some(ext => title && title.endsWith(ext)) - ? title - : `${title}${title.endsWith('.') ? '' : '.'}md` - const filePath = path.join(workspacePath, title) - - try { - await workspace.fs.stat(Uri.file(filePath)) - } catch (e) { - // 文件不存在 - await workspace.fs.writeFile(Uri.file(filePath), Buffer.from('')) - } - - await openPostFile(filePath) - await osOpenActiveFile() - // 设置中关闭了 `autoReveal` 的情况下, 需要两次调用 `workbench.files.action.showActiveFileInExplorer` 命令, 才能正确 `reveal` - if (!workspace.getConfiguration('explorer').get('autoReveal')) await osOpenActiveFile() - - const focusEditor = async () => { - const { activeTextEditor } = window - if (activeTextEditor) - await window.showTextDocument(activeTextEditor.document, { preview: false, preserveFocus: false }) - } - await focusEditor() - // 确保能 focus 到编辑器(不这么做, 有时会聚焦到 explorer 处) - await new Promise(resolve => { - const innerTimeout = setTimeout(() => { - clearTimeout(innerTimeout) - void focusEditor().finally(() => resolve()) - }, 50) - }) -} 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 c400c18e..f4222790 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 @@ -14,15 +14,18 @@ async function confirm(postList: Post[]): Promise { return answer === '确定' } -export async function delPostToLocalFileMap(post: Post | PostTreeItem) { +export async function delPostToLocalFileMap(post?: Post | PostTreeItem) { post = post instanceof PostTreeItem ? post.post : post const view = extTreeViews.postList + let selectedPost = view.selection .map(x => (x instanceof Post ? x : x instanceof PostTreeItem ? x.post : null)) .filter((x): x is Post => x != null) + + if (post === undefined) return if (!selectedPost.includes(post)) { await revealPostListItem(post) - selectedPost = post ? [post] : [] + selectedPost = [post] } if (selectedPost.length <= 0) return diff --git a/src/cmd/post-list/del-post.ts b/src/cmd/post-list/del-post.ts index 8e66dd5c..7cf9f9b7 100644 --- a/src/cmd/post-list/del-post.ts +++ b/src/cmd/post-list/del-post.ts @@ -1,4 +1,4 @@ -import { MessageOptions, ProgressLocation, Uri, window, workspace } from 'vscode' +import { ProgressLocation, Uri, window, workspace } from 'vscode' import { Alert } from '@/infra/alert' import { PostService } from '@/service/post/post' import { PostFileMap, PostFileMapManager } from '@/service/post/post-file-map' @@ -12,7 +12,7 @@ let isDeleting = false async function confirmDelete(selectedPost: Post[]) { const result = { confirmed: false, deleteLocalFileAtSameTime: false } - if (!selectedPost || selectedPost.length <= 0) return result + if (selectedPost.length <= 0) return result const items = ['确定(保留本地文件)', '确定(同时删除本地文件)'] const clicked = await Alert.warn( @@ -20,7 +20,7 @@ async function confirmDelete(selectedPost: Post[]) { { detail: `确认后将会删除 ${selectedPost.map(x => x.title).join(', ')} 这${selectedPost.length}篇博文吗?`, modal: true, - } as MessageOptions, + }, ...items ) switch (clicked) { @@ -41,8 +41,8 @@ export async function delSelectedPost(arg: unknown) { else if (arg instanceof PostTreeItem) post = arg.post else return - const selectedPost: Post[] = post ? [post] : [] - extTreeViews.visiblePostList()?.selection.map(item => { + const selectedPost = [post] + extTreeViews.visiblePostList()?.selection.forEach(item => { const post = item instanceof PostTreeItem ? item.post : item if (post instanceof Post && !selectedPost.includes(post)) selectedPost.push(post) }) @@ -66,11 +66,11 @@ export async function delSelectedPost(arg: unknown) { increment: 0, }) try { - await PostService.delPost(...selectedPost.map(p => p.id)) + await PostService.del(...selectedPost.map(p => p.id)) if (isToDeleteLocalFile) { selectedPost .map(p => PostFileMapManager.getFilePath(p.id) ?? '') - .filter(x => !!x) + .filter(x => x !== '') .forEach(path => { workspace.fs.delete(Uri.file(path)).then(undefined, e => console.error(e)) }) diff --git a/src/cmd/post-list/modify-post-setting.ts b/src/cmd/post-list/modify-post-setting.ts index c48009a3..b30867a9 100644 --- a/src/cmd/post-list/modify-post-setting.ts +++ b/src/cmd/post-list/modify-post-setting.ts @@ -28,25 +28,22 @@ export async function modifyPostSetting(input: Post | PostTreeItem | Uri) { if (!(postId >= 0)) return - if (post) await revealPostListItem(post) + if (post !== undefined) await revealPostListItem(post) - const editDto = await PostService.getPostEditDto(postId) - if (!editDto) return - - const postEditDto = editDto.post + const postEditDto = (await PostService.getPostEditDto(postId)).post const localFilePath = PostFileMapManager.getFilePath(postId) await PostCfgPanel.open({ - panelTitle: '', - breadcrumbs: ['更新博文设置', editDto.post.title], + panelTitle: postEditDto.title, + breadcrumbs: ['更新博文设置', postEditDto.title], post: postEditDto, - localFileUri: localFilePath ? Uri.file(localFilePath) : undefined, - successCallback: ({ id }) => { + localFileUri: localFilePath !== undefined ? Uri.file(localFilePath) : undefined, + afterSuccess: ({ id }) => { void Alert.info('博文已更新') postDataProvider.fireTreeDataChangedEvent(id) postCategoryDataProvider.onPostUpdated({ refreshPost: false, postIds: [id] }) }, beforeUpdate: async post => { - if (localFilePath && fs.existsSync(localFilePath)) { + if (localFilePath !== undefined && fs.existsSync(localFilePath)) { await saveFilePendingChanges(localFilePath) post.postBody = await new LocalPost(localFilePath).readAllText() } diff --git a/src/cmd/post-list/open-post-file.ts b/src/cmd/post-list/open-post-file.ts index 0acb4d14..d9c76699 100644 --- a/src/cmd/post-list/open-post-file.ts +++ b/src/cmd/post-list/open-post-file.ts @@ -10,7 +10,7 @@ export async function openPostFile(post: LocalPost | Post | string, options?: Te else if (post instanceof Post) filePath = PostFileMapManager.getFilePath(post.id) ?? '' else filePath = post - if (!filePath) return + if (filePath === '') return await execCmd( 'vscode.open', diff --git a/src/cmd/post-list/open-post-in-vscode.ts b/src/cmd/post-list/open-post-in-vscode.ts index 736c7037..e577db80 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.getOne(firstCategoryId) : null + let i = firstCategoryId !== null ? await PostCategoryService.getOne(firstCategoryId) : null let categoryTitle = '' while (i != null) { categoryTitle = path.join( @@ -39,15 +39,14 @@ export async function buildLocalPostFileUri(post: Post, includePostId = false): export async function openPostInVscode(postId: number, forceUpdateLocalPostFile = false): Promise { let mappedPostFilePath = PostFileMapManager.getFilePath(postId) - if (mappedPostFilePath === '') mappedPostFilePath = undefined - const isFileExist = !!mappedPostFilePath && fs.existsSync(mappedPostFilePath) - if (mappedPostFilePath && isFileExist && !forceUpdateLocalPostFile) { + const isFileExist = mappedPostFilePath !== undefined && fs.existsSync(mappedPostFilePath) + if (mappedPostFilePath !== undefined && isFileExist && !forceUpdateLocalPostFile) { await openPostFile(mappedPostFilePath) return Uri.file(mappedPostFilePath) } // 本地文件已经被删除了, 确保重新生成博文与本地文件的关联 - if (mappedPostFilePath && !isFileExist) { + if (mappedPostFilePath !== undefined && !isFileExist) { await PostFileMapManager.updateOrCreate(postId, '') mappedPostFilePath = undefined } @@ -56,11 +55,11 @@ export async function openPostInVscode(postId: number, forceUpdateLocalPostFile const workspaceUri = WorkspaceCfg.getWorkspaceUri() await mkDirIfNotExist(workspaceUri) - let fileUri = mappedPostFilePath ? Uri.file(mappedPostFilePath) : await buildLocalPostFileUri(post) + let fileUri = mappedPostFilePath !== undefined ? Uri.file(mappedPostFilePath) : await buildLocalPostFileUri(post) // 博文尚未关联到本地文件的情况 // 本地存在和博文同名的文件, 询问用户是要覆盖还是同时保留两者 - if (!mappedPostFilePath && fs.existsSync(fileUri.fsPath)) { + if (mappedPostFilePath === undefined && fs.existsSync(fileUri.fsPath)) { const opt = ['保留本地文件并以博文 ID 为文件名新建另一个文件', '覆盖本地文件'] const selected = await Alert.info( `无法建立博文与本地文件的关联, 文件名冲突`, diff --git a/src/cmd/post-list/post-list-view.ts b/src/cmd/post-list/post-list-view.ts index 7679c20a..944b3259 100644 --- a/src/cmd/post-list/post-list-view.ts +++ b/src/cmd/post-list/post-list-view.ts @@ -7,6 +7,7 @@ import { PostListState } from '@/model/post-list-state' import { extTreeViews } from '@/tree-view/tree-view-register' import { execCmd } from '@/infra/cmd' import { PageList } from '@/model/page' +import { getListState, updatePostListState } from '@/service/post/post-list-view' let refreshTask: Promise | null = null let isRefreshing = false @@ -27,7 +28,7 @@ async function setPostListContext(pageCount: number, hasPrev: boolean, hasNext: async function goPage(f: (currentIndex: number) => number) { if (isRefreshing) return - const state = PostService.getPostListState() + const state = getListState() if (state === undefined) { void Alert.warn('操作失败: 状态错误') return @@ -39,7 +40,7 @@ async function goPage(f: (currentIndex: number) => number) { return } - await PostService.updatePostListState(index, state.pageCap, state.pageItemCount, state.pageCount) + await updatePostListState(index, state.pageCap, state.pageItemCount, state.pageCount) await PostListView.refresh() } @@ -48,7 +49,7 @@ function isPageIndexInRange(pageIndex: number, state: PostListState) { } function updatePostListViewTitle() { - const state = PostService.getPostListState() + const state = getListState() if (state === undefined) return const views = [extTreeViews.postList, extTreeViews.anotherPostList] @@ -76,14 +77,14 @@ export namespace PostListView { const fut = async () => { await setRefreshing(true) const page = await postDataProvider.loadPost() - const postCount = await PostService.getPostCount() + const postCount = await PostService.getCount() 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.updatePostListState(pageIndex, page.cap, page.items.length, pageCount) + await updatePostListState(pageIndex, page.cap, page.items.length, pageCount) updatePostListViewTitle() await postDataProvider.refreshSearch() await setRefreshing(false) @@ -109,10 +110,10 @@ export namespace PostListView { placeHolder: '请输入页码', validateInput: i => { const n = Number.parseInt(i) - if (isNaN(n) || !n) return '请输入正确格式的页码' + if (isNaN(n) || n === 0) return '请输入正确格式的页码' - const state = PostService.getPostListState() - if (!state) return '博文列表尚未加载' + const state = getListState() + if (state === undefined) return '博文列表尚未加载' if (isPageIndexInRange(n, state)) return undefined diff --git a/src/cmd/post-list/post-pull-all.ts b/src/cmd/post-list/post-pull-all.ts index 32ad1b53..952d3079 100644 --- a/src/cmd/post-list/post-pull-all.ts +++ b/src/cmd/post-list/post-pull-all.ts @@ -43,7 +43,7 @@ export async function postPullAll() { let postCount = 0 await window.withProgress(opt, async p => { - for await (const post of PostService.allPostIter()) { + for await (const post of PostService.iterAll()) { byteCount += Buffer.byteLength(post.postBody, 'utf-8') postCount += 1 if (postCount > MAX_POST_LIMIT || byteCount > MAX_BYTE_LIMIT) { diff --git a/src/cmd/post-list/post-pull.ts b/src/cmd/post-list/post-pull.ts index 38311e81..2f953790 100644 --- a/src/cmd/post-list/post-pull.ts +++ b/src/cmd/post-list/post-pull.ts @@ -12,10 +12,13 @@ import { MarkdownCfg } from '@/ctx/cfg/markdown' export async function postPull(input: Post | PostTreeItem | Uri | undefined | null) { const ctxList: CmdCtx[] = [] - let uri: Uri | undefined input = input instanceof PostTreeItem ? input.post : input - if (parsePostInput(input) && input.id > 0) await handlePostInput(input, ctxList) - else if ((uri = parseUriInput(input))) handleUriInput(uri, ctxList) + if (parsePostInput(input) && input.id > 0) { + await handlePostInput(input, ctxList) + } else { + const uri = parseUriInput(input) + if (uri !== undefined) handleUriInput(uri, ctxList) + } const fileName = resolveFileNames(ctxList) diff --git a/src/cmd/post-list/rename-post.ts b/src/cmd/post-list/rename-post.ts index 2e197eb5..aaee51a3 100644 --- a/src/cmd/post-list/rename-post.ts +++ b/src/cmd/post-list/rename-post.ts @@ -1,6 +1,6 @@ import { escapeRegExp } from 'lodash-es' import path from 'path' -import { MessageOptions, ProgressLocation, Uri, window, workspace } from 'vscode' +import { ProgressLocation, Uri, window, workspace } from 'vscode' import { Post } from '@/model/post' import { PostService } from '@/service/post/post' import { PostFileMapManager } from '@/service/post/post-file-map' @@ -34,15 +34,18 @@ async function renameLinkedFile(post: Post): Promise { } } -export async function renamePost(arg: Post | PostTreeItem) { - const post = arg instanceof PostTreeItem ? arg.post : arg - if (!post) return +export async function renamePost(arg?: Post | PostTreeItem) { + if (arg === undefined) return + + let post: Post + if (arg instanceof PostTreeItem) post = arg.post + else post = arg // arg: Post await revealPostListItem(post) const input = await window.showInputBox({ title: '请输入新的博文标题', - validateInput: v => (v ? undefined : '请输入一个标题'), + validateInput: v => (v === '' ? '请输入一个标题' : undefined), value: post.title, }) @@ -57,7 +60,6 @@ export async function renamePost(arg: Post | PostTreeItem) { async progress => { progress.report({ increment: 10 }) const editDto = await PostService.getPostEditDto(post.id) - if (!editDto) return false progress.report({ increment: 60 }) @@ -65,7 +67,7 @@ export async function renamePost(arg: Post | PostTreeItem) { editingPost.title = input let hasUpdated = false try { - await PostService.updatePost(editingPost) + await PostService.update(editingPost) post.title = input postDataProvider.fireTreeDataChangedEvent(post) hasUpdated = true @@ -73,7 +75,7 @@ export async function renamePost(arg: Post | PostTreeItem) { void Alert.err('更新博文失败', { modal: true, detail: err instanceof Error ? err.message : '服务器返回异常', - } as MessageOptions) + }) } finally { progress.report({ increment: 100 }) } diff --git a/src/cmd/post-list/upload-post.ts b/src/cmd/post-list/upload-post.ts index f0cbd6a4..9e678c98 100644 --- a/src/cmd/post-list/upload-post.ts +++ b/src/cmd/post-list/upload-post.ts @@ -1,4 +1,4 @@ -import { Uri, workspace, window, ProgressLocation, MessageOptions } from 'vscode' +import { Uri, workspace, window, ProgressLocation } from 'vscode' import { Post } from '@/model/post' import { Alert } from '@/infra/alert' import { PostService } from '@/service/post/post' @@ -16,6 +16,7 @@ import { MarkdownCfg } from '@/ctx/cfg/markdown' import { PostListView } from '@/cmd/post-list/post-list-view' import { extractImg } from '@/cmd/extract-img/extract-img' import { LocalPost } from '@/service/local-post' +import { existsSync } from 'fs' async function parseFileUri(fileUri?: Uri) { if (fileUri !== undefined && fileUri.scheme !== 'file') return undefined @@ -33,45 +34,45 @@ async function parseFileUri(fileUri?: Uri) { return undefined } -async function saveLocalPost(localPost: LocalPost) { +export async function saveLocalPost(localPost: LocalPost) { // check format if (!['.md', '.mkd'].some(x => localPost.fileExt === x)) { void Alert.warn('格式错误, 只支持 Markdown 文件') return } - const editDto = await PostService.fetchPostEditTemplate() - if (!editDto) return - - const { post } = editDto + const { post } = await PostService.getTemplate() post.title = localPost.fileNameWithoutExt post.isMarkdown = true - post.categoryIds ??= [] + post.categoryIds = [] void PostCfgPanel.open({ - panelTitle: '', - localFileUri: localPost.filePathUri, + panelTitle: post.title, + localFileUri: Uri.file(localPost.filePath), breadcrumbs: ['新建博文', '博文设置', post.title], post, - successCallback: async savedPost => { + afterSuccess: async savedPost => { await PostListView.refresh() await openPostFile(localPost) await PostFileMapManager.updateOrCreate(savedPost.id, localPost.filePath) await openPostFile(localPost) - postDataProvider.fireTreeDataChangedEvent(undefined) + postDataProvider.fireTreeDataChangedEvent() void Alert.info('博文已创建') }, - beforeUpdate: async (postToSave, panel) => { + beforeUpdate: async postToSave => { await saveFilePendingChanges(localPost.filePath) - // 本地文件已经被删除了 - if (!localPost.exist && panel) { + + if (!existsSync(localPost.filePath)) { void Alert.warn('本地文件已删除, 无法新建博文') return false } - if (MarkdownCfg.getAutoExtractImgSrc()) - await extractImg(localPost.filePathUri, MarkdownCfg.getAutoExtractImgSrc()).catch(console.warn) + const body = await localPost.readAllText() + if (isEmptyBody(body)) return false + + const autoExtractImgSrc = MarkdownCfg.getAutoExtractImgSrc() + if (autoExtractImgSrc !== undefined) await extractImg(Uri.file(localPost.filePath), autoExtractImgSrc) - postToSave.postBody = await localPost.readAllText() + postToSave.postBody = body return true }, }) @@ -86,7 +87,7 @@ function isEmptyBody(body: string) { return false } -export async function uploadPost(input?: Post | PostTreeItem | PostEditDto) { +export async function uploadPost(input?: Post | PostTreeItem | PostEditDto, confirm = true) { if (input === undefined) return if (input instanceof PostTreeItem) input = input.post @@ -102,10 +103,10 @@ export async function uploadPost(input?: Post | PostTreeItem | PostEditDto) { if (post === undefined) return const localFilePath = PostFileMapManager.getFilePath(post.id) - if (!localFilePath) return Alert.warn('本地无该博文的编辑记录') + if (localFilePath === undefined) return Alert.warn('本地无该博文的编辑记录') - if (MarkdownCfg.getAutoExtractImgSrc()) - await extractImg(Uri.file(localFilePath), MarkdownCfg.getAutoExtractImgSrc()).catch(console.warn) + const autoExtractImgSrc = MarkdownCfg.getAutoExtractImgSrc() + if (autoExtractImgSrc !== undefined) await extractImg(Uri.file(localFilePath), autoExtractImgSrc) await saveFilePendingChanges(localFilePath) post.postBody = (await workspace.fs.readFile(Uri.file(localFilePath))).toString() @@ -115,7 +116,7 @@ export async function uploadPost(input?: Post | PostTreeItem | PostEditDto) { post.isMarkdown = path.extname(localFilePath).endsWith('md') || path.extname(localFilePath).endsWith('mkd') || post.isMarkdown - if (MarkdownCfg.isShowConfirmMsgWhenUploadPost()) { + if (MarkdownCfg.isShowConfirmMsgWhenUploadPost() && confirm) { const answer = await Alert.warn( '确认上传博文吗?', { @@ -143,7 +144,7 @@ export async function uploadPost(input?: Post | PostTreeItem | PostEditDto) { let isSaved = false try { - const { id: postId } = await PostService.updatePost(thePost) + const { id: postId } = await PostService.update(thePost) await openPostInVscode(postId) thePost.id = postId @@ -153,7 +154,6 @@ export async function uploadPost(input?: Post | PostTreeItem | PostEditDto) { await PostListView.refresh() } catch (e) { progress.report({ increment: 100 }) - console.log(e) void Alert.err(`上传失败: ${e}`) } @@ -162,13 +162,12 @@ export async function uploadPost(input?: Post | PostTreeItem | PostEditDto) { ) } -export async function uploadPostFile(fileUri?: Uri) { +export async function uploadPostFile(fileUri?: Uri, confirm = true) { const parsedFileUri = await parseFileUri(fileUri) if (parsedFileUri === undefined) return const { fsPath: filePath } = parsedFileUri const postId = PostFileMapManager.getPostId(filePath) - console.log(postId) if (postId !== undefined && postId >= 0) { const dto = await PostService.getPostEditDto(postId) @@ -179,112 +178,6 @@ export async function uploadPostFile(fileUri?: Uri) { const fileContent = Buffer.from(await workspace.fs.readFile(parsedFileUri)).toString() if (isEmptyBody(fileContent)) return - const selected = await Alert.info( - '本地文件尚未关联到博客园博文', - { - modal: true, - detail: `您可以选择新建一篇博文或将本地文件关联到一篇博客园博文(您可以根据标题搜索您在博客园博文)`, - } as MessageOptions, - '新建博文', - '关联已有博文' - ) - if (selected === '关联已有博文') { - const selectedPost = await searchPostByTitle({ - postTitle: path.basename(filePath, path.extname(filePath)), - quickPickTitle: '搜索要关联的博文', - }) - if (selectedPost === undefined) return - - await PostFileMapManager.updateOrCreate(selectedPost.id, filePath) - const postEditDto = await PostService.getPostEditDto(selectedPost.id) - if (postEditDto === undefined) return - if (!fileContent) await workspace.fs.writeFile(parsedFileUri, Buffer.from(postEditDto.post.postBody)) - - await uploadPost(postEditDto.post) - } else if (selected === '新建博文') { - await saveLocalPost(new LocalPost(filePath)) - } -} - -export async function uploadPostNoConfirm(input?: Post | PostTreeItem | PostEditDto) { - if (input === undefined) return - if (input instanceof PostTreeItem) input = input.post - - let post: Post | undefined - - if (input instanceof Post) { - const dto = await PostService.getPostEditDto(input.id) - post = dto?.post - } else { - post = input.post - } - - if (post === undefined) return - - const localFilePath = PostFileMapManager.getFilePath(post.id) - if (!localFilePath) return Alert.warn('本地无该博文的编辑记录') - - if (MarkdownCfg.getAutoExtractImgSrc()) - await extractImg(Uri.file(localFilePath), MarkdownCfg.getAutoExtractImgSrc()).catch(console.warn) - - await saveFilePendingChanges(localFilePath) - post.postBody = (await workspace.fs.readFile(Uri.file(localFilePath))).toString() - - if (isEmptyBody(post.postBody)) return false - - post.isMarkdown = - path.extname(localFilePath).endsWith('md') || path.extname(localFilePath).endsWith('mkd') || post.isMarkdown - - const thePost = post // Dup code for type checking - - return window.withProgress( - { - location: ProgressLocation.Notification, - title: '正在上传博文', - cancellable: false, - }, - async progress => { - progress.report({ - increment: 10, - }) - - let isSaved = false - - try { - const { id: postId } = await PostService.updatePost(thePost) - await openPostInVscode(postId) - thePost.id = postId - - isSaved = true - progress.report({ increment: 100 }) - void Alert.info('上传成功') - await PostListView.refresh() - } catch (e) { - progress.report({ increment: 100 }) - void Alert.err(`上传失败: ${e}`) - } - - return isSaved - } - ) -} - -export async function uploadPostFileNoConfirm(fileUri?: Uri) { - const parsedFileUri = await parseFileUri(fileUri) - if (parsedFileUri === undefined) return - - const { fsPath: filePath } = parsedFileUri - const postId = PostFileMapManager.getPostId(filePath) - - if (postId !== undefined && postId >= 0) { - const dto = await PostService.getPostEditDto(postId) - if (dto !== undefined) await uploadPostNoConfirm(dto) - return - } - - const fileContent = Buffer.from(await workspace.fs.readFile(parsedFileUri)).toString() - if (isEmptyBody(fileContent)) return - const selected = await Alert.info( '本地文件尚未关联到博客园博文', { @@ -295,18 +188,18 @@ export async function uploadPostFileNoConfirm(fileUri?: Uri) { '关联已有博文' ) if (selected === '关联已有博文') { - const selectedPost = await searchPostByTitle({ - postTitle: path.basename(filePath, path.extname(filePath)), - quickPickTitle: '搜索要关联的博文', - }) + const selectedPost = await searchPostByTitle( + path.basename(filePath, path.extname(filePath)), + '搜索要关联的博文' + ) if (selectedPost === undefined) return await PostFileMapManager.updateOrCreate(selectedPost.id, filePath) const postEditDto = await PostService.getPostEditDto(selectedPost.id) if (postEditDto === undefined) return - if (!fileContent) await workspace.fs.writeFile(parsedFileUri, Buffer.from(postEditDto.post.postBody)) + if (fileContent === '') await workspace.fs.writeFile(parsedFileUri, Buffer.from(postEditDto.post.postBody)) - await uploadPostNoConfirm(postEditDto.post) + await uploadPost(postEditDto.post, confirm) } else if (selected === '新建博文') { await saveLocalPost(new LocalPost(filePath)) } diff --git a/src/cmd/show-local-file-to-post-info.ts b/src/cmd/show-local-file-to-post-info.ts index 785360b7..1b0a9c74 100644 --- a/src/cmd/show-local-file-to-post-info.ts +++ b/src/cmd/show-local-file-to-post-info.ts @@ -1,5 +1,5 @@ import path from 'path' -import { MessageOptions, Uri } from 'vscode' +import { Uri } from 'vscode' import { Alert } from '@/infra/alert' import { PostService } from '@/service/post/post' import { PostCategoryService } from '@/service/post/post-category' @@ -20,22 +20,22 @@ export async function showLocalFileToPostInfo(input: Uri | number): Promise= 0)) return + if (filePath === undefined || postId === undefined || postId < 0) return - const post = (await PostService.getPostEditDto(postId))?.post - if (!post) return + const { post } = await PostService.getPostEditDto(postId) 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` : '' + const tagsDesc = (post.tags?.length ?? 0) > 0 ? `博文标签: ${post.tags?.join(', ')}\n` : '' const options = ['在线查看博文', '取消关联'] const postUrl = post.url.startsWith('//') ? `https:${post.url}` : post.url const selected = await Alert.info( @@ -68,7 +67,7 @@ export async function showLocalFileToPostInfo(input: Uri | number): Promise { const activeEditor = window.activeTextEditor - if (activeEditor) { - await activeEditor.insertSnippet(new SnippetString(fmtImgLink(imgLink, 'markdown'))) - return true - } + if (activeEditor === undefined) return false - return false + await activeEditor.insertSnippet(new SnippetString(fmtImgLink(imgLink, 'markdown'))) + return true } diff --git a/src/cmd/view-post-online.ts b/src/cmd/view-post-online.ts index 021e4a98..255cc4fe 100644 --- a/src/cmd/view-post-online.ts +++ b/src/cmd/view-post-online.ts @@ -7,7 +7,7 @@ import { PostTreeItem } from '@/tree-view/model/post-tree-item' export async function viewPostOnline(input?: Post | PostTreeItem | Uri) { let post: Post | undefined = input instanceof Post ? input : input instanceof PostTreeItem ? input.post : undefined - if (!input) input = window.activeTextEditor?.document.uri + if (input === undefined) input = window.activeTextEditor?.document.uri if (input instanceof Uri) { const postId = PostFileMapManager.getPostId(input.fsPath) diff --git a/src/cmd/workspace.ts b/src/cmd/workspace.ts index ffcc8533..1169177b 100644 --- a/src/cmd/workspace.ts +++ b/src/cmd/workspace.ts @@ -1,4 +1,3 @@ -import { MessageOptions } from 'vscode' import { execCmd } from '@/infra/cmd' import { Alert } from '@/infra/alert' import { WorkspaceCfg } from '@/ctx/cfg/workspace' @@ -9,7 +8,7 @@ export namespace Workspace { const uri = WorkspaceCfg.getWorkspaceUri() const options = ['在当前窗口中打开', '在新窗口中打开'] const msg = `即将打开 ${uri.fsPath}` - const input = await Alert.info(msg, { modal: true } as MessageOptions, ...options) + const input = await Alert.info(msg, { modal: true }, ...options) if (input === undefined) return const shouldOpenInNewWindow = input === options[1] diff --git a/src/ctx/cfg/post-list.ts b/src/ctx/cfg/post-list.ts index 55984f18..e671bd73 100644 --- a/src/ctx/cfg/post-list.ts +++ b/src/ctx/cfg/post-list.ts @@ -1,7 +1,7 @@ import { LocalState } from '@/ctx/local-state' export namespace PostListCfg { - export function getPostListPageSize() { + export function getListPageSize() { return LocalState.getExtCfg().get('pageSize.postList') ?? 30 } } diff --git a/src/ctx/cfg/ui.ts b/src/ctx/cfg/ui.ts index 245441f2..dbfb8e0d 100644 --- a/src/ctx/cfg/ui.ts +++ b/src/ctx/cfg/ui.ts @@ -12,32 +12,3 @@ export namespace UiCfg { return cfgGet('disableIngUserAvatar') ?? false } } - -/* -// TODO: need solution -"cnblogsClient.ui.textIngEmoji": { - "order": 17, - "type": "boolean", - "scope": "application", - "default": false, - "markdownDescription": "闪存 Emoji 文本化" -}, -export function isEnableTextIngEmoji() { - return cfgGet('textIngEmoji') ?? false -} -*/ - -/* -// TODO: waiting for VSC API support -"cnblogsClient.ui.fakeExtIcon": { - "order": 18, - "type": "boolean", - "scope": "application", - "default": false, - "markdownDescription": "伪装扩展图标" -} -export function applyFakeExtIcon(cfg: WorkspaceCfg) { - const isEnable = cfg.get('ui.fakeExtIcon') - console.log(isEnable) -} -*/ diff --git a/src/infra/chromium-path-provider.ts b/src/infra/chromium-path-provider.ts index 97ede268..ff857fae 100644 --- a/src/infra/chromium-path-provider.ts +++ b/src/infra/chromium-path-provider.ts @@ -14,16 +14,18 @@ export namespace ChromiumPathProvider { export type ChromiumProviderFunc = () => Promise const selectFromLocalTitle = '选择本地Chromium' export const lookupExecutableFromMacApp = (path?: string) => { - if (path?.endsWith('.app')) { + if (path === undefined) return + + if (path.endsWith('.app')) { path = `${path}/Contents/MacOS` if (!fs.existsSync(path)) return undefined for (const item of fs.readdirSync(path)) { path = `${path}/${item}` - if (fs.statSync(path).mode & fs.constants.S_IXUSR) return path + const flag = fs.statSync(path).mode & fs.constants.S_IXUSR + if (flag !== 0) return path } } - return path } export const selectFromLocal: ChromiumProviderFunc = async (): Promise => { @@ -74,7 +76,7 @@ export namespace ChromiumPathProvider { } } ) - if (chromiumPath) void Alert.info(`Chromium 已下载至${chromiumPath}`) + if (chromiumPath === undefined) void Alert.info(`Chromium 已下载至${chromiumPath}`) return chromiumPath } diff --git a/src/infra/convert/ing-star-to-text.ts b/src/infra/convert/ing-star-to-text.ts deleted file mode 100644 index 6c4f1047..00000000 --- a/src/infra/convert/ing-star-to-text.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function ingStarToText(ingIcon: string) { - const imgTagReg = //gi - const mg = Array.from(ingIcon.matchAll(imgTagReg)) - - if (mg[0] !== undefined) return `✧${mg[0][1]}✧` - else return '' -} diff --git a/src/infra/fmt-img-link.ts b/src/infra/fmt-img-link.ts index bac0c00f..ec7aca60 100644 --- a/src/infra/fmt-img-link.ts +++ b/src/infra/fmt-img-link.ts @@ -1,17 +1,9 @@ export function fmtImgLink(link: string, format: 'html' | 'markdown' | 'raw') { - if (!link) return '' + if (link === '') return '' - let formatted = link - switch (format) { - case 'html': - formatted = `image` - break - case 'markdown': - formatted = `![img](${link})` - break - case 'raw': - default: - } + if (format === 'html') return `image` + if (format === 'markdown') return `![img](${link})` - return formatted + // raw case + return link } diff --git a/src/infra/input-post-setting.ts b/src/infra/input-post-setting.ts index 3879d705..09867055 100644 --- a/src/infra/input-post-setting.ts +++ b/src/infra/input-post-setting.ts @@ -91,7 +91,7 @@ export const inputPostSetting = ( totalSteps: state.totalSteps, placeholder: '<必选>请选择博文访问权限', activeItems: ( - [items.find(x => x.id === configuredPost.accessPermission)].filter(x => !!x) + [items.find(x => x.id === configuredPost.accessPermission)].filter(x => x !== undefined) ), buttons: [], canSelectMany: false, @@ -193,7 +193,7 @@ export const inputPostSetting = ( canSelectMany: false, shouldResume: () => Promise.resolve(false), }) - if (picked) configuredPost.isPublished = picked === items[0] + if (picked !== undefined) configuredPost.isPublished = picked === items[0] return calculateNextStep() } @@ -207,7 +207,12 @@ export const inputPostSetting = ( ] const nextStep = calculateNextStep() - return nextStep - ? MultiStepInput.run(nextStep).then(() => (state.step - 1 === state.totalSteps ? configuredPost : undefined)) - : Promise.resolve(undefined) + + if (nextStep === undefined) { + return Promise.resolve(undefined) + } else { + return MultiStepInput.run(nextStep).then(() => + state.step - 1 === state.totalSteps ? configuredPost : undefined + ) + } } diff --git a/src/infra/save-file-pending-changes.ts b/src/infra/save-file-pending-changes.ts index 5f830aab..cb94ca2f 100644 --- a/src/infra/save-file-pending-changes.ts +++ b/src/infra/save-file-pending-changes.ts @@ -3,5 +3,5 @@ import { window, Uri } from 'vscode' export const saveFilePendingChanges = async (filePath: Uri | string | undefined) => { const localPath = typeof filePath === 'string' ? filePath : filePath?.fsPath const activeEditor = window.visibleTextEditors.find(x => x.document.uri.fsPath === localPath) - if (activeEditor) await activeEditor.document.save() + if (activeEditor !== undefined) await activeEditor.document.save() } diff --git a/src/markdown/markdown.entry.ts b/src/markdown/markdown.entry.ts index dce084b1..2066bb00 100644 --- a/src/markdown/markdown.entry.ts +++ b/src/markdown/markdown.entry.ts @@ -6,7 +6,7 @@ HighlightersFactory.configCodeHighlightOptions({ enableCodeLineNumber: false }) function highlightLines(this: void) { const bgDefinitionStyleId = 'highlightedLineBackground' - if (!document.querySelector(`#${bgDefinitionStyleId}`)) { + if (document.querySelector(`#${bgDefinitionStyleId}`) === null) { const style = document.createElement('style') style.id = bgDefinitionStyleId style.innerHTML = `:root { --highlighted-line-bg: var(--vscode-diffEditor-insertedTextBackground) }` @@ -16,7 +16,7 @@ function highlightLines(this: void) { const highlighter = new HljsHighlighter() document.querySelectorAll('pre[class*="language-"][data-lines-highlight]').forEach(preEl => { const codeEl = preEl.querySelector('code') - if (!codeEl) return + if (codeEl === null) return if (codeEl.firstChild instanceof HTMLDivElement && codeEl.children.length === 1) codeEl.firstChild.outerHTML = codeEl.firstChild.innerHTML highlighter.highlightLines(preEl) diff --git a/src/model/error-response.ts b/src/model/error-response.ts deleted file mode 100644 index 63b76ad1..00000000 --- a/src/model/error-response.ts +++ /dev/null @@ -1,9 +0,0 @@ -interface IErrorResponse { - errors: string[] - type: number - statusCode: number -} - -const isErrorResponse = (obj: any): obj is IErrorResponse => obj.type >= -1 && !!obj.errors && obj.errors.length > 0 - -export { IErrorResponse, isErrorResponse } diff --git a/src/model/ing-view.ts b/src/model/ing-view.ts deleted file mode 100644 index 0f1874f5..00000000 --- a/src/model/ing-view.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Ing, IngComment } from './ing' -import { PartialTheme, Theme } from '@fluentui/react' - -export type IngAppState = { - ingList?: Ing[] - theme: Theme | PartialTheme - isRefreshing: boolean - comments?: Record -} - -export type IngItemState = { - comments?: Ing[] -} diff --git a/src/model/post-category.ts b/src/model/post-category.ts index dced4488..568b9c65 100644 --- a/src/model/post-category.ts +++ b/src/model/post-category.ts @@ -11,13 +11,14 @@ export class PostCategory { visibleChildCount = 0 parent?: PostCategory | null - flattenParents({ includeSelf = true }: { includeSelf?: boolean } = {}): PostCategory[] { + flattenParents(includeSelf: boolean): PostCategory[] { // eslint-disable-next-line @typescript-eslint/no-this-alias let i: PostCategory | null | undefined = this const result: PostCategory[] = [] while (i != null) { if (i !== this || includeSelf) result.unshift(i) - if (i.parent && !(i.parent instanceof PostCategory)) i.parent = Object.assign(new PostCategory(), i.parent) + if (i.parent !== null && i.parent !== undefined && !(i.parent instanceof PostCategory)) + i.parent = Object.assign(new PostCategory(), i.parent) i = i.parent } diff --git a/src/model/post-tag.ts b/src/model/post-tag.ts index d2883adc..37e145cd 100644 --- a/src/model/post-tag.ts +++ b/src/model/post-tag.ts @@ -1,11 +1,7 @@ -class PostTag { +export class PostTag { id = -1 name = '' useCount = 0 privateUseCount = 0 createTime: Date = new Date() } - -type PostTags = PostTag[] - -export { PostTags, PostTag } diff --git a/src/model/post.ts b/src/model/post.ts index 651430e8..a225321d 100644 --- a/src/model/post.ts +++ b/src/model/post.ts @@ -8,7 +8,7 @@ export class Post { blogId = -1 blogTeamIds: number[] = [] canChangeCreatedTime = false - categoryIds: number[] | null = [] + categoryIds: number[] = [] changeCreatedTime = false changePostType = false @@ -101,6 +101,6 @@ export function formatAccessPermission(value: AccessPermission) { case AccessPermission.authenticated: return '登录用户' default: - return '只有我' + return '仅自己' } } diff --git a/src/model/webview-cmd.ts b/src/model/webview-cmd.ts index 5633edbe..a3c0bf83 100644 --- a/src/model/webview-cmd.ts +++ b/src/model/webview-cmd.ts @@ -3,7 +3,6 @@ import { PostCategory } from '@/model/post-category' export namespace Webview.Cmd { export enum Ui { editPostCfg = 'editPostCfg', - showErrorResponse = 'showErrorResponse', updateBreadcrumbs = 'updateBreadcrumbs', updateImageUploadStatus = 'updateImageUploadStatus', setFluentIconBaseUrl = 'setFluentIconBaseUrl', diff --git a/src/model/webview-msg.ts b/src/model/webview-msg.ts index 17cfb362..16a43ae0 100644 --- a/src/model/webview-msg.ts +++ b/src/model/webview-msg.ts @@ -1,11 +1,10 @@ import { Post } from './post' import { Webview } from './webview-cmd' import { ColorThemeKind } from 'vscode' -import { PostTags } from './post-tag' -import { IErrorResponse as ErrorResponse } from './error-response' import { ImgUploadStatus } from './img-upload-status' import { SiteCategory } from '@/model/site-category' import { PostCategory } from '@/model/post-category' +import { PostTag } from '@/model/post-tag' export namespace WebviewMsg { export type Msg = { @@ -17,7 +16,7 @@ export namespace WebviewMsg { activeTheme: ColorThemeKind personalCategories: PostCategory[] siteCategories: SiteCategory[] - tags: PostTags + tags: PostTag[] breadcrumbs?: string[] fileName: string } @@ -26,10 +25,6 @@ export namespace WebviewMsg { post: Post } - export interface ShowErrRespMsg extends Msg { - errorResponse: ErrorResponse - } - export interface UpdateBreadcrumbMsg extends Msg { breadcrumbs?: string[] } diff --git a/src/service/blog-export/blog-export-post.store.ts b/src/service/blog-export/blog-export-post.store.ts index 092d5533..b36ffda2 100644 --- a/src/service/blog-export/blog-export-post.store.ts +++ b/src/service/blog-export/blog-export-post.store.ts @@ -69,37 +69,36 @@ export class ExportPostStore implements Disposable { ) } - list() { - return this._table - .findAll({ - where: { - postType: { - [Op.eq]: 'BlogPost', - }, - }, - order: [['id', 'desc']], - limit: 1000, - attributes: { - exclude: ['body'], + async list() { + const all = await this._table.findAll({ + where: { + postType: { + [Op.eq]: 'BlogPost', }, - }) - .then(data => data.map(x => x.dataValues)) + }, + order: [['id', 'desc']], + limit: 1000, + attributes: { + exclude: ['body'], + }, + }) + + return all.map(x => x.dataValues) } - getBody(id: number) { - return this._table - .findOne({ - where: { - id: { - [Op.eq]: id, - }, + async getBody(id: number) { + const one = await this._table.findOne({ + where: { + id: { + [Op.eq]: id, }, - attributes: ['body'], - }) - .then(x => x?.dataValues.body ?? '') + }, + attributes: ['body'], + }) + return one?.dataValues.body ?? '' } dispose() { - this._sequelize?.close().catch(console.warn) + void this._sequelize?.close() } } diff --git a/src/service/blog-export/blog-export-records.store.ts b/src/service/blog-export/blog-export-records.store.ts index 36c2899d..22e0d685 100644 --- a/src/service/blog-export/blog-export-records.store.ts +++ b/src/service/blog-export/blog-export-records.store.ts @@ -15,7 +15,7 @@ export namespace BlogExportRecordsStore { } export async function clearCache(): Promise { - if (cacheList) await cacheList.catch(() => false) + if (cacheList !== null) await cacheList.catch(() => false) cacheList = null cache = null diff --git a/src/service/blog-export/blog-export.ts b/src/service/blog-export/blog-export.ts index 46181c8f..8390aa38 100644 --- a/src/service/blog-export/blog-export.ts +++ b/src/service/blog-export/blog-export.ts @@ -38,7 +38,8 @@ export namespace BlogExportApi { beforeRedirect: [ (opt, resp) => { const location = resp.headers.location - if (location && location.includes('account.cnblogs.com')) throw new Error('未授权') + if (location === undefined) return + if (location.includes('account.cnblogs.com')) throw new Error('未授权') }, ], }, diff --git a/src/service/downloaded-export.store.ts b/src/service/downloaded-export.store.ts index cf5420a1..156d9c78 100644 --- a/src/service/downloaded-export.store.ts +++ b/src/service/downloaded-export.store.ts @@ -42,11 +42,12 @@ export namespace DownloadedExportStore { }) if (prunedItems.length > 0) { - await Promise.all( - [updateList(items)].concat( - prunedItems.map(p => (p.id ? updateExport(p.id, undefined) : Promise.resolve())) + const futList = [updateList(items)].concat( + prunedItems.map(p => + p.id !== null && p.id !== undefined ? updateExport(p.id, undefined) : Promise.resolve() ) ) + await Promise.all(futList) } } @@ -68,7 +69,7 @@ export namespace DownloadedExportStore { let item = LocalState.getState(key) as DownloadedBlogExport | undefined - if (prune && item) { + if (prune && item !== undefined) { const isExist = await promisify(exists)(item.filePath) if (!isExist) { item = undefined diff --git a/src/service/img.ts b/src/service/img.ts index f23a0a5b..8daf347f 100644 --- a/src/service/img.ts +++ b/src/service/img.ts @@ -6,16 +6,21 @@ import { lookup, extension } from 'mime-types' import { AppConst } from '@/ctx/app-const' export namespace ImgService { - export async function upload< - T extends Readable & { + export async function upload( + file: Readable & { name?: string fileName?: string filename?: string path?: string | Buffer - }, - >(file: T) { - const { name, fileName, filename, path: _path } = file - const finalName = path.basename(isString(_path) ? _path : fileName || filename || name || 'image.png') + } + ) { + let finalName: string + if (isString(file.path)) finalName = file.path + else if (file.filename !== undefined) finalName = file.filename + else if (file.fileName !== undefined) finalName = file.fileName + else if (file.name !== undefined) finalName = file.name + else finalName = 'image.png' + const ext = path.extname(finalName) let mimeType = lookup(ext) @@ -40,10 +45,9 @@ export namespace ImgService { * @param name The name that expected applied to the downloaded image * @returns The {@link Readable} stream */ - export async function download(url: string, name?: string): Promise { + export async function download(url: string, name = 'image'): Promise { const resp = await httpClient.get(url, { responseType: 'buffer' }) const contentType = resp.headers['content-type'] ?? 'image/png' - name = !name ? 'image' : name const readable = Readable.from(resp.body) diff --git a/src/service/ing/ing-list-webview-provider.ts b/src/service/ing/ing-list-webview-provider.ts index a1e8f8db..8ed2e257 100644 --- a/src/service/ing/ing-list-webview-provider.ts +++ b/src/service/ing/ing-list-webview-provider.ts @@ -9,15 +9,14 @@ import { window, } from 'vscode' import { parseWebviewHtml } from '@/service/parse-webview-html' -import { IngWebviewHostCmd, IngWebviewUiCmd, Webview } from '@/model/webview-cmd' +import { IngWebviewHostCmd, Webview } from '@/model/webview-cmd' import { IngService } from '@/service/ing/ing' -import { IngAppState } from '@/model/ing-view' import { IngType, IngTypesMetadata } from '@/model/ing' import { isNumber } from 'lodash-es' import { CommentIngCmdHandler } from '@/cmd/ing/comment-ing' import { execCmd } from '@/infra/cmd' -import { ingStarToText } from '@/infra/convert/ing-star-to-text' import { UiCfg } from '@/ctx/cfg/ui' +import { ingStarIconToText } from '@/wasm' export class IngListWebviewProvider implements WebviewViewProvider { readonly viewId = `${globalCtx.extName}.ing-list-webview` @@ -29,7 +28,7 @@ export class IngListWebviewProvider implements WebviewViewProvider { private _ingType = IngType.all get observer(): IngWebviewMessageObserver { - if (!this._view) throw Error('Cannot access the observer until the webviewView initialized!') + if (this._view === null) throw Error('Cannot access observer') this._observer ??= new IngWebviewMessageObserver(this) return this._observer } @@ -48,7 +47,7 @@ export class IngListWebviewProvider implements WebviewViewProvider { // eslint-disable-next-line @typescript-eslint/no-unused-vars async resolveWebviewView(webviewView: WebviewView, context: WebviewViewResolveContext, token: CancellationToken) { - if (this._view && this._view === webviewView) return + if (this._view !== null && this._view === webviewView) return this._view = webviewView @@ -82,7 +81,7 @@ export class IngListWebviewProvider implements WebviewViewProvider { .postMessage({ payload: { isRefreshing: true }, command: Webview.Cmd.Ing.Ui.setAppState, - } as IngWebviewUiCmd>) + }) .then(undefined, () => undefined) const rawIngList = await IngService.getList({ type: ingType, @@ -91,7 +90,7 @@ export class IngListWebviewProvider implements WebviewViewProvider { }) const ingList = rawIngList.map(ing => { if (UiCfg.isDisableIngUserAvatar()) ing.userIconUrl = '' - if (UiCfg.isEnableTextIngStar()) ing.icons = ingStarToText(ing.icons) + if (UiCfg.isEnableTextIngStar()) ing.icons = `${ingStarIconToText(ing.icons)}⭐` return ing }) const comments = await IngService.getCommentList(...ingList.map(x => x.id)) @@ -103,7 +102,7 @@ export class IngListWebviewProvider implements WebviewViewProvider { isRefreshing: false, comments, }, - } as IngWebviewUiCmd>) + }) .then(undefined, () => undefined) } else { this._view.show() @@ -116,14 +115,14 @@ export class IngListWebviewProvider implements WebviewViewProvider { } async updateComments(ingIds: number[]) { - if (!this._view || !this._view.visible) return + if (this._view === null || !this._view.visible) return const comments = await IngService.getCommentList(...ingIds) await this._view.webview.postMessage({ command: Webview.Cmd.Ing.Ui.setAppState, payload: { comments, }, - } as IngWebviewUiCmd>) + }) } private provideHtml(webview: CodeWebview) { @@ -152,10 +151,10 @@ export class IngListWebviewProvider implements WebviewViewProvider { } private setTitle() { - if (!this._view) return + if (this._view === null) return const ingTypeSuffix = IngTypesMetadata.find(([x]) => x === this.ingType)?.[1].displayName ?? '' const pageIndexSuffix = this.pageIndex > 1 ? `(第${this.pageIndex}页)` : '' - this._view.title = `闪存 ${ingTypeSuffix ? ' - ' + ingTypeSuffix : ''}${pageIndexSuffix}` + this._view.title = `闪存 ${ingTypeSuffix !== '' ? ' - ' + ingTypeSuffix : ''}${pageIndexSuffix}` } } @@ -175,7 +174,8 @@ class IngWebviewMessageObserver { const { ingType, pageIndex } = payload return this._provider.refreshingList({ ingType: - ingType && Object.values(IngType).includes(ingType as IngType) + // TODO: need type + (ingType as boolean) && Object.values(IngType).includes(ingType as IngType) ? (ingType as IngType) : undefined, pageIndex: isNumber(pageIndex) ? pageIndex : undefined, diff --git a/src/service/ing/ing.ts b/src/service/ing/ing.ts index 9216c2f0..420a2ff0 100644 --- a/src/service/ing/ing.ts +++ b/src/service/ing/ing.ts @@ -21,7 +21,7 @@ export namespace IngService { export async function pub(content: string, isPrivate: boolean) { try { const req = await getAuthedIngReq() - await req.pub(content, isPrivate) + await req.publish(content, isPrivate) return true } catch (e) { void Alert.err(`闪存发布失败: ${e}`) diff --git a/src/service/is-target-workspace.ts b/src/service/is-target-workspace.ts index f0778c58..6acb4e1b 100644 --- a/src/service/is-target-workspace.ts +++ b/src/service/is-target-workspace.ts @@ -11,13 +11,13 @@ export const isTargetWorkspace = (): boolean => { let currentFolder = folders?.length === 1 ? folders[0].uri.path : undefined let targetFolder = WorkspaceCfg.getWorkspaceUri().path const platform = os.platform() - if (platform === 'win32' && targetFolder && currentFolder) { - const replacer = (sub: string, m0: string | null | undefined, m2: string | null | undefined) => - m0 && m2 ? m0.toLowerCase() + m2 : sub + if (platform === 'win32' && targetFolder !== '' && currentFolder !== undefined) { + const replacer = (sub: string, m0?: string, m2?: string) => + m0 !== undefined && m2 !== undefined ? m0.toLowerCase() + m2 : sub currentFolder = currentFolder.replace(diskSymbolRegex, replacer) targetFolder = targetFolder.replace(diskSymbolRegex, replacer) } - const isTarget = !!currentFolder && currentFolder === targetFolder + const isTarget = currentFolder === targetFolder void execCmd('setContext', `${globalCtx.extName}.isTargetWorkspace`, isTarget) return isTarget } diff --git a/src/service/local-post.ts b/src/service/local-post.ts index 40ce1387..bd18d282 100644 --- a/src/service/local-post.ts +++ b/src/service/local-post.ts @@ -1,14 +1,9 @@ import path from 'path' -import fs from 'fs' import { Uri, workspace } from 'vscode' export class LocalPost { constructor(public filePath: string) {} - get fileName(): string { - return path.basename(this.filePath) - } - get fileNameWithoutExt(): string { return path.basename(this.filePath, this.fileExt) } @@ -17,16 +12,8 @@ export class LocalPost { return path.extname(this.filePath) } - get filePathUri() { - return Uri.file(this.filePath) - } - - get exist() { - return fs.existsSync(this.filePath) - } - async readAllText() { - const arr = await workspace.fs.readFile(this.filePathUri) + const arr = await workspace.fs.readFile(Uri.file(this.filePath)) const buf = Buffer.from(arr) return buf.toString() } diff --git a/src/service/multi-step-input.ts b/src/service/multi-step-input.ts index 27f4125f..ad4c91f8 100644 --- a/src/service/multi-step-input.ts +++ b/src/service/multi-step-input.ts @@ -77,11 +77,11 @@ export class MultiStepInput { input.placeholder = placeholder input.items = items input.canSelectMany = canSelectMany - if (activeItems) { + if (activeItems !== undefined) { input.activeItems = activeItems input.selectedItems = activeItems } - input.buttons = [...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), ...(buttons || [])] + input.buttons = [...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), ...(buttons ?? [])] input.ignoreFocusOut = ignoreFocusout ?? false try { @@ -94,15 +94,17 @@ export class MultiStepInput { else resolve(item) }), input.onDidChangeValue(() => { - if (onValueChange) void onValueChange(input) + if (onValueChange !== undefined) void onValueChange(input) }), input.onDidChangeSelection(() => { - if (onSelectionChange) void onSelectionChange(input) + if (onSelectionChange !== undefined) void onSelectionChange(input) }), input.onDidHide(() => { ;(async () => { reject( - shouldResume && (await shouldResume()) ? InputFlowAction.resume : InputFlowAction.cancel + shouldResume !== undefined && (await shouldResume()) + ? InputFlowAction.resume + : InputFlowAction.cancel ) })().catch(reject) }), @@ -110,7 +112,7 @@ export class MultiStepInput { resolve(canSelectMany ? Array.from(input.selectedItems) : input.selectedItems[0]) }) ) - if (this.current) this.current.dispose() + if (this.current !== undefined) this.current.dispose() this.current = input this.current.show() @@ -148,10 +150,10 @@ export class MultiStepInput { input.placeholder = placeHolder input.password = password ?? false input.totalSteps = totalSteps - input.value = value || '' + input.value = value ?? '' input.prompt = prompt input.ignoreFocusOut = ignoreFocusOut ?? false - input.buttons = [...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), ...(buttons || [])] + input.buttons = [...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), ...(buttons ?? [])] let validating = validateInput('') try { @@ -166,7 +168,7 @@ export class MultiStepInput { const value = input.value input.enabled = false input.busy = true - if (!(await validateInput(value))) resolve(value) + if ((await validateInput(value)) === undefined) resolve(value) input.enabled = true input.busy = false @@ -180,12 +182,14 @@ export class MultiStepInput { input.onDidHide(() => { ;(async () => { reject( - shouldResume && (await shouldResume()) ? InputFlowAction.resume : InputFlowAction.cancel + shouldResume !== undefined && (await shouldResume()) + ? InputFlowAction.resume + : InputFlowAction.cancel ) })().catch(reject) }) ) - if (this.current) this.current.dispose() + if (this.current !== undefined) this.current.dispose() this.current = input this.current.show() @@ -197,9 +201,9 @@ export class MultiStepInput { private async stepThrough(start: InputStep) { let step: InputStep | void = start - while (step) { + while (step !== undefined) { this.steps.push(step) - if (this.current) { + if (this.current !== undefined) { this.current.enabled = false this.current.busy = true } @@ -218,6 +222,6 @@ export class MultiStepInput { } } } - if (this.current) this.current.dispose() + if (this.current !== undefined) this.current.dispose() } } diff --git a/src/service/post/create.ts b/src/service/post/create.ts new file mode 100644 index 00000000..40b8c5fd --- /dev/null +++ b/src/service/post/create.ts @@ -0,0 +1,49 @@ +import { homedir } from 'os' +import path from 'path' +import { Uri, window, workspace } from 'vscode' +import { osOpenActiveFile } from '@/cmd/open/os-open-active-file' +import { WorkspaceCfg } from '@/ctx/cfg/workspace' +import { saveLocalPost } from '@/cmd/post-list/upload-post' +import { openPostFile } from '@/cmd/post-list/open-post-file' +import { LocalPost } from '@/service/local-post' +import { existsSync } from 'fs' + +export async function createPost() { + const workspacePath = WorkspaceCfg.getWorkspaceUri().fsPath + const dir = workspacePath.replace(homedir(), '~') + const title = await window.showInputBox({ + placeHolder: '请输入标题', + prompt: `文件将会保存至 ${dir}`, + title: '新建博文', + validateInput: input => { + if (input === '') return '标题不能为空' + return + }, + }) + if (title === undefined) return + + const filePath = path.join(workspacePath, `${title}.md`) + + if (!existsSync(filePath)) await workspace.fs.writeFile(Uri.file(filePath), Buffer.from('# Hello World\n')) + + await openPostFile(filePath) + await osOpenActiveFile() + // 设置中关闭了 `autoReveal` 的情况下, 需要两次调用 `workbench.files.action.showActiveFileInExplorer` 命令, 才能正确 `reveal` + if (workspace.getConfiguration('explorer').get('autoReveal') === false) await osOpenActiveFile() + + const focusEditor = async () => { + const editor = window.activeTextEditor + if (editor !== undefined) + await window.showTextDocument(editor.document, { preview: false, preserveFocus: false }) + } + await focusEditor() + // 确保能 focus 到编辑器(不这么做, 有时会聚焦到 explorer 处) + await new Promise(resolve => { + const innerTimeout = setTimeout(() => { + clearTimeout(innerTimeout) + void focusEditor().finally(() => resolve()) + }, 50) + }) + + await saveLocalPost(new LocalPost(filePath)) +} diff --git a/src/service/post/post-cfg-panel.ts b/src/service/post/post-cfg-panel.ts index 3f233cc9..9db3c387 100644 --- a/src/service/post/post-cfg-panel.ts +++ b/src/service/post/post-cfg-panel.ts @@ -1,60 +1,64 @@ import { cloneDeep } from 'lodash-es' -import vscode, { Uri } from 'vscode' +import vscode, { WebviewPanel, Uri } from 'vscode' import { Post } from '@/model/post' import { globalCtx } from '@/ctx/global-ctx' import { PostCategoryService } from './post-category' import { PostTagService } from './post-tag' import { PostService } from './post' -import { isErrorResponse } from '@/model/error-response' import { WebviewMsg } from '@/model/webview-msg' import { WebviewCommonCmd, Webview } from '@/model/webview-cmd' -import { uploadImg } from '@/cmd/upload-img/upload-img' import { ImgUploadStatusId } from '@/model/img-upload-status' import { openPostFile } from '@/cmd/post-list/open-post-file' import { parseWebviewHtml } from '@/service/parse-webview-html' import path from 'path' +import { Alert } from '@/infra/alert' +import { uploadFsImage } from '@/cmd/upload-img/upload-fs-img' +import { uploadClipboardImg } from '@/cmd/upload-img/upload-clipboard-img' -const panels: Map = new Map() +const panels: Map = new Map() type PostCfgPanelOpenOption = { post: Post panelTitle?: string localFileUri?: Uri - breadcrumbs?: string[] - successCallback: (post: Post) => any - beforeUpdate?: (postToUpdate: Post, panel: vscode.WebviewPanel) => Promise + breadcrumbs: string[] + afterSuccess: (post: Post) => any + beforeUpdate: (postToUpdate: Post, panel: WebviewPanel) => Promise } export namespace PostCfgPanel { - const resourceRootUri = () => globalCtx.assetsUri - const setHtml = async (webview: vscode.Webview): Promise => { webview.html = await parseWebviewHtml('post-cfg', webview) } export const buildPanelId = (postId: number, postTitle: string): string => `${postId}-${postTitle}` export const findPanelById = (panelId: string) => panels.get(panelId) - export const open = async (option: PostCfgPanelOpenOption) => { + + export async function open(option: PostCfgPanelOpenOption) { const { post, breadcrumbs, localFileUri } = option - const panelTitle = option.panelTitle ? option.panelTitle : `博文设置 - ${post.title}` + const panelTitle = option.panelTitle !== undefined ? option.panelTitle : `博文设置 - ${post.title}` await openPostFile(post, { viewColumn: vscode.ViewColumn.One, }) const panelId = buildPanelId(post.id, post.title) let panel = tryRevealPanel(panelId, option) - if (panel) return + if (panel !== undefined) return const disposables: (vscode.Disposable | undefined)[] = [] panel = await createPanel(panelTitle, post) const { webview } = panel + let fileName: string + if (localFileUri !== undefined) fileName = path.basename(localFileUri.fsPath, path.extname(localFileUri.fsPath)) + else fileName = '' + disposables.push( - webview.onDidReceiveMessage(async ({ command }: WebviewMsg.Msg) => { + panel.webview.onDidReceiveMessage(async ({ command }: WebviewMsg.Msg) => { if (command !== Webview.Cmd.Ext.refreshPost) return await webview.postMessage({ command: Webview.Cmd.Ui.setFluentIconBaseUrl, - baseUrl: webview.asWebviewUri(Uri.joinPath(resourceRootUri(), 'fonts')).toString() + '/', + baseUrl: webview.asWebviewUri(Uri.joinPath(globalCtx.assetsUri, 'fonts')).toString() + '/', } as WebviewMsg.SetFluentIconBaseUrlMsg) await webview.postMessage({ command: Webview.Cmd.Ui.editPostCfg, @@ -64,9 +68,7 @@ export namespace PostCfgPanel { siteCategories: cloneDeep(await PostCategoryService.getSitePresetList()), tags: cloneDeep(await PostTagService.fetchTags()), breadcrumbs, - fileName: localFileUri - ? path.basename(localFileUri.fsPath, path.extname(localFileUri?.fsPath)) - : '', + fileName, } as WebviewMsg.EditPostCfgMsg) }), observeWebviewMessages(panel, option), @@ -75,14 +77,9 @@ export namespace PostCfgPanel { ) } - const tryRevealPanel = ( - panelId: string | undefined, - options: PostCfgPanelOpenOption - ): vscode.WebviewPanel | undefined => { - if (!panelId) return - + const tryRevealPanel = (panelId: string, options: PostCfgPanelOpenOption): WebviewPanel | undefined => { const panel = findPanelById(panelId) - if (!panel) return + if (panel === undefined) return try { const { breadcrumbs } = options @@ -99,7 +96,7 @@ export namespace PostCfgPanel { return panel } - const createPanel = async (panelTitle: string, post: Post): Promise => { + const createPanel = async (panelTitle: string, post: Post): Promise => { const panelId = buildPanelId(post.id, post.title) const panel = vscode.window.createWebviewPanel(panelId, panelTitle, vscode.ViewColumn.Two, { enableScripts: true, @@ -112,19 +109,28 @@ export namespace PostCfgPanel { return panel } - const onUploadImageCmd = async (panel: vscode.WebviewPanel | undefined, message: WebviewMsg.UploadImgMsg) => { - if (panel === undefined) return - + const doUploadImg = async (panel: WebviewPanel, message: WebviewMsg.UploadImgMsg) => { const { webview } = panel - await webview.postMessage({ - command: Webview.Cmd.Ui.updateImageUploadStatus, - status: { - id: ImgUploadStatusId.uploading, + + const selected = await Alert.info( + '上传图片到博客园', + { + modal: true, + detail: '选择图片来源', }, - imageId: message.imageId, - } as WebviewMsg.UpdateImgUpdateStatusMsg) + '本地图片', + '剪贴板图片' + ) + if (selected === undefined) return + try { - const imageUrl = await uploadImg() + let imageUrl: string | undefined + + if (selected === '本地图片') imageUrl = await uploadFsImage() + else if (selected === '剪贴板图片') imageUrl = await uploadClipboardImg() + + if (imageUrl === undefined) return + await webview.postMessage({ command: Webview.Cmd.Ui.updateImageUploadStatus, status: { @@ -133,23 +139,12 @@ export namespace PostCfgPanel { }, imageId: message.imageId, } as WebviewMsg.UpdateImgUpdateStatusMsg) - } catch (err) { - if (isErrorResponse(err)) { - await webview.postMessage({ - command: Webview.Cmd.Ui.updateImageUploadStatus, - status: { - id: ImgUploadStatusId.failed, - errors: err.errors, - }, - imageId: message.imageId, - } as WebviewMsg.UpdateImgUpdateStatusMsg) - } + } catch (e) { + void Alert.err(`操作失败: { e}`) } } - const observeActiveColorSchemaChange = (panel: vscode.WebviewPanel | undefined): vscode.Disposable | undefined => { - if (panel === undefined) return - + const observeActiveColorSchemaChange = (panel: WebviewPanel) => { const { webview } = panel return vscode.window.onDidChangeActiveColorTheme(async theme => { await webview.postMessage({ @@ -160,65 +155,48 @@ export namespace PostCfgPanel { } const observeWebviewMessages = ( - panel: vscode.WebviewPanel | undefined, + panel: WebviewPanel | undefined, options: PostCfgPanelOpenOption ): vscode.Disposable | undefined => { if (panel === undefined) return const { webview } = panel - const { beforeUpdate, successCallback } = options + const { beforeUpdate, afterSuccess } = options return webview.onDidReceiveMessage(async message => { - const { command } = (message ?? {}) as WebviewMsg.Msg - switch (command) { - case Webview.Cmd.Ext.uploadPost: - try { - if (!panel) return - - const { post: postToUpdate } = message as WebviewMsg.UploadPostMsg - if (beforeUpdate) { - if (!(await beforeUpdate(postToUpdate, panel))) { - panel.dispose() - return - } - } - const postSavedModel = await PostService.updatePost(postToUpdate) - panel.dispose() - successCallback(Object.assign({}, postToUpdate, postSavedModel)) - } catch (err) { - if (isErrorResponse(err)) { - await webview.postMessage({ - command: Webview.Cmd.Ui.showErrorResponse, - errorResponse: err, - } as WebviewMsg.ShowErrRespMsg) - } else { - throw err - } - } - break - case Webview.Cmd.Ext.disposePanel: - panel?.dispose() - break - case Webview.Cmd.Ext.uploadImg: - await onUploadImageCmd(panel, message) - break - case Webview.Cmd.Ext.getChildCategories: - { - const { payload } = message as WebviewCommonCmd - await webview.postMessage({ - command: Webview.Cmd.Ui.updateChildCategories, - payload: { - value: await PostCategoryService.getAllUnder(payload.parentId).catch(() => []), - parentId: payload.parentId, - }, - } as WebviewCommonCmd) - } - break + const { command } = message as WebviewMsg.Msg + + if (command === Webview.Cmd.Ext.uploadPost) { + const { post } = message as WebviewMsg.UploadPostMsg + + if (!(await beforeUpdate(post, panel))) return + + try { + const postSavedModel = await PostService.update(post) + panel.dispose() + afterSuccess(Object.assign({}, post, postSavedModel)) + } catch (e) { + void Alert.err(`操作失败: ${e}`) + } + return + } else if (command === Webview.Cmd.Ext.disposePanel) { + panel.dispose() + } else if (command === Webview.Cmd.Ext.uploadImg) { + await doUploadImg(panel, message) + } else if (command === Webview.Cmd.Ext.getChildCategories) { + const { payload } = message as WebviewCommonCmd + await webview.postMessage({ + command: Webview.Cmd.Ui.updateChildCategories, + payload: { + value: await PostCategoryService.getAllUnder(payload.parentId).catch(() => []), + parentId: payload.parentId, + }, + }) } }) } const observerPanelDisposeEvent = ( - panel: vscode.WebviewPanel | undefined, + panel: WebviewPanel | undefined, disposables: (vscode.Disposable | undefined)[] ): vscode.Disposable | undefined => { if (panel === undefined) return diff --git a/src/service/post/post-file-map.ts b/src/service/post/post-file-map.ts index 2d97a692..7f9fbf09 100644 --- a/src/service/post/post-file-map.ts +++ b/src/service/post/post-file-map.ts @@ -2,7 +2,7 @@ import { postCategoryDataProvider } from '@/tree-view/provider/post-category-tre import { postDataProvider } from '@/tree-view/provider/post-data-provider' import { LocalState } from '@/ctx/local-state' -const validatePostFileMap = (map: PostFileMap) => map[0] >= 0 && !!map[1] +const validatePostFileMap = (map: PostFileMap) => map[0] >= 0 && map[1] !== '' export type PostFileMap = [postId: number, filePath: string] @@ -36,12 +36,12 @@ export namespace PostFileMapManager { export async function updateOrCreate(postId: number, filePath: string, { emitEvent = true } = {}) { const validFileExt = ['.md', '.html'] - if (filePath && !validFileExt.some(x => filePath.endsWith(x))) + if (filePath !== '' && !validFileExt.some(x => filePath.endsWith(x))) throw Error('Invalid filepath, file must have type markdown or html') const maps = getMaps() - const exist = maps.find(p => p[0] === postId) - if (exist) exist[1] = filePath + const map = maps.find(p => p[0] === postId) + if (map !== undefined) map[1] = filePath else maps.push([postId, filePath]) await LocalState.setState(storageKey, maps.filter(validatePostFileMap)) @@ -58,12 +58,13 @@ export namespace PostFileMapManager { export function findByFilePath(path: string) { const maps = getMaps().filter(validatePostFileMap) - return maps.find(x => x[0] && x[1] === path) + return maps.find(x => x[0] !== 0 && x[1] === path) } export function getFilePath(postId: number) { const map = findByPostId(postId) if (map === undefined) return + if (map[1] === '') return return map[1] } diff --git a/src/service/post/post-list-view.ts b/src/service/post/post-list-view.ts index 293801e7..bb5b0d8e 100644 --- a/src/service/post/post-list-view.ts +++ b/src/service/post/post-list-view.ts @@ -1,5 +1,21 @@ import { Post } from '@/model/post' import { extTreeViews } from '@/tree-view/tree-view-register' +import { PageList } from '@/model/page' +import { PostListState } from '@/model/post-list-state' +import { LocalState } from '@/ctx/local-state' +import { PostListRespItem } from '@/model/post-list-resp-item' +import { ZzkSearchResult } from '@/model/zzk-search-result' + +export interface PostListModel { + category: unknown // TODO: need type + categoryName: string + pageIndex: number + pageSize: number + postList: PostListRespItem[] + postsCount: number + + zzkSearchResult: ZzkSearchResult | null +} export async function revealPostListItem( post: Post | undefined, @@ -10,3 +26,29 @@ export async function revealPostListItem( const view = extTreeViews.visiblePostList() await view?.reveal(post, options) } + +export function getListState() { + const state = LocalState.getState('postListState') + if (state === undefined) throw Error('State is undefined') + return state +} + +export async function updatePostListState( + pageIndex: number, + pageCap: number, + pageItemCount: number, + pageCount: number +) { + const hasPrev = PageList.hasPrev(pageIndex) + const hasNext = PageList.hasNext(pageIndex, pageCount) + + const finalState = { + pageIndex, + pageCap, + pageItemCount, + pageCount, + hasPrev, + hasNext, + } + await LocalState.setState('postListState', finalState) +} diff --git a/src/service/post/post-tag.ts b/src/service/post/post-tag.ts index 0ced2330..f0a33d3d 100644 --- a/src/service/post/post-tag.ts +++ b/src/service/post/post-tag.ts @@ -7,7 +7,7 @@ let cachedTags: PostTag[] | null = null export namespace PostTagService { export async function fetchTags(forceRefresh = false): Promise { - if (cachedTags && !forceRefresh) return cachedTags + if (cachedTags !== null && !forceRefresh) return cachedTags const url = `${AppConst.ApiBase.BLOG_BACKEND}/tags/list` const resp = await AuthedReq.get(url, consHeader()) diff --git a/src/service/post/post-title-sanitizer.ts b/src/service/post/post-title-sanitizer.ts index ad0174c4..d8ed0a60 100644 --- a/src/service/post/post-title-sanitizer.ts +++ b/src/service/post/post-title-sanitizer.ts @@ -18,7 +18,7 @@ export namespace InvalidPostTitleStore { export function store(map: InvalidPostFileNameMap): Thenable { const [postId, invalidName] = map const key = buildStorageKey(postId) - if (invalidName) return LocalState.setState(key, invalidName) + if (invalidName === undefined || invalidName == null) return LocalState.setState(key, invalidName) else return LocalState.setState(key, undefined) } diff --git a/src/service/post/post.ts b/src/service/post/post.ts index 76cf67d5..b873954a 100644 --- a/src/service/post/post.ts +++ b/src/service/post/post.ts @@ -1,9 +1,7 @@ import { PostEditDto } from '@/model/post-edit-dto' import { PostUpdatedResp } from '@/model/post-updated-response' -import { ZzkSearchResult } from '@/model/zzk-search-result' import { MarkdownCfg } from '@/ctx/cfg/markdown' 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 } from '@/infra/http/infra/header' @@ -14,8 +12,8 @@ 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' +import { PostListModel } from '@/service/post/post-list-view' async function getAuthedPostReq() { const token = await AuthManager.acquireToken() @@ -25,8 +23,7 @@ async function getAuthedPostReq() { } export namespace PostService { - export const getPostListState = () => LocalState.getState('postListState') - + // TODO: need refactor export async function fetchPostList({ search = '', pageIndex = 1, pageSize = 30, categoryId = <'' | number>'' }) { const para = consUrlPara( ['t', '1'], @@ -52,7 +49,7 @@ export namespace PostService { } } - export async function getPostList(pageIndex: number, pageCap: number) { + export async function getList(pageIndex: number, pageCap: number) { const req = await getAuthedPostReq() try { const resp = await req.getList(pageIndex, pageCap) @@ -63,7 +60,7 @@ export namespace PostService { } } - export async function getPostCount() { + export async function getCount() { const req = await getAuthedPostReq() try { return await req.getCount() @@ -74,10 +71,10 @@ export namespace PostService { } // TODO: need better impl - export async function* allPostIter() { - const postCount = await getPostCount() + export async function* iterAll() { + const postCount = await getCount() for (const i of Array(postCount).keys()) { - const list = await PostService.getPostList(i + 1, 1) + const list = await PostService.getList(i + 1, 1) const id = list[0].id const dto = await PostService.getPostEditDto(id) yield dto.post @@ -103,7 +100,7 @@ export namespace PostService { } } - export async function delPost(...postIds: number[]) { + export async function del(...postIds: number[]) { const req = await getAuthedPostReq() try { if (postIds.length === 1) await req.delOne(postIds[0]) @@ -113,7 +110,7 @@ export namespace PostService { } } - export async function updatePost(post: Post) { + export async function update(post: Post) { if (MarkdownCfg.isIgnoreYfmWhenUploadPost()) post.postBody = rmYfm(post.postBody) const body = JSON.stringify(post) const req = await getAuthedPostReq() @@ -122,28 +119,8 @@ export namespace PostService { return JSON.parse(resp) } - export async function updatePostListState( - pageIndex: number, - pageCap: number, - pageItemCount: number, - pageCount: number - ) { - const hasPrev = PageList.hasPrev(pageIndex) - const hasNext = PageList.hasNext(pageIndex, pageCount) - - const finalState = { - pageIndex, - pageCap, - pageItemCount, - pageCount, - hasPrev, - hasNext, - } as PostListState - await LocalState.setState('postListState', finalState) - } - // TODO: need caahe - export async function fetchPostEditTemplate() { + export async function getTemplate() { const req = await getAuthedPostReq() try { const resp = await req.getTemplate() @@ -161,14 +138,3 @@ export namespace PostService { } } } - -interface PostListModel { - category: unknown // TODO: need type - categoryName: string - pageIndex: number - pageSize: number - postList: PostListRespItem[] - postsCount: number - - zzkSearchResult: ZzkSearchResult | null -} diff --git a/src/service/post/search-post-by-title.ts b/src/service/post/search-post-by-title.ts index afdbb0ff..94cfbdf2 100644 --- a/src/service/post/search-post-by-title.ts +++ b/src/service/post/search-post-by-title.ts @@ -15,40 +15,46 @@ class PostPickItem implements QuickPickItem { } } -export const searchPostByTitle = ({ postTitle = '', quickPickTitle = '按标题搜索博文' }): Promise => - new Promise(resolve => { - const quickPick = window.createQuickPick() - quickPick.title = quickPickTitle - quickPick.value = postTitle ?? '' - quickPick.placeholder = '输入标题以搜索博文' - const handleValueChange = async () => { - if (!quickPick.value) return - - const value = quickPick.value - try { - quickPick.busy = true - const data = await PostService.fetchPostList({ search: value }) - const postList = data.page.items - const pickItems = postList.map(p => new PostPickItem(p)) - if (value === quickPick.value) quickPick.items = pickItems - } catch (e) { - throw Error(`请求博文列表失败: ${e}`) - } finally { - quickPick.busy = false - } +export function searchPostByTitle(postTitle: string, quickPickTitle: string) { + const quickPick = window.createQuickPick() + quickPick.title = quickPickTitle + quickPick.value = postTitle ?? '' + quickPick.placeholder = '输入标题以搜索博文' + + const handleValueChange = async () => { + if (quickPick.value === '') return + + const value = quickPick.value + quickPick.busy = true + + try { + const data = await PostService.fetchPostList({ search: value }) + const postList = data.page.items + const pickItems = postList.map(p => new PostPickItem(p)) + if (value === quickPick.value) quickPick.items = pickItems + } catch (e) { + throw Error(`请求博文列表失败: ${e}`) + } finally { + quickPick.busy = false } - quickPick.onDidChangeValue(async () => { - await handleValueChange() - }) - let selected: PostPickItem | undefined = undefined - quickPick.onDidChangeSelection(() => { - selected = quickPick.selectedItems[0] - if (selected) quickPick.hide() - }) + } + + quickPick.onDidChangeValue(handleValueChange) + let selected: PostPickItem | undefined = undefined + quickPick.onDidChangeSelection(() => { + selected = quickPick.selectedItems[0] + if (selected !== undefined) quickPick.hide() + }) + + const fut = new Promise(resolve => { quickPick.onDidHide(() => { resolve(selected?.post) quickPick.dispose() }) - quickPick.show() - void handleValueChange() }) + + quickPick.show() + void handleValueChange() + + return fut +} diff --git a/src/setup/setup-cmd.ts b/src/setup/setup-cmd.ts index b78d4678..e270159f 100644 --- a/src/setup/setup-cmd.ts +++ b/src/setup/setup-cmd.ts @@ -1,5 +1,5 @@ import { globalCtx } from '@/ctx/global-ctx' -import { uploadPostFile, uploadPost, uploadPostNoConfirm, uploadPostFileNoConfirm } from '@/cmd/post-list/upload-post' +import { uploadPostFile, uploadPost } from '@/cmd/post-list/upload-post' import { uploadImg } from '@/cmd/upload-img/upload-img' import { osOpenLocalPostFile } from '@/cmd/open/os-open-local-post-file' import { showLocalFileToPostInfo } from '@/cmd/show-local-file-to-post-info' @@ -30,7 +30,6 @@ import { postPull } from '@/cmd/post-list/post-pull' import { postPullAll } from '@/cmd/post-list/post-pull-all' import { delPostToLocalFileMap } from '@/cmd/post-list/del-post-to-local-file-map' import { CopyPostLinkCmdHandler } from '@/cmd/post-list/copy-link' -import { createLocal } from '@/cmd/post-list/create-local' import { modifyPostSetting } from '@/cmd/post-list/modify-post-setting' import { renamePost } from '@/cmd/post-list/rename-post' import { openPostInVscode } from '@/cmd/post-list/open-post-in-vscode' @@ -38,6 +37,7 @@ 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' +import { createPost } from '@/service/post/create' function withPrefix(prefix: string) { return (rest: string) => `${prefix}${rest}` @@ -66,11 +66,13 @@ export function setupExtCmd() { regCmd(withAppName('.post.search'), PostListView.Search.search), regCmd(withAppName('.post.rename'), renamePost), regCmd(withAppName('.post.modify-setting'), modifyPostSetting), - regCmd(withAppName('.post.create-local'), createLocal), + regCmd(withAppName('.post.create'), createPost), regCmd(withAppName('.post.upload'), uploadPost), regCmd(withAppName('.post.upload-file'), uploadPostFile), - regCmd(withAppName('.post.upload-no-confirm'), uploadPostNoConfirm), - regCmd(withAppName('.post.upload-file-no-confirm'), uploadPostFileNoConfirm), + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + regCmd(withAppName('.post.upload-no-confirm'), arg => uploadPost(arg, false)), + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + regCmd(withAppName('.post.upload-file-no-confirm'), arg => uploadPostFile(arg, false)), regCmd(withAppName('.post.pull'), postPull), regCmd(withAppName('.post.pull-all'), postPullAll), regCmd(withAppName('.post.open-in-blog-admin'), openPostInBlogAdmin), diff --git a/src/tree-view/convert.ts b/src/tree-view/convert.ts index 57b3c879..cfb10a65 100644 --- a/src/tree-view/convert.ts +++ b/src/tree-view/convert.ts @@ -31,10 +31,10 @@ export type TreeItemSource = Post | PostCategory | TreeItem | BaseTreeItemSource type Converter = (s: T) => TreeItem | Promise const postConverter: Converter = obj => { - const descDatePublished = obj.datePublished ? ` \n发布于: ${format(obj.datePublished, 'yyyy-MM-dd HH:mm')}` : '' + const descDatePublished = ` \n发布于: ${format(obj.datePublished, 'yyyy-MM-dd HH:mm')}` const localPath = PostFileMapManager.getFilePath(obj.id) - const localPathForDesc = localPath?.replace(homedir(), '~') || '未关联本地文件' - const descLocalPath = localPath ? ` \n本地路径: ${localPathForDesc}` : '' + const localPathForDesc = localPath !== undefined ? localPath.replace(homedir(), '~') : '未关联本地文件' + const descLocalPath = localPath !== undefined ? ` \n本地路径: ${localPathForDesc}` : '' let url = obj.url url = url.startsWith('//') ? `https:${url}` : url return Object.assign(new TreeItem(`${obj.title}`, TreeItemCollapsibleState.Collapsed), { @@ -46,7 +46,7 @@ const postConverter: Converter = obj => { }, contextValue: contextValues.post(obj), iconPath: new ThemeIcon(obj.isMarkdown ? 'markdown' : 'file-code'), - description: localPath ? localPathForDesc : '', + description: localPath !== undefined ? localPathForDesc : '', resourceUri: Uri.joinPath(WorkspaceCfg.getWorkspaceUri(), obj.title + (obj.isMarkdown ? '.md' : '.html')), }) } diff --git a/src/tree-view/model/blog-export/record.ts b/src/tree-view/model/blog-export/record.ts index 0b4cd2e9..0adfbcdf 100644 --- a/src/tree-view/model/blog-export/record.ts +++ b/src/tree-view/model/blog-export/record.ts @@ -3,7 +3,7 @@ import { BaseEntryTreeItem } from '@/tree-view/model/base-entry-tree-item' import { BaseTreeItemSource } from '@/tree-view/model/base-tree-item-source' import { BlogExportRecordMetadata } from './record-metadata' import { parseStatusIcon } from './parser' -import { TreeItem, TreeItemCollapsibleState, ThemeIcon } from 'vscode' +import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode' import format from 'date-fns/format' import parseISO from 'date-fns/parseISO' import { DownloadedExportStore } from '@/service/downloaded-export.store' @@ -54,18 +54,20 @@ export class BlogExportRecordTreeItem extends BaseTreeItemSource implements Base getChildrenAsync: () => Promise = () => Promise.resolve(this.parseChildren()) reportDownloadingProgress(progress?: Partial | null) { - this._downloadingProgress = progress ? Object.assign({}, this._downloadingProgress ?? {}, progress ?? {}) : null + if (progress !== null && progress !== undefined) + this._downloadingProgress = Object.assign({}, this._downloadingProgress ?? {}, progress ?? {}) + else this._downloadingProgress = null } private pollingStatus() { - const timeoutId = setTimeout(() => { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + const timeoutId = setTimeout(async () => { clearTimeout(timeoutId) - BlogExportApi.getById(this.record.id) - .then(record => { - this.record = record - }) - .catch(console.warn) - .finally(() => this._treeDataProvider.refreshItem(this)) + try { + this.record = await BlogExportApi.getById(this.record.id) + } finally { + this._treeDataProvider.refreshItem(this) + } }, 1500) } @@ -79,7 +81,7 @@ export class BlogExportRecordTreeItem extends BaseTreeItemSource implements Base const formattedFileSize = filesize(fileBytes) const dateTimeFormat = 'yyyy MM-dd HH:mm' const localExport = await DownloadedExportStore.findById(id) - const items = [ + return [ new BlogExportRecordMetadata( this, id, @@ -112,7 +114,7 @@ export class BlogExportRecordTreeItem extends BaseTreeItemSource implements Base undefined, new ThemeIcon('vscode-cnb-date') ), - ...(dateExported + ...(dateExported !== null && dateExported !== undefined ? [ new BlogExportRecordMetadata( this, @@ -123,7 +125,7 @@ export class BlogExportRecordTreeItem extends BaseTreeItemSource implements Base ), ] : []), - ...(localExport && !_downloadingProgress + ...(localExport !== undefined && (_downloadingProgress === null || _downloadingProgress === undefined) ? [ new DownloadedExportTreeItem(this, localExport, { label: `本地文件: ${localExport.filePath.replace( @@ -133,7 +135,7 @@ export class BlogExportRecordTreeItem extends BaseTreeItemSource implements Base }), ] : []), - ...(_downloadingProgress + ...(_downloadingProgress !== undefined && _downloadingProgress !== null ? [ new BlogExportRecordMetadata( this, @@ -145,8 +147,6 @@ export class BlogExportRecordTreeItem extends BaseTreeItemSource implements Base ] : []), ] - - return items } private formatDownloadProgress(filesize: typeof import('filesize').filesize): string { diff --git a/src/tree-view/model/post-metadata.ts b/src/tree-view/model/post-metadata.ts index 15a6f716..100f976b 100644 --- a/src/tree-view/model/post-metadata.ts +++ b/src/tree-view/model/post-metadata.ts @@ -61,11 +61,15 @@ export abstract class PostMetadata extends BaseTreeItemSource { post: Post | PostTreeItem exclude?: RootPostMetadataType[] }): Promise { - let parsedPost = post instanceof PostTreeItem ? post.post : post - const postEditDto = await PostService.getPostEditDto(parsedPost.id) - parsedPost = postEditDto?.post || parsedPost + let parsedPost + if (post instanceof PostTreeItem) parsedPost = post.post + else parsedPost = post // post: Post + + const dto = await PostService.getPostEditDto(parsedPost.id) + parsedPost = dto.post + return Promise.all( - rootMetadataMap(parsedPost, postEditDto) + rootMetadataMap(parsedPost, dto) .filter(([type]) => !exclude.includes(type)) .map(([, factory]) => factory()) .map(x => (x instanceof Promise ? x : Promise.resolve(x))) @@ -132,8 +136,7 @@ export class PostCategoryMetadata extends PostMetadata { } static async parse(parent: Post, editDto?: PostEditDto): Promise { - editDto = editDto ? editDto : await PostService.getPostEditDto(parent.id) - if (editDto == null) return [] + if (editDto === undefined) editDto = await PostService.getPostEditDto(parent.id) const categoryIds = editDto.post.categoryIds ?? [] const futList = categoryIds.map(PostCategoryService.getOne) @@ -146,7 +149,7 @@ export class PostCategoryMetadata extends PostMetadata { new PostCategoryMetadata( parent, category - .flattenParents() + .flattenParents(true) .map(({ title }) => title) .join('/'), category.categoryId @@ -172,12 +175,10 @@ export class PostTagMetadata extends PostMetadata { } static async parse(parent: Post, editDto?: PostEditDto): Promise { - editDto = editDto ? editDto : await PostService.getPostEditDto(parent.id) - if (editDto == null) return [] + if (editDto === undefined) await PostService.getPostEditDto(parent.id) + if (editDto === undefined) return [] - const { - post: { tags }, - } = editDto + const tags = editDto.post.tags return (tags ?? [])?.map(tag => new PostTagMetadata(parent, tag)) } diff --git a/src/tree-view/provider/account-view-data-provider.ts b/src/tree-view/provider/account-view-data-provider.ts index acd6fbb8..06d7b6a3 100644 --- a/src/tree-view/provider/account-view-data-provider.ts +++ b/src/tree-view/provider/account-view-data-provider.ts @@ -13,7 +13,7 @@ export class AccountViewDataProvider implements TreeDataProvider { } getChildren(element?: TreeItem): ProviderResult { - if (!AuthManager.isAuthed || element) return [] + if (!AuthManager.isAuthed() || element !== undefined) return [] const userName = AuthManager.getUserInfo()?.DisplayName return [ diff --git a/src/tree-view/provider/blog-export-provider.ts b/src/tree-view/provider/blog-export-provider.ts index 3a60657f..99527fdb 100644 --- a/src/tree-view/provider/blog-export-provider.ts +++ b/src/tree-view/provider/blog-export-provider.ts @@ -63,9 +63,10 @@ export class BlogExportProvider implements TreeDataProvider } async refreshDownloadedExports({ force = true } = {}) { - if (this._downloadedExportEntry) { + const entry = this._downloadedExportEntry + if (entry !== null && entry !== undefined) { const hasCacheRefreshed = force - ? await this._downloadedExportEntry.refresh().then( + ? await entry.refresh().then( () => true, () => false ) @@ -98,11 +99,13 @@ export class BlogExportProvider implements TreeDataProvider */ clearCache = true, } = {}): Promise { + // TODO: need refactor const hasCacheRefreshed = force ? await BlogExportRecordsStore?.refresh() .then(() => true) .catch(e => { if (notifyOnError) void Alert.err(`刷新博客备份记录失败: ${e}`) + return false }) : clearCache ? await BlogExportRecordsStore.clearCache().then( 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 23385b4f..63a87170 100644 --- a/src/tree-view/provider/post-category-tree-data-provider.ts +++ b/src/tree-view/provider/post-category-tree-data-provider.ts @@ -77,7 +77,7 @@ export class PostCategoryTreeDataProvider implements TreeDataProvider { } getChildren(parent?: PostListTreeItem): ProviderResult { - if (!parent) { + if (parent === undefined) { const items: PostListTreeItem[] = this._searchResultEntry == null ? [] : [this._searchResultEntry] if (this.page == null) { @@ -53,11 +54,11 @@ export class PostDataProvider implements TreeDataProvider { } async loadPost() { - const { pageIndex } = PostService.getPostListState() - const pageCap = PostListCfg.getPostListPageSize() + const pageIndex = getListState().pageIndex + const pageCap = PostListCfg.getListPageSize() try { - const result = await PostService.getPostList(pageIndex, pageCap) + const result = await PostService.getList(pageIndex, pageCap) this.page = { index: pageIndex, cap: pageCap, @@ -65,7 +66,7 @@ export class PostDataProvider implements TreeDataProvider { items: result.map(it => Object.assign(new Post(), it)), } - this.fireTreeDataChangedEvent(undefined) + this.fireTreeDataChangedEvent() return this.page } catch (e) { @@ -74,7 +75,7 @@ export class PostDataProvider implements TreeDataProvider { } } - fireTreeDataChangedEvent(item: PostListTreeItem | undefined): void + fireTreeDataChangedEvent(item?: PostListTreeItem): void fireTreeDataChangedEvent(id: number): void fireTreeDataChangedEvent(item: PostListTreeItem | number | undefined): void { if (typeof item !== 'number') this._onDidChangeTreeData.fire(item) @@ -99,19 +100,17 @@ export class PostDataProvider implements TreeDataProvider { const zzkResult = data.zzkResult this._searchResultEntry = new PostSearchResultEntry(key, postList, matchedPostCount, zzkResult) - this.fireTreeDataChangedEvent(undefined) + this.fireTreeDataChangedEvent() } clearSearch() { this._searchResultEntry = null - this.fireTreeDataChangedEvent(undefined) + this.fireTreeDataChangedEvent() } async refreshSearch(): Promise { - const { _searchResultEntry } = this - - if (_searchResultEntry) { - const { searchKey } = _searchResultEntry + if (this._searchResultEntry !== null) { + const searchKey = this._searchResultEntry.searchKey this._searchResultEntry = null await this.search({ key: searchKey }) } diff --git a/src/tree-view/tree-view-register.ts b/src/tree-view/tree-view-register.ts index 6269fd3c..846f9fd9 100644 --- a/src/tree-view/tree-view-register.ts +++ b/src/tree-view/tree-view-register.ts @@ -92,7 +92,7 @@ export class ExtTreeViews implements Required { name: TKey ): NonNullable<(typeof _views)[TKey]> { const value = _views[name] - if (!value) throw Error(`tree view ${name} not registered yet`) + if (value === undefined) throw Error(`tree view ${name} not registered yet`) return value } } diff --git a/ui/ing/App.tsx b/ui/ing/App.tsx index 045ebcc2..7eae5364 100644 --- a/ui/ing/App.tsx +++ b/ui/ing/App.tsx @@ -2,14 +2,21 @@ import React, { Component, ReactNode } from 'react' import { IngWebviewUiCmd, Webview } from '@/model/webview-cmd' 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 { ThemeProvider } from '@fluentui/react/lib/Theme' import { Spinner, Stack } from '@fluentui/react' import { cloneWith } from 'lodash-es' +import { PartialTheme, Theme } from '@fluentui/react' -export class App extends Component { +export type State = { + ingList?: Ing[] + theme: Theme | PartialTheme + isRefreshing: boolean + comments?: Record +} + +export class App extends Component { constructor(props: unknown) { super(props) @@ -49,20 +56,21 @@ export class App extends Component { private observeMessages() { window.addEventListener('message', ({ data: { command, payload } }: { data: IngWebviewUiCmd }) => { if (command === Webview.Cmd.Ing.Ui.setAppState) { - const { ingList, isRefreshing, comments } = payload as Partial + const { ingList, isRefreshing, comments } = payload as Partial this.setState({ ingList: ingList?.map(Ing.parse) ?? this.state.ingList, isRefreshing: isRefreshing ?? this.state.isRefreshing, - comments: comments - ? Object.assign( - {}, - this.state.comments ?? {}, - cloneWith(comments, v => { - for (const key in v) v[key] = v[key].map(IngComment.parse) - return v - }) - ) - : this.state.comments, + comments: + comments !== undefined + ? Object.assign( + {}, + this.state.comments ?? {}, + cloneWith(comments, v => { + for (const key in v) v[key] = v[key].map(IngComment.parse) + return v + }) + ) + : this.state.comments, }) return } diff --git a/ui/ing/IngItem.tsx b/ui/ing/IngItem.tsx index 6c330969..870a7c53 100644 --- a/ui/ing/IngItem.tsx +++ b/ui/ing/IngItem.tsx @@ -1,6 +1,5 @@ import React, { Component } from 'react' import { Ing, IngComment, IngSendFromType } from '@/model/ing' -import { IngItemState } from '@/model/ing-view' import { take } from 'lodash-es' import { ActivityItem, IPersonaProps, Link, Text } from '@fluentui/react' import { format, formatDistanceStrict } from 'date-fns' @@ -8,12 +7,16 @@ import { zhCN } from 'date-fns/locale' import { getVsCodeApiSingleton } from 'share/vscode-api' import { IngWebviewHostCmd, Webview } from '@/model/webview-cmd' -interface IngItemProps { +type Props = { ing: Ing comments?: IngComment[] } -export class IngItem extends Component { +export type State = { + comments?: Ing[] +} + +export class IngItem extends Component { readonly icons = { vscodeLogo: ( @@ -44,7 +47,7 @@ export class IngItem extends Component { ), } as const - constructor(props: IngItemProps) { + constructor(props: Props) { super(props) this.state = {} } @@ -70,7 +73,7 @@ export class IngItem extends Component { comments={[ // eslint-disable-next-line @typescript-eslint/naming-convention
, - icons ? ( + icons !== '' ? ( { }} /> - {comments ?
{comments.map(this.renderComment)}
: <>} + {comments !== undefined ? ( +
{comments.map(this.renderComment)}
+ ) : ( + <> + )} ) } diff --git a/ui/ing/IngList.tsx b/ui/ing/IngList.tsx index f39b1030..f083eca4 100644 --- a/ui/ing/IngList.tsx +++ b/ui/ing/IngList.tsx @@ -3,13 +3,13 @@ import { Ing, IngComment } from '@/model/ing' import { IngItem } from 'ing/IngItem' import { Stack } from '@fluentui/react' -interface IngListProps { +type Props = { ingList: Ing[] comments: Record } -export class IngList extends Component { - constructor(props: IngListProps) { +export class IngList extends Component { + constructor(props: Props) { super(props) } diff --git a/ui/post-cfg/App.tsx b/ui/post-cfg/App.tsx index 23144767..41533abf 100644 --- a/ui/post-cfg/App.tsx +++ b/ui/post-cfg/App.tsx @@ -8,41 +8,46 @@ 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 { darkTheme, lightTheme } from 'share/theme' import { getVsCodeApiSingleton } from 'share/vscode-api' -interface AppState { +type State = { post?: Post theme?: Theme | PartialTheme breadcrumbs?: string[] fileName: string - useNestCategoriesSelect: boolean } -export interface AppProps extends Record {} - -export class App extends Component { - constructor(props: AppProps) { +export class App extends Component { + constructor(props: unknown) { super(props) - this.state = { theme: ActiveThemeProvider.activeTheme(), fileName: '', useNestCategoriesSelect: false } + this.state = { + theme: ActiveThemeProvider.activeTheme(), + fileName: '', + } this.observerMessages() getVsCodeApiSingleton().postMessage({ command: Webview.Cmd.Ext.refreshPost }) } render() { - const { post, fileName } = this.state - const isReady = post != null - const content = ( - <> - {this.renderBreadcrumbs()} - - + let content + if (this.state.post === undefined) { + content = ( + + + + ) + } else { + const { fileName } = this.state + content = ( + <> + {this.renderBreadcrumbs()} + - this.state.breadcrumbs && this.state.breadcrumbs.length > 1 + this.state.breadcrumbs !== undefined && this.state.breadcrumbs.length > 1 ? this.setState({ breadcrumbs: this.state.breadcrumbs .slice(0, this.state.breadcrumbs.length - 1) @@ -51,30 +56,22 @@ export class App extends Component { : undefined } fileName={fileName} - useNestCategoriesSelect={this.state.useNestCategoriesSelect} /> - - - - ) + + + ) + } + return ( - {isReady ? content : this.renderSpinner()} + {content} ) } - private renderSpinner() { - return ( - - - - ) - } - private renderBreadcrumbs() { const breadcrumbs = this.state.breadcrumbs - if (!breadcrumbs || breadcrumbs.length <= 0) return <> + if (breadcrumbs === undefined || breadcrumbs.length <= 0) return <> const items = breadcrumbs.map(breadcrumb => ({ text: breadcrumb, key: breadcrumb }) as IBreadcrumbItem) return @@ -99,7 +96,6 @@ export class App extends Component { post, breadcrumbs, fileName, - useNestCategoriesSelect: personalCategories.some(c => c.childCount > 0), }) } else if (command === Webview.Cmd.Ui.updateBreadcrumbs) { const { breadcrumbs } = message as WebviewMsg.UpdateBreadcrumbMsg diff --git a/ui/post-cfg/components/AccessPermissionSelector.tsx b/ui/post-cfg/components/AccessPermissionSelector.tsx deleted file mode 100644 index 5fc6a8f7..00000000 --- a/ui/post-cfg/components/AccessPermissionSelector.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { ChoiceGroup, IChoiceGroupOption, Label, Stack } from '@fluentui/react' -import { AccessPermission, formatAccessPermission } from '@/model/post' -import React from 'react' - -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 -> { - constructor(props: IAccessPermissionSelectorProps) { - props.accessPermission ??= AccessPermission.undeclared - super(props) - - this.state = {} - } - - render() { - return ( - - - - this.props.onChange?.apply(this, [option?.value ?? AccessPermission.undeclared]) - } - selectedKey={this.props.accessPermission?.toString()} - /> - - ) - } -} diff --git a/ui/post-cfg/components/CategorySelect.tsx b/ui/post-cfg/components/CategorySelect.tsx deleted file mode 100644 index 19e5b8bf..00000000 --- a/ui/post-cfg/components/CategorySelect.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Checkbox, Stack } from '@fluentui/react' -import { Component } from 'react' -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 - onChange?: (categoryIds: number[]) => void -} - -interface CategoriesSelectorState { - categories: PostCategory[] - categoryIds: number[] -} - -class CategorySelect extends Component { - constructor(props: CategoriesSelectorProps) { - super(props) - this.state = { categories: PersonalCategoryStore.get(), categoryIds: props.categoryIds ?? [] } - } - - render() { - const categories = this.state.categories - const categoryIds = this.state.categoryIds - const items = categories.map(category => ( - this.onCheckboxChanged(category.categoryId, isChecked)} - label={category.title} - checked={categoryIds?.includes(category.categoryId)} - /> - )) - return ( - - {items} - - ) - } - - private onCheckboxChanged(categoryId: number, isChecked?: boolean) { - const { categoryIds } = this.state - - const position = categoryIds.findIndex(eq(categoryId)) - const isInclude = position >= 0 - - if (isChecked && !isInclude) categoryIds.push(categoryId) - else if (isInclude) categoryIds.splice(position, 1) - - this.props.onChange?.apply(this, [categoryIds]) - } -} - -export { CategorySelect } diff --git a/ui/post-cfg/components/ErrorResponse.tsx b/ui/post-cfg/components/ErrorResponse.tsx deleted file mode 100644 index 2ef993fa..00000000 --- a/ui/post-cfg/components/ErrorResponse.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { MessageBar, MessageBarType } from '@fluentui/react' -import { Webview } from '@/model/webview-cmd' -import { WebviewMsg } from '@/model/webview-msg' -import React from 'react' -import { Optional } from 'utility-types' -import { PostFormContext } from './PostFormContext' - -export interface IErrorResponseProps extends Record {} - -export type IErrorResponseState = { - errors: string[] -} - -export class ErrorResponse extends React.Component { - static contextType = PostFormContext - declare context: React.ContextType - - private elementId = '' - - constructor() { - super({}) - - this.state = { errors: [] } - window.addEventListener('message', msg => { - // 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: '' }) - } - }) - } - - reveal() { - document.querySelector(`#${this.elementId}`)?.scrollIntoView() - } - - render() { - const errors = this.state.errors - if (errors.length <= 0) return <> - - this.elementId = `errorResponse ${Date.now()}` - return ( - this.setState({ errors: [] })} - id={this.elementId} - messageBarType={MessageBarType.error} - > - {errors.join('\n')} - - ) - } -} diff --git a/ui/post-cfg/components/NestCategorySelect.tsx b/ui/post-cfg/components/NestCategorySelect.tsx deleted file mode 100644 index 19caef61..00000000 --- a/ui/post-cfg/components/NestCategorySelect.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { ActionButton, Checkbox, Icon, Link, Spinner, Stack } from '@fluentui/react' -import { PostCategory } from '@/model/post-category' -import { take } from 'lodash-es' -import { PersonalCategoryStore } from 'post-cfg/service/personal-category-store' -import React from 'react' - -export type INestCategoriesSelectProps = { - selected?: number[] - parent?: number | null - onSelect?: (value: number[]) => void - level?: number -} - -export type INestCategoriesSelectState = { - expanded?: Set | null - children?: PostCategory[] - showAll?: boolean - limit: number -} - -export default class NestCategorySelect extends React.Component< - INestCategoriesSelectProps, - INestCategoriesSelectState -> { - constructor(props: INestCategoriesSelectProps) { - super(props) - - this.state = { - limit: 10, - } - } - - get isRoot(): boolean { - return this.props.parent == null - } - - render() { - if (this.props.parent && !this.state.children) { - PersonalCategoryStore.getByParent(this.props.parent) - .then(v => this.setState({ children: v })) - .catch(console.warn) - return - } - - const categories = this.isRoot ? PersonalCategoryStore.get() : this.state.children ?? [] - return ( - - {(this.state.showAll ? categories : take(categories, this.state.limit)).map(c => ( -
- - - { - const selected = this.props.selected ?? [] - this.props.onSelect?.( - isChecked - ? Array.from(new Set([...selected, c.categoryId])) - : selected.filter(x => x !== c.categoryId) - ) - }} - > - {/* Button used toggle expand/collapse status */} - {c.childCount > 0 ? ( - this.onToggleExpandStatus(c)} - styles={{ - root: { - height: 'auto', - }, - }} - iconProps={{ - iconName: this.checkIsExpanded(c) ? 'SkypeCircleMinus' : 'CirclePlus', - }} - /> - ) : ( - <> - )} - - - {this.checkIsExpanded(c) ? ( - - ) : ( - <> - )} - -
- ))} - - {this.isRoot ? ( -
- this.setState({ showAll: !this.state.showAll })} - style={{ height: 'auto', padding: 0, fontSize: '14px' }} - > - - -  {this.state.showAll ? '收起' : '展开'} - - -
- ) : ( - <> - )} -
- ) - } - - private onToggleExpandStatus(category: PostCategory) { - const isExpanded = this.checkIsExpanded(category) - - let expandedSet = this.state.expanded - - if (isExpanded) { - expandedSet?.delete(category.categoryId) - } else { - expandedSet ??= new Set() - expandedSet.add(category.categoryId) - } - - this.setState({ expanded: expandedSet && expandedSet.size > 0 ? new Set(expandedSet) : null }) - expandedSet?.clear() - } - - private checkIsExpanded(category: PostCategory): boolean { - return this.state.expanded?.has(category.categoryId) ?? false - } -} - -export type ICategoryItemProps = { - category: PostCategory - onChange: (isChecked: boolean) => void - isChecked: boolean -} - -// eslint-disable-next-line @typescript-eslint/naming-convention -export function CategoryCheckbox({ category, onChange, isChecked }: ICategoryItemProps) { - return ( - onChange(isChecked ?? false)} - > - ) -} diff --git a/ui/post-cfg/components/CommonOptions.tsx b/ui/post-cfg/components/OptionCheckBox.tsx similarity index 59% rename from ui/post-cfg/components/CommonOptions.tsx rename to ui/post-cfg/components/OptionCheckBox.tsx index f6087c72..a8f3de72 100644 --- a/ui/post-cfg/components/CommonOptions.tsx +++ b/ui/post-cfg/components/OptionCheckBox.tsx @@ -1,27 +1,23 @@ -import { Checkbox, Label, Stack } from '@fluentui/react' +import { Checkbox, Stack } from '@fluentui/react' import * as React from 'react' +import { Component } from 'react' type Option = { [key: string]: { label: string; checked: boolean } } -export type ICommonOptionsProps = { +type Props = { options: TOption - onChange?: (optionKey: keyof TOption, checked: boolean, stateObj: { [p in typeof optionKey]: boolean }) => void + onChange: (optionKey: keyof TOption, checked: boolean, stateObj: { [p in typeof optionKey]: boolean }) => void } -export class CommonOptions extends React.Component> { - constructor(props: ICommonOptionsProps) { +export class OptionCheckBox extends Component> { + constructor(props: Props) { super(props) - - this.state = {} } render() { return ( - - - - {this.renderOptions()} - + + {this.renderOptions()} ) } diff --git a/ui/post-cfg/components/PostForm.tsx b/ui/post-cfg/components/PostForm.tsx index d6b7b2ea..3ad4095e 100644 --- a/ui/post-cfg/components/PostForm.tsx +++ b/ui/post-cfg/components/PostForm.tsx @@ -1,158 +1,125 @@ import { DefaultButton, PrimaryButton } from '@fluentui/react/lib/Button' import { Stack } from '@fluentui/react/lib/Stack' -import React from 'react' -import { CategorySelect } from './CategorySelect' -import { SiteHomeContributionOptionsSelector } from './SiteHomeContributionOptionsSelector' -import { PostCfg } from '@/model/post-cfg' -import { Label, Spinner } from '@fluentui/react' -import { SiteCategorySelector } from './SiteCategorySelector' -import { TagsInput } from './TagsInput' -import { CommonOptions } from './CommonOptions' -import { AccessPermissionSelector } from './AccessPermissionSelector' -import { PasswordInput } from './PasswordInput' +import React, { Component } from 'react' +import { OptionCheckBox } from './OptionCheckBox' import { getVsCodeApiSingleton } from '../../share/vscode-api' -import { ErrorResponse } from './ErrorResponse' import { Webview } from '@/model/webview-cmd' import { WebviewMsg } from '@/model/webview-msg' -import { InputSummary } from './InputSummary' -import { IPostFormContext, PostFormContext } from './PostFormContext' -import PostEntryNameInput from './PostEntryNameInput' -import PostTitleInput from 'post-cfg/components/PostTitleInput' -import NestCategorySelect from './NestCategorySelect' import { Post } from '@/model/post' +import TitleInput from './input/TitleInput' +import { TagInput } from './input/TagInput' +import { CatSelect } from './select/CatSelect' +import { PwdInput } from './input/PwdInput' +import { SummaryInput } from './input/SummaryInput' +import { SiteHomeContributionOptionSelect } from './select/SiteHomeContributionOptionSelect' +import UrlSlugInput from './input/UrlSlugInput' +import { PermissionSelect } from './select/PermissionSelect' +import { SiteCatSelect } from './select/SiteCatSelect' +import { PersonalCategoryStore } from '../service/personal-category-store' -export type IPostFormProps = { - post?: Post +type Props = { + post: Post fileName?: string - useNestCategoriesSelect: boolean - onConfirm?: (postCfg: PostCfg) => void - onTitleChange?: (title: string) => void + onTitleChange: (title: string) => void } +type State = Post -export interface IPostFormState extends PostCfg {} - -export class PostForm extends React.Component { - static contextType?: React.Context = PostFormContext - declare context: React.ContextType - - constructor(props: IPostFormProps) { +export class PostForm extends Component { + constructor(props: Props) { super(props) - this.state = Object.assign({}, props.post ?? new Post()) + this.state = this.props.post } render() { - if (!this.props.post) return <> + const props = this.props + const state = this.state - const { disabled: isDisabled, status } = this.context return ( -
- - { - this.setState({ title: v ?? '' }) - this.props.onTitleChange?.(v ?? '') - }} - > - - - - {this.props.useNestCategoriesSelect ? ( - this.setState({ categoryIds })} - selected={this.state.categoryIds ?? []} - > - ) : ( - this.setState({ categoryIds })} - categoryIds={this.state.categoryIds ?? []} - /> - )} - - - this.setState({ tags })} /> - this.setState({ tags })} /> - { - this.setState({ accessPermission: value }) - }} - /> - this.setState(stateObj)} - /> - this.setState({ password: value })} - password={this.state.password ?? ''} - /> - this.setState({ description: summary })} - onFeatureImageChange={imageUrl => this.setState({ featuredImage: imageUrl })} - /> - - this.setState({ inSiteCandidate: v, inSiteHome: v ? false : this.state.inSiteHome }) - } - onInSiteHomeChange={v => - this.setState({ - inSiteHome: v, - inSiteCandidate: v ? false : this.state.inSiteCandidate, - }) - } - inSiteCandidate={this.state.inSiteCandidate} - inSiteHome={this.state.inSiteHome} - /> - this.setState({ siteCategoryId: categoryId })} - /> - this.setState({ entryName: value })} - /> - + + { + this.setState({ title: v ?? '' }) + this.props.onTitleChange?.(v ?? '') + }} + > + this.setState({ tags })} /> + this.setState({ categoryIds })} + /> + { + this.setState({ accessPermission: value }) + }} + /> + this.setState({ password: value })} password={state.password ?? ''} /> + this.setState({ description: summary })} + onFeatureImageChange={imageUrl => this.setState({ featuredImage: imageUrl })} + /> + + this.setState({ inSiteCandidate: v, inSiteHome: v ? false : state.inSiteHome }) + } + onInSiteHomeChange={v => + this.setState({ + inSiteHome: v, + inSiteCandidate: v ? false : state.inSiteCandidate, + }) + } + inSiteCandidate={state.inSiteCandidate} + inSiteHome={state.inSiteHome} + /> + this.setState({ siteCategoryId: categoryId })} + /> + this.setState({ entryName: value })} /> + this.setState(stateObj)} + /> +
- this.onConfirm()} - allowDisabledFocus - /> - this.onCancel()} /> - {status === 'submitting' ? : <>} + +
+ + this.confirm()} allowDisabledFocus /> + this.cancel()} /> - - +
+
) } - private onConfirm() { - this.context.set({ disabled: true, status: 'submitting' }) + private confirm() { getVsCodeApiSingleton().postMessage({ command: Webview.Cmd.Ext.uploadPost, - post: Object.assign({}, this.props.post, this.state), + post: this.state, } as WebviewMsg.UploadPostMsg) } - private onCancel() { + private cancel() { getVsCodeApiSingleton().postMessage({ command: Webview.Cmd.Ext.disposePanel } as WebviewMsg.Msg) } } diff --git a/ui/post-cfg/components/PostFormContext.ts b/ui/post-cfg/components/PostFormContext.ts deleted file mode 100644 index 35641b41..00000000 --- a/ui/post-cfg/components/PostFormContext.ts +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' - -export type IPostFormContext = { - disabled: boolean - status: 'loading' | 'submitting' | '' - set(v: Omit): void -} -export const defaultPostFormContext: IPostFormContext = { disabled: false, status: '', set: () => void 0 } -// eslint-disable-next-line @typescript-eslint/naming-convention -export const PostFormContext = React.createContext(defaultPostFormContext) diff --git a/ui/post-cfg/components/PostFormContextProvider.tsx b/ui/post-cfg/components/PostFormContextProvider.tsx deleted file mode 100644 index cb6f99b6..00000000 --- a/ui/post-cfg/components/PostFormContextProvider.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react' -import { PostFormContext, IPostFormContext, defaultPostFormContext } from './PostFormContext' -import { ReactNode } from 'react' - -export type IPostFormContextProviderProps = { - value?: Partial - children: ReactNode -} - -export type IPostFormContextProviderState = { - value: IPostFormContext -} - -export class PostFormContextProvider extends React.Component< - IPostFormContextProviderProps, - IPostFormContextProviderState -> { - constructor(props: IPostFormContextProviderProps) { - super(props) - const set = (value: IPostFormContext) => this.setState({ value: Object.assign(value, { set }) }) - this.state = { - value: Object.assign({}, defaultPostFormContext, { - set, - } as IPostFormContext), - } - } - - render() { - const { children } = this.props - return {children} - } -} diff --git a/ui/post-cfg/components/TagsInput.tsx b/ui/post-cfg/components/TagsInput.tsx deleted file mode 100644 index d4016739..00000000 --- a/ui/post-cfg/components/TagsInput.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { - ActionButton, - Icon, - ITag, - Label, - Stack, - TagItem, - TagItemSuggestion, - TagPicker, - ValidationState, - Text, -} from '@fluentui/react' -import React from 'react' -import { TagStore } from '../service/tag-store' -import { PostTags, PostTag } from '@/model/post-tag' - -export type ITagsInputProps = { - selectedTagNames?: string[] - onChange?: (tagNames: string[]) => void -} - -export type ITagsInputState = { - tags: PostTags - selectedTags: ITag[] -} - -export interface INewTag extends ITag { - readonly isNew: boolean -} - -export class TagsInput extends React.Component { - constructor(props: ITagsInputProps) { - super(props) - - const tags = TagStore.get() - this.state = { - tags, - selectedTags: - this.props.selectedTagNames - ?.map(name => tags.find(x => x.name === name)) - .filter(t => t) - .map(this.toITag) ?? [], - } - } - - render() { - return this.state ? ( - - - - this.filterSuggestedTags(text, selectedItems)} - pickerSuggestionsProps={{ - suggestionsHeaderText: '选择标签', - loadingText: '加载中', - }} - onRenderSuggestionsItem={tag => { - const isNewTag = (tag as INewTag).isNew - const tagEl = ( - - - {tag.name} - - ) - const el = isNewTag ? ( - - 新建标签: "{tagEl}" - - ) : ( - tagEl - ) - return ( - - {el} - - ) - }} - onRenderItem={props => { - const { item: tag } = props - return ( - - - - {tag.name} - - - ) - }} - getTextFromItem={item => item.name ?? ''} - selectedItems={this.state.selectedTags} - onChange={tags => { - tags ??= [] - tags = tags.filter(x => x) - this.props.onChange?.apply(this, [tags.map(t => t.name)]) - this.setState({ selectedTags: tags }) - }} - onEmptyResolveSuggestions={items => this.filterSuggestedTags('', items)} - inputProps={{ placeholder: '点击选择标签' }} - onValidateInput={input => - input?.length <= 50 ? ValidationState.valid : ValidationState.invalid - } - onInputChange={value => (value.length <= 50 ? value : value.substring(0, 49))} - /> - - - ) : ( - <> - ) - } - - private toITag(this: void, tag: PostTag): ITag { - return { name: tag.name, key: tag.id } - } - - private filterSuggestedTags(filterText: string, selectedTags?: ITag[]): ITag[] { - filterText = filterText?.trim() ?? '' - const { tags } = this.state - const filteredTags = tags - .filter( - tag => - (!filterText || tag.name.indexOf(filterText) >= 0) && - (!selectedTags || selectedTags.findIndex(st => st.name === tag.name) < 0) - ) - .map(x => ({ name: x.name, key: x.id }) as ITag) - if (filteredTags.length <= 0 || tags.findIndex(t => t.name.toLowerCase() === filterText.toLowerCase()) < 0) - filteredTags.push({ name: filterText, key: filterText, isNew: true } as INewTag) - - return filteredTags - } -} diff --git a/ui/post-cfg/components/PasswordInput.tsx b/ui/post-cfg/components/input/PwdInput.tsx similarity index 63% rename from ui/post-cfg/components/PasswordInput.tsx rename to ui/post-cfg/components/input/PwdInput.tsx index dc81d23a..04b5563c 100644 --- a/ui/post-cfg/components/PasswordInput.tsx +++ b/ui/post-cfg/components/input/PwdInput.tsx @@ -1,16 +1,14 @@ import { Label, Stack, TextField } from '@fluentui/react' -import React from 'react' +import React, { Component } from 'react' -export type IPasswordInputProps = { - password?: string - onChange?: (password: string) => void +type Props = { + password: string + onChange: (password: string) => void } -export class PasswordInput extends React.Component { - constructor(props: IPasswordInputProps) { +export class PwdInput extends Component { + constructor(props: Props) { super(props) - - this.state = {} } render() { @@ -19,7 +17,7 @@ export class PasswordInput extends React.Component { void this.props.onChange?.apply(this, [v])} + onChange={(_, v) => void this.props.onChange(v ?? '')} value={this.props.password} canRevealPassword > diff --git a/ui/post-cfg/components/InputSummary.tsx b/ui/post-cfg/components/input/SummaryInput.tsx similarity index 65% rename from ui/post-cfg/components/InputSummary.tsx rename to ui/post-cfg/components/input/SummaryInput.tsx index f3e8cb7b..6428e2b8 100644 --- a/ui/post-cfg/components/InputSummary.tsx +++ b/ui/post-cfg/components/input/SummaryInput.tsx @@ -2,36 +2,40 @@ import { ActionButton, Label, MessageBar, MessageBarType, Stack, TextField, Text import { ImgUploadStatusId } from '@/model/img-upload-status' import { Webview } from '@/model/webview-cmd' import { WebviewMsg } from '@/model/webview-msg' -import React from 'react' +import React, { Component } from 'react' import { getVsCodeApiSingleton } from 'share/vscode-api' -export type IInputSummaryProps = { - summary?: string - featureImageUrl?: string - onChange?: (summary: string) => void - onFeatureImageChange?: (imageUrl: string) => void +type Props = { + summary: string + featureImgUrl: string + onChange: (summary: string) => void + onFeatureImageChange: (imageUrl: string) => void } -export type IInputSummaryState = { +type State = { isCollapse: boolean disabled: boolean - errors?: string[] + errors: string[] } -export class InputSummary extends React.Component { +export class SummaryInput extends Component { private uploadingImageId = '' - constructor(props: IInputSummaryProps) { + constructor(props: Props) { super(props) - const { featureImageUrl, summary } = props + const { featureImgUrl, summary } = props - this.state = { isCollapse: !featureImageUrl && !summary, disabled: false } + this.state = { + isCollapse: featureImgUrl === '' && summary === '', + disabled: false, + errors: [], + } window.addEventListener('message', this.observerMessage) } render() { const isCollapse = this.state.isCollapse - const featureImageUrl = this.props.featureImageUrl + const featureImgUrl = this.props.featureImgUrl return ( @@ -39,13 +43,13 @@ export class InputSummary extends React.Component this.setState({ isCollapse: !isCollapse })} styles={{ root: { height: 'auto', paddingLeft: 0 } }} > - + - {!isCollapse && featureImageUrl ? ( + {!isCollapse && featureImgUrl !== '' ? ( void this.props.onFeatureImageChange?.apply(this, [''])} styles={{ root: { height: 'auto', paddingLeft: 0 } }} @@ -86,10 +90,10 @@ export class InputSummary extends React.Component{this.renderFeatureImage()} - {this.state.errors ? ( + {this.state.errors.length > 0 ? ( { - this.setState({ errors: undefined }) + this.setState({ errors: [] }) }} messageBarType={MessageBarType.error} > @@ -104,43 +108,49 @@ export class InputSummary extends React.Component) => { const data = ev.data as WebviewMsg.Msg - 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 ?? '']) - } + if (data.command !== Webview.Cmd.Ui.updateImageUploadStatus) return + + const msg = data as WebviewMsg.UpdateImgUpdateStatusMsg + const { imageId, status } = msg + if (imageId === this.uploadingImageId) { + this.setState({ disabled: status.id === ImgUploadStatusId.uploading }) + if (status.id === ImgUploadStatusId.uploaded && this.props.onFeatureImageChange !== undefined) + this.props.onFeatureImageChange(status.imageUrl ?? '') } } private uploadFeatureImage() { this.uploadingImageId = `${Date.now()}` - getVsCodeApiSingleton().postMessage({ + + const msg = { command: Webview.Cmd.Ext.uploadImg, imageId: this.uploadingImageId, - } as WebviewMsg.UploadImgMsg) + } as WebviewMsg.UploadImgMsg + + getVsCodeApiSingleton().postMessage(msg) } private renderFeatureImage() { - const { featureImageUrl } = this.props - if (featureImageUrl === undefined) { + const featureImgUrl = this.props.featureImgUrl + + if (featureImgUrl !== '') { return ( - this.uploadFeatureImage()} - width={135} - styles={{ root: { height: 70 } }} - iconProps={{ iconName: 'Add' }} - disabled={this.state.disabled} - > - 上传图片 - + + 题图 + ) } + return ( - - 题图 - + this.uploadFeatureImage()} + width={135} + styles={{ root: { height: 70 } }} + iconProps={{ iconName: 'Add' }} + disabled={this.state.disabled} + > + 上传图片 + ) } } diff --git a/ui/post-cfg/components/input/TagInput.tsx b/ui/post-cfg/components/input/TagInput.tsx new file mode 100644 index 00000000..52482fd4 --- /dev/null +++ b/ui/post-cfg/components/input/TagInput.tsx @@ -0,0 +1,125 @@ +import { + ActionButton, + Icon, + ITag, + Label, + Stack, + TagItem, + TagItemSuggestion, + TagPicker, + ValidationState, + Text, +} from '@fluentui/react' +import React, { Component } from 'react' +import { PostTag } from '@/model/post-tag' +import { TagStore } from '../../service/tag-store' + +type Props = { + selectedTagNames: string[] + onChange: (tagNames: string[]) => void +} + +type State = { + tags: PostTag[] + selectedTags: ITag[] +} + +export class TagInput extends Component { + constructor(props: Props) { + super(props) + + const tags = TagStore.get() + + const selectedTags = this.props.selectedTagNames + .map(name => tags.find(x => x.name === name)) + .filter(tag => tag !== undefined) + .map(tag => tag as PostTag) + .map(tag => ({ name: tag.name, key: tag.id })) + + this.state = { + tags, + selectedTags, + } + } + + render() { + return ( + + + + filterSuggestedTags(this.state, text, selectedItems ?? []) + } + onRenderSuggestionsItem={tag => { + const isNewTag = (tag as NewTag).isNew ?? false + const tagEl = ( + + + {tag.name} + + ) + const el = isNewTag ? ( + + 新建标签: "{tagEl}" + + ) : ( + tagEl + ) + return ( + + {el} + + ) + }} + onRenderItem={props => { + const { item: tag } = props + return ( + + + + {tag.name} + + + ) + }} + getTextFromItem={item => item.name ?? ''} + selectedItems={this.state.selectedTags} + onChange={tags => { + if (tags !== undefined) { + this.props.onChange(tags.map(t => t.name)) + this.setState({ selectedTags: tags }) + } + }} + onEmptyResolveSuggestions={items => filterSuggestedTags(this.state, '', items ?? [])} + inputProps={{ placeholder: '点击选择' }} + onValidateInput={input => (input.length <= 50 ? ValidationState.valid : ValidationState.invalid)} + onInputChange={value => (value.length <= 50 ? value : value.substring(0, 49))} + /> + + ) + } +} + +type NewTag = { + name: string + key: string | number + isNew?: boolean +} + +function filterSuggestedTags(state: State, filterText: string, selectedTags: ITag[]) { + filterText = filterText.trim() + + const filteredTags = state.tags + .filter( + tag => + (filterText === '' || tag.name.indexOf(filterText) >= 0) && + selectedTags.findIndex(st => st.name === tag.name) < 0 + ) + .map(x => ({ name: x.name, key: x.id }) as ITag) + + if (filteredTags.length <= 0 || state.tags.findIndex(t => t.name.toLowerCase() === filterText.toLowerCase()) < 0) + filteredTags.push({ name: filterText, key: filterText, isNew: true } as NewTag) + + return filteredTags +} diff --git a/ui/post-cfg/components/PostTitleInput.tsx b/ui/post-cfg/components/input/TitleInput.tsx similarity index 65% rename from ui/post-cfg/components/PostTitleInput.tsx rename to ui/post-cfg/components/input/TitleInput.tsx index f5c45f15..2a71536d 100644 --- a/ui/post-cfg/components/PostTitleInput.tsx +++ b/ui/post-cfg/components/input/TitleInput.tsx @@ -1,34 +1,27 @@ import { ActionButton, Label, Stack, Text, TextField } from '@fluentui/react' -import React from 'react' +import React, { Component } from 'react' -export type IPostTitleInputProps = { +type Props = { value: string fileName: string onChange: (value: string | null | undefined) => unknown } -export type IPostTitleInputState = { - value: IPostTitleInputProps['value'] -} - -export default class PostTitleInput extends React.Component { - constructor(props: IPostTitleInputProps) { +export default class TitleInput extends Component { + constructor(props: Props) { super(props) - this.state = { - value: props.value, - } } render() { return ( - + + - - {this.props.fileName && this.props.fileName !== this.state.value ? ( + {this.props.fileName !== '' && this.props.fileName !== this.props.value ? ( { this.setState({ value: this.props.fileName }) - this.props.onChange(this.state.value) + this.props.onChange(this.props.value) }} styles={{ root: { height: 'auto', whiteSpace: 'nowrap' } }} secondaryText={this.props.fileName} @@ -44,7 +37,7 @@ export default class PostTitleInput extends React.Component { this.setState({ value: v ?? '' }) this.props.onChange(v) diff --git a/ui/post-cfg/components/PostEntryNameInput.tsx b/ui/post-cfg/components/input/UrlSlugInput.tsx similarity index 86% rename from ui/post-cfg/components/PostEntryNameInput.tsx rename to ui/post-cfg/components/input/UrlSlugInput.tsx index 7d2ee532..385fb5a1 100644 --- a/ui/post-cfg/components/PostEntryNameInput.tsx +++ b/ui/post-cfg/components/input/UrlSlugInput.tsx @@ -1,24 +1,23 @@ import { ActionButton, ITextField, Label, Stack, TextField } from '@fluentui/react' import * as React from 'react' +import { Component } from 'react' -export type IPostEntryNameInputProps = { - entryName?: string - onChange?: (value: string) => void +type Props = { + entryName: string + onChange: (value: string) => void } -export type IPostEntryNameInputState = { +type State = { isCollapsed: boolean } -export default class PostEntryNameInput extends React.Component { +export default class UrlSlugInput extends Component { textFieldComp?: ITextField | null - constructor(props: IPostEntryNameInputProps) { + constructor(props: Props) { super(props) - this.state = { - isCollapsed: true, - } + this.state = { isCollapsed: true } } render() { diff --git a/ui/post-cfg/components/select/CatSelect.tsx b/ui/post-cfg/components/select/CatSelect.tsx new file mode 100644 index 00000000..21391c5f --- /dev/null +++ b/ui/post-cfg/components/select/CatSelect.tsx @@ -0,0 +1,48 @@ +import { ComboBox } from '@fluentui/react' +import { Component } from 'react' +import { PostCategory } from '@/model/post-category' + +type Props = { + allCats: PostCategory[] + selectedCatIds: number[] + onChange: (categoryIds: number[]) => void +} +type State = { selectedCatIds: number[] } + +export class CatSelect extends Component { + constructor(props: Props) { + super(props) + this.state = { selectedCatIds: this.props.selectedCatIds } + } + + render() { + const opts = this.props.allCats.map(cat => ({ + data: cat.categoryId, + key: cat.categoryId, + text: cat.title, + })) + + return ( + { + if (opt !== undefined) { + if (val === undefined) { + const selectedCatIds = this.state.selectedCatIds.filter(x => x !== opt.data) + this.setState({ selectedCatIds }) + } else { + this.state.selectedCatIds.push(opt.data as number) + this.setState(this.state) + } + } + this.props.onChange(this.state.selectedCatIds) + }} + /> + ) + } +} diff --git a/ui/post-cfg/components/select/PermissionSelect.tsx b/ui/post-cfg/components/select/PermissionSelect.tsx new file mode 100644 index 00000000..f7ac4760 --- /dev/null +++ b/ui/post-cfg/components/select/PermissionSelect.tsx @@ -0,0 +1,47 @@ +import { ChoiceGroup, IChoiceGroupOption, Label, Stack } from '@fluentui/react' +import { AccessPermission } from '@/model/post' +import React, { Component } from 'react' + +type Props = { + accessPermission: AccessPermission + onChange: (ap: AccessPermission) => void +} + +export class PermissionSelect extends Component { + constructor(props: Props) { + super(props) + } + + render() { + const opt: IChoiceGroupOption[] = [ + { + text: '所有人', + key: AccessPermission.undeclared.toString(), + value: AccessPermission.undeclared, + }, + { + text: '登录用户', + key: AccessPermission.authenticated.toString(), + value: AccessPermission.authenticated, + }, + { + text: '仅自己', + key: AccessPermission.owner.toString(), + value: AccessPermission.owner, + }, + ] + return ( + + + { + if (option !== undefined) this.props.onChange(option.value as AccessPermission) + }} + selectedKey={this.props.accessPermission?.toString()} + styles={{ flexContainer: { display: 'flex', justifyContent: 'space-between' } }} + /> + + ) + } +} diff --git a/ui/post-cfg/components/SiteCategorySelector.tsx b/ui/post-cfg/components/select/SiteCatSelect.tsx similarity index 84% rename from ui/post-cfg/components/SiteCategorySelector.tsx rename to ui/post-cfg/components/select/SiteCatSelect.tsx index 14745a0e..e74fae0d 100644 --- a/ui/post-cfg/components/SiteCategorySelector.tsx +++ b/ui/post-cfg/components/select/SiteCatSelect.tsx @@ -1,28 +1,28 @@ import { ActionButton, Checkbox, Label, Stack } from '@fluentui/react' import { SiteCategory } from '@/model/site-category' -import React from 'react' -import { SiteCategoryStore } from '../service/site-category-store' +import React, { Component } from 'react' +import { SiteCategoryStore } from '../../service/site-category-store' -export type ISiteCategoriesSelectorProps = { - categoryIds?: number[] - onChange?: (siteCategoryId: number) => void +type Props = { + categoryIds: number[] + onChange: (siteCategoryId: number) => void } -export type ISiteCategoriesSelectorState = { +type State = { siteCategories: SiteCategory[] isCollapsed: boolean categoryIds: number[] categoryExpandState: { [key: number]: boolean | undefined } } -export class SiteCategorySelector extends React.Component { - constructor(props: ISiteCategoriesSelectorProps) { +export class SiteCatSelect extends Component { + constructor(props: Props) { super(props) const siteCategories = SiteCategoryStore.get() const categoryExpandState: { selectedParentCategoryId?: boolean } = {} let selectedParentCategoryId = -1 - if (props.categoryIds && props.categoryIds.length > 0) { + if (props.categoryIds.length > 0) { selectedParentCategoryId = siteCategories.find(x => x.children.find(child => child.id === props.categoryIds?.[0]))?.id ?? -1 } @@ -31,7 +31,7 @@ export class SiteCategorySelector extends React.Component { const { children, id: parentId } = parent const { categoryExpandState } = this.state - const isParentExpanded = !!categoryExpandState[parentId] + const isParentExpanded = categoryExpandState[parentId] === true const groupItems = children.map(child => ( - {this.state.categoryExpandState[parentId] ? ( + {this.state.categoryExpandState[parentId] === true ? ( {groupItems} @@ -119,8 +119,8 @@ export class SiteCategorySelector extends React.Component void onInSiteCandidateChange: (value: boolean) => void } -export type ISiteHomeContributionOptionsSelector = { - isCollapse: boolean -} - -export class SiteHomeContributionOptionsSelector extends React.Component< - ISiteHomeContributionOptionsSelectorProps, - ISiteHomeContributionOptionsSelector -> { - onChange: ((options: ISiteHomeContributionOptions) => void) | null = null +type State = { isCollapse: boolean } - constructor(props: ISiteHomeContributionOptionsSelectorProps) { +export class SiteHomeContributionOptionSelect extends Component { + constructor(props: Props) { super(props) this.state = { isCollapse: true } } diff --git a/ui/post-cfg/model/site-home-contribution-options.ts b/ui/post-cfg/model/site-home-contribution-options.ts deleted file mode 100644 index bb00b61a..00000000 --- a/ui/post-cfg/model/site-home-contribution-options.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type SiteHomeContributionOptions = { - inSiteCandidate: boolean - inSiteHome: boolean -} diff --git a/ui/post-cfg/service/personal-category-store.ts b/ui/post-cfg/service/personal-category-store.ts index 6065af77..8a296676 100644 --- a/ui/post-cfg/service/personal-category-store.ts +++ b/ui/post-cfg/service/personal-category-store.ts @@ -34,7 +34,6 @@ export namespace PersonalCategoryStore { }: { data: WebviewCommonCmd }) => { - console.log('onUpdate', message) if (message.payload.parentId === parent) { clearTimeout(timeoutId)