From ee955b0c5c37feb914cb4c0772a406a3671df17d Mon Sep 17 00:00:00 2001 From: terwer Date: Thu, 21 Mar 2024 23:00:05 +0800 Subject: [PATCH] feat: upload list --- libs/zhi-siyuan-picgo/package.json | 6 +- libs/zhi-siyuan-picgo/src/index.spec.ts | 5 +- libs/zhi-siyuan-picgo/src/index.ts | 5 +- .../src/lib/models/ImageItem.ts | 54 ++++ .../src/lib/models/ParsedImage.ts | 40 +++ .../src/lib/models/PicgoPostResult.ts | 37 +++ .../src/lib/parser/ImageParser.ts | 242 +++++++++++++++++ .../src/lib/siyuanPicgoPostApi.ts | 255 +++++++++++++++++- .../zhi-siyuan-picgo/src/lib/utils/md5Util.ts | 26 ++ libs/zhi-siyuan-picgo/tsconfig.json | 3 +- packages/picgo-plugin-app/custom.d.ts | 8 - packages/picgo-plugin-app/package.json | 5 +- .../src/components/home/DragUpload.vue | 16 ++ .../src/components/home/ElectronIndex.vue | 78 +++++- .../src/components/home/PictureList.vue | 139 ++++++++++ .../src/components/home/UploadButton.vue | 144 ++++++++++ .../src/composables/usePicgoCommon.ts | 48 ++++ .../src/composables/usePicgoInitPage.ts | 97 +++++++ .../src/composables/usePicgoManage.ts | 142 ++++++++++ .../src/composables/usePicgoUpload.ts | 226 ++++++++++++++++ .../src/composables/useSiyuanApi.ts | 63 +++++ .../src/models/SiyuanConfig.ts | 38 --- .../src/stores/useSiyuanSetting.ts | 12 +- .../picgo-plugin-app/src/utils/siyuanPicgo.ts | 6 +- packages/picgo-plugin-app/src/utils/utils.ts | 79 ++++++ pnpm-lock.yaml | 41 +++ 26 files changed, 1744 insertions(+), 71 deletions(-) create mode 100644 libs/zhi-siyuan-picgo/src/lib/models/ImageItem.ts create mode 100644 libs/zhi-siyuan-picgo/src/lib/models/ParsedImage.ts create mode 100644 libs/zhi-siyuan-picgo/src/lib/models/PicgoPostResult.ts create mode 100644 libs/zhi-siyuan-picgo/src/lib/parser/ImageParser.ts create mode 100644 libs/zhi-siyuan-picgo/src/lib/utils/md5Util.ts delete mode 100644 packages/picgo-plugin-app/custom.d.ts create mode 100644 packages/picgo-plugin-app/src/components/home/DragUpload.vue create mode 100644 packages/picgo-plugin-app/src/components/home/PictureList.vue create mode 100644 packages/picgo-plugin-app/src/components/home/UploadButton.vue create mode 100644 packages/picgo-plugin-app/src/composables/usePicgoCommon.ts create mode 100644 packages/picgo-plugin-app/src/composables/usePicgoInitPage.ts create mode 100644 packages/picgo-plugin-app/src/composables/usePicgoManage.ts create mode 100644 packages/picgo-plugin-app/src/composables/usePicgoUpload.ts create mode 100644 packages/picgo-plugin-app/src/composables/useSiyuanApi.ts delete mode 100644 packages/picgo-plugin-app/src/models/SiyuanConfig.ts create mode 100644 packages/picgo-plugin-app/src/utils/utils.ts diff --git a/libs/zhi-siyuan-picgo/package.json b/libs/zhi-siyuan-picgo/package.json index d56ab93..5d184dc 100644 --- a/libs/zhi-siyuan-picgo/package.json +++ b/libs/zhi-siyuan-picgo/package.json @@ -29,8 +29,12 @@ "@terwer/vite-config-custom": "^0.7.6" }, "dependencies": { + "js-md5": "^0.8.3", + "universal-picgo": "workspace:*", + "zhi-common": "^1.31.0", + "zhi-device": "^2.11.0", "zhi-lib-base": "^0.8.0", - "universal-picgo": "workspace:*" + "zhi-siyuan-api": "^2.18.6" }, "publishConfig": { "access": "public" diff --git a/libs/zhi-siyuan-picgo/src/index.spec.ts b/libs/zhi-siyuan-picgo/src/index.spec.ts index 1bbf6ed..141ad47 100644 --- a/libs/zhi-siyuan-picgo/src/index.spec.ts +++ b/libs/zhi-siyuan-picgo/src/index.spec.ts @@ -1,7 +1,8 @@ import { describe, it } from "vitest" +import { getFileHash } from "./lib/utils/md5Util" describe("index", () => { it("test index", () => { - console.log("hello") + console.log(getFileHash("hello")) }) -}) \ No newline at end of file +}) diff --git a/libs/zhi-siyuan-picgo/src/index.ts b/libs/zhi-siyuan-picgo/src/index.ts index 1983134..00b3768 100644 --- a/libs/zhi-siyuan-picgo/src/index.ts +++ b/libs/zhi-siyuan-picgo/src/index.ts @@ -19,8 +19,9 @@ import { PicgoTypeEnum, PluginLoaderDb, } from "universal-picgo" +import { SiyuanConfig as SiyuanPicgoConfig } from "zhi-siyuan-api" -export { SiyuanPicgoPostApi } +export { SiyuanPicgoConfig, SiyuanPicgoPostApi } export { ConfigDb, PluginLoaderDb, ExternalPicgoConfigDb } export { PicgoTypeEnum } -export { type IPicGo, type IImgInfo, type IPicgoDb, type IConfig, type IExternalPicgoConfig } +export { type IPicGo, type IImgInfo, type IPicgoDb, type IConfig, type IExternalPicgoConfig } \ No newline at end of file diff --git a/libs/zhi-siyuan-picgo/src/lib/models/ImageItem.ts b/libs/zhi-siyuan-picgo/src/lib/models/ImageItem.ts new file mode 100644 index 0000000..34c48d0 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/models/ImageItem.ts @@ -0,0 +1,54 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2023-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { getFileHash } from "../utils/md5Util" + +/** + * 图片信息 + */ +export class ImageItem { + /** + * 文件, + */ + name: string + /** + * 文件名称的Hash,构造函数指定 + */ + hash: string + /** + * 原始资源地址 + */ + originUrl: string + /** + * 资源地址 + */ + url: string + /** + * 资源备注 + */ + alt?: string + /** + * 标题 + */ + title?: string + /** + * 是否本地 + */ + isLocal: boolean + + constructor(originUrl: string, url: string, isLocal: boolean, alt?: string, title?: string) { + this.originUrl = originUrl + this.name = originUrl.substring(originUrl.lastIndexOf("/") + 1) + this.hash = getFileHash(this.name) + this.url = url + this.isLocal = isLocal + this.alt = alt ?? "" + this.title = title ?? "" + } +} diff --git a/libs/zhi-siyuan-picgo/src/lib/models/ParsedImage.ts b/libs/zhi-siyuan-picgo/src/lib/models/ParsedImage.ts new file mode 100644 index 0000000..bda9996 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/models/ParsedImage.ts @@ -0,0 +1,40 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +/** + * 解析的图片 + * + * @author terwer + * @since 0.8.0 + */ +export class ParsedImage { + /** + * 链接 + */ + url: string + /** + * 备注 + */ + alt: string + /** + * 标题 + */ + title: string + /** + * 是否本地 + */ + isLocal: boolean + + constructor() { + this.url = "" + this.isLocal = false + this.alt = "" + this.title = "" + } +} diff --git a/libs/zhi-siyuan-picgo/src/lib/models/PicgoPostResult.ts b/libs/zhi-siyuan-picgo/src/lib/models/PicgoPostResult.ts new file mode 100644 index 0000000..b7f8d9d --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/models/PicgoPostResult.ts @@ -0,0 +1,37 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +/** + * Picgo处理文章统一返回结果 + */ +export class PicgoPostResult { + /** + * 是否成功 + */ + flag: boolean + /** + * 是否有图片 + */ + hasImages: boolean + /** + * 处理后的文章链接 + */ + mdContent: string + /** + * 错误信息 + */ + errmsg: string + + constructor() { + this.flag = false + this.hasImages = false + this.mdContent = "" + this.errmsg = "" + } +} diff --git a/libs/zhi-siyuan-picgo/src/lib/parser/ImageParser.ts b/libs/zhi-siyuan-picgo/src/lib/parser/ImageParser.ts new file mode 100644 index 0000000..debf582 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/parser/ImageParser.ts @@ -0,0 +1,242 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { ILogger, simpleLogger } from "zhi-lib-base" +import { ParsedImage } from "../models/ParsedImage" +import { ImageItem } from "../models/ImageItem" + +/** + * 图片解析器 + * + * 自动解析文章中的img标签, + * 自动处理src外链、base64数据 + * + * @author terwer + * @since 0.1.0 + */ +export class ImageParser { + private readonly logger: ILogger + + constructor(isDev?: boolean) { + this.logger = simpleLogger("image-parser", "zhi-siyuan-picgo", isDev) + } + + /** + * 检测是否有外链图片 + * + * @param content 文章正文 + */ + public hasExternalImages(content: string): boolean { + const flag = false + + const imgRegex = /!\[.*]\((http|https):\/.*\/.*\)/g + const matches = content.match(imgRegex) + if (matches != null && matches.length > 0) { + return true + } + + const imgBase64Regex = /!\[.*]\((data:image):\/.*\/.*\)/g + const base64Matches = content.match(imgBase64Regex) + if (base64Matches != null && base64Matches.length > 0) { + return true + } + + return flag + } + + /** + * 剔除外链图片 + * + * @param content 文章正文 + */ + public removeImages(content: string): string { + let newcontent = content + + newcontent = newcontent.replace(/!\[.*]\((http|https):\/.*\/.*\)/g, "") + + return newcontent + } + + /** + * 解析图片块为图片链接 + * + * @param content 图片块 + * @private + */ + public parseImagesToArray(content: string): ParsedImage[] { + let ret = [] as ParsedImage[] + const remoteImages = this.parseRemoteImagesToArray(content) + const localImages = this.parseLocalImagesToArray(content) + + // 会有很多重复值 + // ret = ret.concat(remoteImages, localImages) + // 下面的写法可以去重 + ret = [...new Set([...remoteImages, ...localImages])] + + return ret + } + + /** + * 解析图片块为远程图片链接 + * + * @param markdownText 图片块 + * @private + */ + public parseRemoteImagesToArray(markdownText: string): ParsedImage[] { + this.logger.debug("准备解析文本中的远程图片=>", markdownText) + // 定义正则表达式来匹配以 http 或 https 开头的 Markdown 格式的图片链接 + // 只能匹配有属性和备注的情况 + // const regex = /!\[(.*?)\]\(((https|http|ftp)?:\/\/[^\s/$.?#].[^\s]*)\s*"(.*?)"\)\s*{:\s*([^\n]*)}/g + // 同时兼容有属性和没有属性的情况 + const regex = /!\[(.*?)\]\((https?:\/\/\S+\.(?:jpe?g|png|gif))(?:\s+"(?:[^"\\]|\\.)*")?\s*(?:{:\s*([^\n]*)})?\)/g + + // 匹配普通图片链接: + // ![Cat](https://example.com/cat.png) + // 匹配结果: + // match[1]: "Cat" + // match[2]: "https://example.com/cat.png" + // match[3]: undefined + + // 匹配带注释的图片链接: + // ![Dog](https://example.com/dog.jpg "A dog in the park") + // 匹配结果: + // match[1]: "Dog" + // match[2]: "https://example.com/dog.jpg" + // match[3]: "A dog in the park" + + // 匹配带属性的图片链接: + // ![Fish](https://example.com/fish.gif){width=200 height=150} + // 匹配结果: + // match[1]: "Fish" + // match[2]: "https://example.com/fish.gif" + // match[3]: "width=200 height=150" + + // 使用正则表达式来匹配 Markdown 格式的图片链接,并提取其中的各个属性 + const ParsedImages = [] + for (const match of markdownText.matchAll(regex)) { + const altText = match[1] ? match[1] : "" + const url = match[2] ? match[2] : "" + const title = match[3] ? match[3].replace(/"/g, "") : "" + + // 将图片链接的各个属性封装成一个对象,并添加到数组中 + ParsedImages.push({ + url, + alt: altText, + title, + isLocal: false, + }) + } + this.logger.debug("远程图片解析完毕.", ParsedImages) + return ParsedImages + } + + /** + * 解析图片块为本地图片链接 + * + * @param markdownText 图片块 + */ + public parseLocalImagesToArray(markdownText: string): ParsedImage[] { + this.logger.debug("准备解析文本中的本地图片=>", markdownText) + + // 定义正则表达式来匹配以 assets 开头但不以 http、https 或 ftp 开头的 Markdown 格式的图片链接 + // const regex = /!\[(.*?)\]\(((?!http|https|ftp)assets\/.*?)\s*("(?:.*[^"])")?\)\s*(\{(?:.*[^"])})?/g + const regex = /!\[(.*?)\]\(((?!http|https|ftp)assets\/.*?)\s*("(?:[^"\\]|\\.)*")?\s*(\{(?:[^"\\]|\\.)*\})?\)/g + // 这样的正则表达式可以同时匹配到以下格式的图片链接: + // ![图片](assets/image.png) + // ![带注释的图片](assets/image.png "注释") + // ![带属性的图片](assets/image.png){width=100 height=200} + + // 使用正则表达式来匹配 Markdown 格式的图片链接,并提取其中的各个属性 + const ParsedImages = [] + for (const match of markdownText.matchAll(regex)) { + const altText = match[1] ? match[1] : "" + const url = match[2] ? match[2] : "" + const title = match[3] ? match[3].replace(/"/g, "") : "" + + // 将图片链接的各个属性封装成一个对象,并添加到数组中 + ParsedImages.push({ + url, + alt: altText, + title, + isLocal: true, + }) + } + this.logger.debug("本地图片解析完毕.", ParsedImages) + return ParsedImages + } + + /** + * 将外链外链图片替换为图床链接 + * + * @param content 正文 + * @param replaceMap 替换信息 + */ + public replaceImagesWithImageItemArray(content: string, replaceMap: any): string { + let newcontent = content + + const imgRegex = /!\[.*]\(assets\/.*\..*\)/g + const matches = newcontent.match(imgRegex) + // 没有图片,无需处理 + if (matches == null || matches.length === 0) { + this.logger.warn("未匹配到本地图片,将不会替换图片链接") + return newcontent + } + + for (let i = 0; i < matches.length; i++) { + const img = matches[i] + this.logger.debug("img=>", img) + + const src = img.replace(/!\[.*]\(/g, "").replace(/\)/, "") + this.logger.debug("src=>", src) + let url + let title + const urlAttrs = src.split(" ") + if (urlAttrs.length > 1) { + url = urlAttrs[0] + title = urlAttrs[1].replace(/"/g, "") + } else { + url = urlAttrs[0] + } + + const tempImageItem = new ImageItem(url, "", true) + const hash = tempImageItem.hash + const replaceImageItem: ImageItem = replaceMap[hash] + const alt = replaceImageItem?.alt ?? "" + + let newImg = `![${alt}](${replaceImageItem?.url})` + if (title) { + newImg = `![${alt}](${replaceImageItem?.url} "${title}")` + } + this.logger.debug("newImg=>", newImg) + + // 使用正则表达式和replace方法来实现replaceAll方法 + // 将search转换为正则表达式,使用g标志表示全局匹配 + const imgRegex = new RegExp(img, "g") + newcontent = newcontent.replace(imgRegex, newImg) + } + + return newcontent + } + + /** + * 下载图片到本地并打包成zip + * + * @@deprecated 不再支持 + */ + // public async downloadMdWithImages(): Promise {} + + /** + * 下载图片到本地并保存到思源 + * + * @deprecated 思源笔记已经有此功能 + */ + // public async downloadImagesToSiyuan(): Promise { + // throw new Error("思源笔记已经有此功能,无需重新实现") + // } +} diff --git a/libs/zhi-siyuan-picgo/src/lib/siyuanPicgoPostApi.ts b/libs/zhi-siyuan-picgo/src/lib/siyuanPicgoPostApi.ts index 66b20a3..7e68ec2 100644 --- a/libs/zhi-siyuan-picgo/src/lib/siyuanPicgoPostApi.ts +++ b/libs/zhi-siyuan-picgo/src/lib/siyuanPicgoPostApi.ts @@ -9,22 +9,48 @@ import { ILogger, simpleLogger } from "zhi-lib-base" import { SiyuanPicGoUploadApi } from "./siyuanPicGoUploadApi" -import { hasNodeEnv, IImgInfo, win } from "universal-picgo" -import { IPicGo } from "universal-picgo" +import { hasNodeEnv, IImgInfo, IPicGo, win } from "universal-picgo" +import { ParsedImage } from "./models/ParsedImage" +import { ImageItem } from "./models/ImageItem" +import { SIYUAN_PICGO_FILE_MAP_KEY } from "./constants" +import { JsonUtil, StrUtil } from "zhi-common" +import { SiyuanConfig, SiyuanKernelApi } from "zhi-siyuan-api" +import { ImageParser } from "./parser/ImageParser" +import { PicgoPostResult } from "./models/PicgoPostResult" +import { DeviceDetection, DeviceTypeEnum, SiyuanDevice } from "zhi-device" /** * Picgo与文章交互的通用方法 */ class SiyuanPicgoPostApi { private readonly logger: ILogger - // private readonly imageParser: ImageParser - // private readonly siyuanApi: SiyuanKernelApi - // private readonly siyuanConfig: SiyuanConfig + private readonly imageParser: ImageParser + private readonly siyuanApi: SiyuanKernelApi + private readonly siyuanConfig: SiyuanConfig + private readonly isSiyuanOrSiyuanNewWin: boolean private readonly picgoApi: SiyuanPicGoUploadApi public cfgUpdating: boolean - constructor(isDev?: boolean) { + constructor(siyuanConfig: SiyuanConfig, isDev?: boolean) { this.logger = simpleLogger("picgo-post-api", "zhi-siyuan-picgo", isDev) + + this.imageParser = new ImageParser(isDev) + + this.siyuanConfig = siyuanConfig + this.siyuanApi = new SiyuanKernelApi(siyuanConfig) + + this.isSiyuanOrSiyuanNewWin = (() => { + const deviceType = DeviceDetection.getDevice() + // 三种情况,主窗口、挂件、新窗口 + const isSiyuanOrSiyuanNewWin = + deviceType === DeviceTypeEnum.DeviceType_Siyuan_MainWin || + deviceType === DeviceTypeEnum.DeviceType_Siyuan_RendererWin || + deviceType === DeviceTypeEnum.DeviceType_Siyuan_Widget + this.logger.debug("deviceType=>", deviceType) + this.logger.debug("isSiyuanOrSiyuanNewWin=>", String(isSiyuanOrSiyuanNewWin)) + return isSiyuanOrSiyuanNewWin + })() + // 初始化 PicGO this.picgoApi = new SiyuanPicGoUploadApi(isDev) this.cfgUpdating = false @@ -40,7 +66,7 @@ class SiyuanPicgoPostApi { } /** - * 上传图片到PicGO + * 上传图片到PicGO,此方法不会修改元数据 * * @param input 路径数组,可为空,为空上传剪贴板 */ @@ -48,6 +74,221 @@ class SiyuanPicgoPostApi { return this.picgoApi.upload(input) } + /** + * 将字符串数组格式的图片信息转换成图片对象数组 + * + * @param attrs 文章属性 + * @param retImgs 字符串数组格式的图片信息 + * @param imageBaseUrl - 本地图片前缀,一般是思源的地址 + */ + public async doConvertImagesToImagesItemArray( + attrs: any, + retImgs: ParsedImage[], + imageBaseUrl?: string + ): Promise { + const ret = [] as ImageItem[] + for (let i = 0; i < retImgs.length; i++) { + const retImg = retImgs[i] + const originUrl = retImg.url + let imgUrl = retImg.url + + // 获取属性存储的映射数据 + let fileMap = {} as any + this.logger.debug("attrs=>", attrs) + if (!StrUtil.isEmptyString(attrs[SIYUAN_PICGO_FILE_MAP_KEY])) { + fileMap = JsonUtil.safeParse(attrs[SIYUAN_PICGO_FILE_MAP_KEY], {}) + this.logger.debug("fileMap=>", fileMap) + } + + // 处理思源本地图片预览 + // 这个是从思源查出来解析的是否是本地 + if (retImg.isLocal) { + const baseUrl = imageBaseUrl ?? this.siyuanConfig.apiUrl ?? "" + imgUrl = StrUtil.pathJoin(baseUrl, "/" + imgUrl) + } + + const imageItem = new ImageItem(originUrl, imgUrl, retImg.isLocal, retImg.alt, retImg.title) + // fileMap 查出来的是是否上传,上传了,isLocal就false + if (fileMap[imageItem.hash]) { + const newImageItem = fileMap[imageItem.hash] + this.logger.debug("newImageItem=>", newImageItem) + if (!newImageItem.isLocal) { + imageItem.isLocal = false + imageItem.url = newImageItem.url + } + } + + // imageItem.originUrl = decodeURIComponent(imageItem.originUrl) + imageItem.url = decodeURIComponent(imageItem.url) + this.logger.debug("imageItem=>", imageItem) + ret.push(imageItem) + } + + this.logger.debug("ret=>", ret) + return ret + } + + /** + * 上传当前文章图片到图床(提供给外部调用) + * + * @param pageId 文章ID + * @param attrs 文章属性 + * @param mdContent 文章的Markdown文本 + */ + public async uploadPostImagesToBed(pageId: string, attrs: any, mdContent: string): Promise { + const ret = new PicgoPostResult() + + const localImages = this.imageParser.parseLocalImagesToArray(mdContent) + const uniqueLocalImages = [...new Set([...localImages])] + this.logger.debug("uniqueLocalImages=>", uniqueLocalImages) + + if (uniqueLocalImages.length === 0) { + ret.flag = false + ret.hasImages = false + ret.mdContent = mdContent + ret.errmsg = "文章中没有图片" + return ret + } + + // 开始上传 + try { + ret.hasImages = true + + const imageItemArray = await this.doConvertImagesToImagesItemArray(attrs, uniqueLocalImages) + + const replaceMap = {} as any + let hasLocalImages = false + for (let i = 0; i < imageItemArray.length; i++) { + const imageItem = imageItemArray[i] + if (imageItem.originUrl.includes("assets")) { + replaceMap[imageItem.hash] = imageItem + } + + if (!imageItem.isLocal) { + this.logger.debug("已经上传过图床,请勿重复上传=>", imageItem.originUrl) + continue + } + + hasLocalImages = true + + let newattrs: any + let isLocal: boolean + let newImageItem: ImageItem + try { + // 实际上传逻辑 + await this.uploadSingleImageToBed(pageId, attrs, imageItem) + // 上传完成,需要获取最新链接 + newattrs = await this.siyuanApi.getBlockAttrs(pageId) + isLocal = false + const newfileMap = JsonUtil.safeParse(newattrs[SIYUAN_PICGO_FILE_MAP_KEY], {}) + newImageItem = newfileMap[imageItem.hash] + } catch (e) { + newattrs = attrs + isLocal = true + newImageItem = imageItem + this.logger.warn("单个图片上传异常", { pageId, attrs, imageItem }) + this.logger.warn("单个图片上传失败,错误信息如下", e) + } + + // 无论成功失败都要保存元数据,失败了当做本地图片 + replaceMap[imageItem.hash] = new ImageItem( + newImageItem.originUrl, + newImageItem.url, + isLocal, + newImageItem.alt, + newImageItem.title + ) + } + + if (!hasLocalImages) { + // ElMessage.info("未发现本地图片,不上传!若之前上传过,将做链接替换") + this.logger.warn("未发现本地图片,不上传!若之前上传过,将做链接替换") + } + + // 处理链接替换 + this.logger.debug("准备替换正文图片,replaceMap=>", JSON.stringify(replaceMap)) + this.logger.debug("开始替换正文,原文=>", JSON.stringify({ mdContent })) + ret.mdContent = this.imageParser.replaceImagesWithImageItemArray(mdContent, replaceMap) + this.logger.debug("图片链接替换完成,新正文=>", JSON.stringify({ newmdContent: ret.mdContent })) + + ret.flag = true + this.logger.debug("正文替换完成,最终结果=>", ret) + } catch (e: any) { + ret.flag = false + ret.errmsg = e.toString() + this.logger.error("文章图片上传失败=>", e) + } + return ret + } + + /** + * 上传单张图片到图床 + * + * @param pageId 文章ID + * @param attrs 文章属性 + * @param imageItem 图片信息 + * @param forceUpload 强制上传 + */ + public async uploadSingleImageToBed( + pageId: string, + attrs: any, + imageItem: ImageItem, + forceUpload?: boolean + ): Promise { + const mapInfoStr = attrs[SIYUAN_PICGO_FILE_MAP_KEY] ?? "{}" + const fileMap = JsonUtil.safeParse(mapInfoStr, {}) + this.logger.warn("fileMap=>", fileMap) + + // 处理上传 + const filePaths = [] + if (!forceUpload && !imageItem.isLocal) { + this.logger.warn("非本地图片,忽略=>", imageItem.url) + return + } + + let imageFullPath: string + if (this.isSiyuanOrSiyuanNewWin) { + const win = SiyuanDevice.siyuanWindow() + const dataDir: string = win.siyuan.config.system.dataDir + imageFullPath = `${dataDir}/assets/${imageItem.name}` + this.logger.info(`Will upload picture from ${imageFullPath}, imageItem =>`, imageItem) + + const fs = win.require("fs") + if (!fs.existsSync(imageFullPath)) { + imageFullPath = imageItem.url + } + } else { + imageFullPath = imageItem.url + } + this.logger.warn("isSiyuanOrSiyuanNewWin=>" + this.isSiyuanOrSiyuanNewWin + ", imageFullPath=>", imageFullPath) + filePaths.push(imageFullPath) + + // 批量上传 + const imageJson: any = await this.picgoApi.upload(filePaths) + this.logger.warn("图片上传完成,imageJson=>", imageJson) + const imageJsonObj = JsonUtil.safeParse(imageJson, []) as any + // 处理后续 + if (imageJsonObj && imageJsonObj.length > 0) { + const img = imageJsonObj[0] + if (!img?.imgUrl || StrUtil.isEmptyString(img.imgUrl)) { + throw new Error( + "图片上传失败,可能原因:PicGO配置错误或者该平台不支持图片覆盖,请检查配置或者尝试上传新图片。请打开picgo.log查看更多信息" + ) + } + const newImageItem = new ImageItem(imageItem.originUrl, img.imgUrl, false, imageItem.alt, imageItem.title) + fileMap[newImageItem.hash] = newImageItem + } else { + throw new Error("图片上传失败,可能原因:PicGO配置错误,请检查配置。请打开picgo.log查看更多信息") + } + + this.logger.warn("newFileMap=>", fileMap) + + const newFileMapStr = JSON.stringify(fileMap) + await this.siyuanApi.setBlockAttrs(pageId, { + [SIYUAN_PICGO_FILE_MAP_KEY]: newFileMapStr, + }) + } + // =================================================================================================================== private updateConfig() { diff --git a/libs/zhi-siyuan-picgo/src/lib/utils/md5Util.ts b/libs/zhi-siyuan-picgo/src/lib/utils/md5Util.ts new file mode 100644 index 0000000..9467b21 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/utils/md5Util.ts @@ -0,0 +1,26 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { md5 } from "js-md5" + +/** + * 获取文件名的hash + * + * @param filename 文件名 + */ +export const getFileHash = (filename: string): string => { + // import { createHash } from "crypto" + // const hash = createHash("sha256") + // hash.update(filename) + // return hash.digest("hex") + + // Base64.toBase64(filename).substring(0, 8); + + return md5(filename) +} diff --git a/libs/zhi-siyuan-picgo/tsconfig.json b/libs/zhi-siyuan-picgo/tsconfig.json index 08cfe0c..51eed8f 100644 --- a/libs/zhi-siyuan-picgo/tsconfig.json +++ b/libs/zhi-siyuan-picgo/tsconfig.json @@ -31,7 +31,8 @@ "src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", - "src/**/*.vue" + "src/**/*.vue", + "custom.d.ts" ], "references": [ { diff --git a/packages/picgo-plugin-app/custom.d.ts b/packages/picgo-plugin-app/custom.d.ts deleted file mode 100644 index aba7512..0000000 --- a/packages/picgo-plugin-app/custom.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * GNU GENERAL PUBLIC LICENSE - * Version 3, 29 June 2007 - * - * Copyright (C) 2023-2024 Terwer, Inc. - * Everyone is permitted to copy and distribute verbatim copies - * of this license document, but changing it is not allowed. - */ diff --git a/packages/picgo-plugin-app/package.json b/packages/picgo-plugin-app/package.json index 7bed4c9..a5ce775 100644 --- a/packages/picgo-plugin-app/package.json +++ b/packages/picgo-plugin-app/package.json @@ -6,7 +6,8 @@ "scripts": { "serve": "python -u scripts/serve.py && vite", "dev": "python -u scripts/dev.py", - "build": "python -u scripts/build.py" + "build": "python -u scripts/build.py", + "lint": "vue-tsc --noEmit" }, "devDependencies": { "@terwer/eslint-config-custom": "^1.3.6", @@ -33,7 +34,9 @@ "vue-i18n": "^9.10.2", "vue-router": "^4.3.0", "zhi-common": "^1.31.0", + "zhi-device": "^2.11.0", "zhi-lib-base": "^0.8.0", + "zhi-siyuan-api": "^2.18.6", "zhi-siyuan-picgo": "workspace:*" } } diff --git a/packages/picgo-plugin-app/src/components/home/DragUpload.vue b/packages/picgo-plugin-app/src/components/home/DragUpload.vue new file mode 100644 index 0000000..4602685 --- /dev/null +++ b/packages/picgo-plugin-app/src/components/home/DragUpload.vue @@ -0,0 +1,16 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/home/ElectronIndex.vue b/packages/picgo-plugin-app/src/components/home/ElectronIndex.vue index 5121bfd..a5efd9b 100644 --- a/packages/picgo-plugin-app/src/components/home/ElectronIndex.vue +++ b/packages/picgo-plugin-app/src/components/home/ElectronIndex.vue @@ -8,13 +8,83 @@ --> - + diff --git a/packages/picgo-plugin-app/src/components/home/PictureList.vue b/packages/picgo-plugin-app/src/components/home/PictureList.vue new file mode 100644 index 0000000..48ccc68 --- /dev/null +++ b/packages/picgo-plugin-app/src/components/home/PictureList.vue @@ -0,0 +1,139 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/home/UploadButton.vue b/packages/picgo-plugin-app/src/components/home/UploadButton.vue new file mode 100644 index 0000000..dbed97d --- /dev/null +++ b/packages/picgo-plugin-app/src/components/home/UploadButton.vue @@ -0,0 +1,144 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/composables/usePicgoCommon.ts b/packages/picgo-plugin-app/src/composables/usePicgoCommon.ts new file mode 100644 index 0000000..cd1da2d --- /dev/null +++ b/packages/picgo-plugin-app/src/composables/usePicgoCommon.ts @@ -0,0 +1,48 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { useSiyuanDevice } from "$composables/useSiyuanDevice.ts" +import { reactive } from "vue" +import { isDev } from "@/utils/Constants.ts" +import { ImageItem } from "zhi-siyuan-picgo/src/lib/models/ImageItem.ts" + +/** + * Picgo公共组件 + * + * @author terwer + * @since 0.6.1 + */ +export const usePicgoCommon = () => { + // private data + const { isInSiyuanOrSiyuanNewWin } = useSiyuanDevice() + + // public data + const picgoCommonData = reactive({ + isUploadLoading: false, + popWidth: 400, + showDebugMsg: isDev, + loggerMsg: "", + isSiyuanOrSiyuanNewWin: isInSiyuanOrSiyuanNewWin(), + fileList: { + files: [], + }, + }) + + // public methods + const picgoCommonMethods = { + getPicgoCommonData: () => { + return picgoCommonData + }, + } + + return { + picgoCommonData, + picgoCommonMethods, + } +} diff --git a/packages/picgo-plugin-app/src/composables/usePicgoInitPage.ts b/packages/picgo-plugin-app/src/composables/usePicgoInitPage.ts new file mode 100644 index 0000000..0d80c4c --- /dev/null +++ b/packages/picgo-plugin-app/src/composables/usePicgoInitPage.ts @@ -0,0 +1,97 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { createAppLogger } from "@/utils/appLogger.ts" +import { ImageParser } from "zhi-siyuan-picgo/src/lib/parser/ImageParser.ts" +import { isDev } from "@/utils/Constants.ts" +import { useSiyuanApi } from "$composables/useSiyuanApi.ts" +import { ParsedImage } from "zhi-siyuan-picgo/src/lib/models/ParsedImage.ts" +import { onMounted, watch } from "vue" +import { SiyuanPicGo } from "@/utils/siyuanPicgo.ts" + +/** + * Picgo页面初始化组件 + */ +export const usePicgoInitPage = (props: any, deps: any) => { + const logger = createAppLogger("picgo-common") + const { kernelApi } = useSiyuanApi() + + // private data + const siyuanApi = kernelApi + const imageParser = new ImageParser(isDev) + + // deps + const picgoCommonMethods = deps.picgoCommonMethods + + // deps data + const picgoCommonData = picgoCommonMethods.getPicgoCommonData() + + // private methods + const initPage = async () => { + const pageId = props.pageId + console.log("pageId=>", pageId) + + // 图片信息 + const imageBlocks: any[] = await siyuanApi.getImageBlocksByID(pageId) + logger.debug("查询文章中的图片块=>", imageBlocks) + + if (!imageBlocks || imageBlocks.length === 0) { + return + } + + // 解析图片地址 + let retImgs: ParsedImage[] = [] + imageBlocks.forEach((page) => { + const parsedImages: ParsedImage[] = imageParser.parseImagesToArray(page.markdown) + + // 会有很多重复值 + // retImgs = retImgs.concat(retImgs, parsedImages) + // 下面的写法可以去重 + retImgs = [...new Set([...retImgs, ...parsedImages])] + }) + logger.debug("解析出来的所有的图片地址=>", retImgs) + + // 将字符串数组格式的图片信息转换成图片对象数组 + const attrs = await siyuanApi.getBlockAttrs(pageId) + const picgoPostApi = await SiyuanPicGo.getInstance() + const imageItemArray = await picgoPostApi.doConvertImagesToImagesItemArray(attrs, retImgs) + + // 页面属性 + for (let i = 0; i < imageItemArray.length; i++) { + const imageItem = imageItemArray[i] + picgoCommonData.fileList.files.push(imageItem) + } + } + + // publish methods + const picgoInitMethods = { + initPage: async () => { + await initPage() + }, + } + + /** + * 监听props + */ + watch( + () => props.pageId, + async () => { + await initPage() + logger.debug("Picgo初始化") + } + ) + + onMounted(async () => { + await initPage() + }) + + return { + picgoInitMethods, + } +} diff --git a/packages/picgo-plugin-app/src/composables/usePicgoManage.ts b/packages/picgo-plugin-app/src/composables/usePicgoManage.ts new file mode 100644 index 0000000..b1a827f --- /dev/null +++ b/packages/picgo-plugin-app/src/composables/usePicgoManage.ts @@ -0,0 +1,142 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { useVueI18n } from "$composables/useVueI18n.ts" +import { useSiyuanApi } from "$composables/useSiyuanApi.ts" +import { createAppLogger } from "@/utils/appLogger.ts" +import { reactive } from "vue" +import { ElMessage, ElMessageBox } from "element-plus" +import { BrowserUtil } from "zhi-device" +import { ImageItem } from "zhi-siyuan-picgo/src/lib/models/ImageItem.ts" +import { SiyuanPicGo } from "@/utils/siyuanPicgo.ts" +import { copyToClipboardInBrowser } from "@/utils/utils.ts" + +/** + * Picgo图片管理组件 + */ +export const usePicgoManage = (props: any, deps: any) => { + const logger = createAppLogger("picgo-manage") + + // private data + const { t } = useVueI18n() + const { kernelApi } = useSiyuanApi() + + const siyuanApi = kernelApi + + // public data + const picgoManageData = reactive({ + dialogImageUrl: "", + dialogPreviewVisible: false, + }) + + // deps + const picgoCommonMethods = deps.picgoCommonMethods + + // deps data + const picgoCommonData = picgoCommonMethods.getPicgoCommonData() + + // public methods + const picgoManageMethods = { + handleUploadCurrentImageToBed: async (imageItem: ImageItem) => { + picgoCommonData.isUploadLoading = true + + if (!picgoCommonData.isSiyuanOrSiyuanNewWin) { + const errMsg = "由于浏览器的安全限制,无法获取本地文件的完整路径,因此非electron环境只能通过剪贴板上传" + ElMessage.error(errMsg) + picgoCommonData.loggerMsg = t("main.opt.failure") + "=>" + errMsg + picgoCommonData.isUploadLoading = false + return + } + + if (!imageItem.isLocal) { + ElMessageBox.confirm("已经是远程图片,是否仍然覆盖上传?", t("main.opt.warning"), { + confirmButtonText: t("main.opt.ok"), + cancelButtonText: t("main.opt.cancel"), + type: "warning", + }) + .then(async () => { + try { + await picgoManageMethods.doUploadImagesToBed(imageItem, true) + picgoCommonData.isUploadLoading = false + + ElMessage.success("图片已经成功上传至图床,即将刷新页面") + BrowserUtil.reloadPage() + } catch (e) { + picgoCommonData.isUploadLoading = false + + ElMessage({ + type: "error", + message: t("main.opt.failure") + "=>" + e, + }) + logger.error(t("main.opt.failure") + "=>" + e) + } + }) + .catch((e) => { + picgoCommonData.isUploadLoading = false + + if (e.toString().indexOf("cancel") <= -1) { + ElMessage({ + type: "error", + message: t("main.opt.failure") + ",图片上传异常=>" + e, + }) + logger.error(t("main.opt.failure") + "=>" + e) + } + }) + } else { + try { + await picgoManageMethods.doUploadImagesToBed(imageItem) + picgoCommonData.isUploadLoading = false + + ElMessage.success("图片已经成功上传至图床,即将刷新页面") + BrowserUtil.reloadPage() + } catch (e) { + picgoCommonData.isUploadLoading = false + + ElMessage({ + type: "error", + message: t("main.opt.failure") + "=>" + e, + }) + logger.error(t("main.opt.failure") + "=>" + e) + } + + picgoCommonData.isUploadLoading = false + } + }, + + /** + * 单个传,否则无法将图片对应 + * @param imageItem + * @param forceUpload 强制上传 + */ + doUploadImagesToBed: async (imageItem: ImageItem, forceUpload?: boolean) => { + const pageId = props.pageId + const attrs = await siyuanApi.getBlockAttrs(pageId) + + const picgoPostApi = await SiyuanPicGo.getInstance() + await picgoPostApi.uploadSingleImageToBed(pageId, attrs, imageItem, forceUpload) + }, + + onImageUrlCopy: (url: string) => { + if (BrowserUtil.isInBrowser) { + const mdUrl = `![](${url})` + copyToClipboardInBrowser(mdUrl) + } + }, + + handlePictureCardPreview: (url: string) => { + picgoManageData.dialogImageUrl = url ?? "" + picgoManageData.dialogPreviewVisible = true + }, + } + + return { + picgoManageData, + picgoManageMethods, + } +} diff --git a/packages/picgo-plugin-app/src/composables/usePicgoUpload.ts b/packages/picgo-plugin-app/src/composables/usePicgoUpload.ts new file mode 100644 index 0000000..0eb7721 --- /dev/null +++ b/packages/picgo-plugin-app/src/composables/usePicgoUpload.ts @@ -0,0 +1,226 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { useRouter } from "vue-router" +import { createAppLogger } from "@/utils/appLogger.ts" +import { useVueI18n } from "$composables/useVueI18n.ts" +import { reactive } from "vue" +import { ImageItem } from "zhi-siyuan-picgo/src/lib/models/ImageItem.ts" +import { ElMessage } from "element-plus" +import { SiyuanPicGo } from "@/utils/siyuanPicgo.ts" +import { BrowserUtil } from "zhi-device" +import { useSiyuanApi } from "$composables/useSiyuanApi.ts" +import { StrUtil } from "zhi-common" + +/** + * Picgo上传组件 + */ +export const usePicgoUpload = (props: any, deps: any, refs: any) => { + // private data + const logger = createAppLogger("picgo-upload") + const { t } = useVueI18n() + const router = useRouter() + const { kernelApi } = useSiyuanApi() + + const siyuanApi = kernelApi + + // public data + const picgoUploadData = reactive({}) + + // deps + const picgoCommonMethods = deps.picgoCommonMethods + + // deps data + const picgoCommonData = picgoCommonMethods.getPicgoCommonData() + + // refs + const refSelectedFiles = refs.refSelectedFiles + + // private methods + /** + * 处理图片后续 + * + * @param imgInfos + */ + const doAfterUpload = (imgInfos: any) => { + let imageJson + if (typeof imgInfos == "string") { + logger.warn("doAfterUpload返回的是字符串,需要解析") + imageJson = JSON.parse(imgInfos) + } else { + imageJson = imgInfos + } + + picgoCommonData.loggerMsg = JSON.stringify(imgInfos) + logger.debug("doAfterUpload,imgInfos=>", imgInfos) + + if (imageJson && imageJson.length > 0) { + imageJson.forEach((img: any) => { + const rtnItem = new ImageItem(img.imgUrl, img.imgUrl, false) + picgoCommonData.loggerMsg += "\nnewItem=>" + JSON.stringify(rtnItem) + + picgoCommonData.fileList.files.push(rtnItem) + }) + } + ElMessage.success(t("main.opt.success")) + } + + // public methods + const picgoUploadMethods = { + bindFileControl: () => { + refSelectedFiles.value.click() + }, + doUploadPicSelected: async (event: any) => { + picgoCommonData.isUploadLoading = true + + try { + const fileList = event.target.files + + logger.debug("onRequest fileList=>", fileList) + if (!fileList || fileList.length === 0) { + ElMessage.error("请选择图片") + picgoCommonData.loggerMsg = t("main.opt.failure") + "=>" + "请选择图片" + picgoCommonData.isUploadLoading = false + return + } + + if (!picgoCommonData.isSiyuanOrSiyuanNewWin) { + const errMsg = "由于浏览器的安全限制,无法获取本地文件的完整路径,因此非electron环境只能通过剪贴板上传" + ElMessage.error(errMsg) + picgoCommonData.loggerMsg = t("main.opt.failure") + "=>" + errMsg + picgoCommonData.isUploadLoading = false + return + } + + // 获取选择的文件的路径数组 + const filePaths = [] + for (let i = 0; i < fileList.length; i++) { + if (fileList.item(i).path) { + filePaths.push(fileList.item(i).path) + logger.debug("路径不为空") + } else { + logger.debug("路径为空,忽略") + } + } + + const picgoPostApi = await SiyuanPicGo.getInstance() + const imgInfos = await picgoPostApi.upload(filePaths) + // 处理后续 + doAfterUpload(imgInfos) + + picgoCommonData.isUploadLoading = false + } catch (e: any) { + if (e.toString().indexOf("cancel") <= -1) { + ElMessage({ + type: "error", + message: t("main.opt.failure") + "=>" + e, + }) + logger.error(t("main.opt.failure") + "=>" + e) + } + picgoCommonData.loggerMsg = t("main.opt.failure") + "=>" + e + picgoCommonData.isUploadLoading = false + } + }, + doUploadPicFromClipboard: async () => { + picgoCommonData.isUploadLoading = true + + try { + const picgoPostApi = await SiyuanPicGo.getInstance() + const imgInfos = await picgoPostApi.upload() + // 处理后续 + doAfterUpload(imgInfos) + + picgoCommonData.isUploadLoading = false + } catch (e: any) { + if (e.toString().indexOf("cancel") <= -1) { + ElMessage({ + type: "error", + message: t("main.opt.failure") + "=>" + e, + }) + logger.error(t("main.opt.failure") + "=>", e) + } + picgoCommonData.loggerMsg = t("main.opt.failure") + "=>" + e + picgoCommonData.isUploadLoading = false + } + }, + /** + * 单个传,否则无法将图片对应 + * @param imageItem + * @param forceUpload 强制上传 + */ + doUploadImagesToBed: async (imageItem: ImageItem, forceUpload?: boolean) => { + const pageId = props.pageId + const attrs = await siyuanApi.getBlockAttrs(pageId) + + const picgoPostApi = await SiyuanPicGo.getInstance() + await picgoPostApi.uploadSingleImageToBed(pageId, attrs, imageItem, forceUpload) + }, + doUploaddAllImagesToBed: async () => { + picgoCommonData.isUploadLoading = true + + if (!picgoCommonData.isSiyuanOrSiyuanNewWin) { + const errMsg = "由于浏览器的安全限制,无法获取本地文件的完整路径,因此非electron环境只能通过剪贴板上传" + ElMessage.error(errMsg) + picgoCommonData.loggerMsg = t("main.opt.failure") + "=>" + errMsg + picgoCommonData.isUploadLoading = false + return + } + + try { + let hasLocalImages = false + const imageItemArray = picgoCommonData.fileList.files + + for (let i = 0; i < imageItemArray.length; i++) { + const imageItem = imageItemArray[i] + if (!imageItem.isLocal) { + logger.debug("已经上传过图床,请勿重复上传=>", imageItem.originUrl) + continue + } + + hasLocalImages = true + await picgoUploadMethods.doUploadImagesToBed(imageItem) + } + + picgoCommonData.isUploadLoading = false + if (!hasLocalImages) { + ElMessage.warning("未发现本地图片,不上传") + } else { + ElMessage.success("图片已经全部上传至图床,即将刷新页面") + BrowserUtil.reloadPage() + } + } catch (e) { + picgoCommonData.isUploadLoading = false + + ElMessage({ + type: "error", + message: t("main.opt.failure") + "=>" + e, + }) + logger.error(t("main.opt.failure") + "=>" + e) + } + }, + doDownloadAllImagesToLocal: async () => { + if (StrUtil.isEmptyString(props.pageId)) { + ElMessage.error("pageId不能为空") + return + } + + picgoCommonData.isUploadLoading = true + + try { + } finally { + // picgoCommonData.isUploadLoading = false + } + }, + } + + return { + picgoUploadData, + picgoUploadMethods, + } +} diff --git a/packages/picgo-plugin-app/src/composables/useSiyuanApi.ts b/packages/picgo-plugin-app/src/composables/useSiyuanApi.ts new file mode 100644 index 0000000..1291fb9 --- /dev/null +++ b/packages/picgo-plugin-app/src/composables/useSiyuanApi.ts @@ -0,0 +1,63 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { createAppLogger } from "@/utils/appLogger.ts" +import { useSiyuanSetting } from "@/stores/useSiyuanSetting.ts" +import { SiyuanPicgoConfig } from "zhi-siyuan-picgo" +import { useSiyuanDevice } from "$composables/useSiyuanDevice.ts" +import { SiYuanApiAdaptor, SiyuanKernelApi } from "zhi-siyuan-api" + +/** + * 通用 Siyuan API 封装 + */ +export const useSiyuanApi = () => { + const logger = createAppLogger("use-siyuan-api") + const { getSiyuanSetting } = useSiyuanSetting() + + const siyuanSetting = getSiyuanSetting() + const siyuanApiUrl = siyuanSetting.value.apiUrl + const siyuanAuthToken = siyuanSetting.value.password + const siyuanConfig = new SiyuanPicgoConfig(siyuanApiUrl, siyuanAuthToken) + siyuanConfig.cookie = siyuanSetting.value.cookie + + const blogApi = new SiYuanApiAdaptor(siyuanConfig) + const kernelApi = new SiyuanKernelApi(siyuanConfig) + const { isInChromeExtension } = useSiyuanDevice() + + const isStorageViaSiyuanApi = () => { + // docker - 在 .env.docker 配置 VITE_DEFAULT_TYPE=siyuan + // vercel - 在环境变量配置 VITE_DEFAULT_TYPE=siyuan + // node - 启动参数加 VITE_DEFAULT_TYPE=siyuan node VITE_SIYUAN_API_URL=http://127.0.0.1:6806 + // 插件SPA(PC客户端) - VITE_DEFAULT_TYPE: siyuan + // 插件SPA(Docker浏览器客户端) - VITE_DEFAULT_TYPE: siyuan + // 插件SPA(本地客户端浏览器) - VITE_DEFAULT_TYPE: siyuan + // const storeViaSiyuanApi = process.env.VITE_DEFAULT_TYPE === "siyuan" + const defaultType = process.env.VITE_DEFAULT_TYPE ?? "siyuan" + const storeViaSiyuanApi = defaultType === "siyuan" + logger.info("defaultType=>", defaultType) + logger.info("storeViaSiyuanApi=>", String(storeViaSiyuanApi)) + return storeViaSiyuanApi + } + + const isUseSiyuanProxy = () => { + if (isInChromeExtension()) { + return false + } + + return isStorageViaSiyuanApi() + } + + return { + blogApi, + kernelApi, + siyuanConfig, + isStorageViaSiyuanApi, + isUseSiyuanProxy, + } +} diff --git a/packages/picgo-plugin-app/src/models/SiyuanConfig.ts b/packages/picgo-plugin-app/src/models/SiyuanConfig.ts deleted file mode 100644 index 1835512..0000000 --- a/packages/picgo-plugin-app/src/models/SiyuanConfig.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * GNU GENERAL PUBLIC LICENSE - * Version 3, 29 June 2007 - * - * Copyright (C) 2024 Terwer, Inc. - * Everyone is permitted to copy and distribute verbatim copies - * of this license document, but changing it is not allowed. - */ - -/** - * 思源笔记配置 - * - * @author terwer - * @since 1.0.0 - */ -class SiyuanConfig { - /** - * 思源笔记伺服地址 - */ - public apiUrl = "" - - /** - * 思源笔记 API token - */ - public password = "" - - /** - * 请求 cookie - */ - public cookie = "" - - constructor(apiUrl?: string, password?: string) { - this.apiUrl = apiUrl ?? "http://127.0.0.1:6806" - this.password = password ?? "" - } -} - -export default SiyuanConfig diff --git a/packages/picgo-plugin-app/src/stores/useSiyuanSetting.ts b/packages/picgo-plugin-app/src/stores/useSiyuanSetting.ts index 32c0217..8280857 100644 --- a/packages/picgo-plugin-app/src/stores/useSiyuanSetting.ts +++ b/packages/picgo-plugin-app/src/stores/useSiyuanSetting.ts @@ -7,12 +7,12 @@ * of this license document, but changing it is not allowed. */ -import { RemovableRef, StorageSerializers, useLocalStorage } from "@vueuse/core" -import SiyuanConfig from "@/models/SiyuanConfig.ts" +import { RemovableRef, StorageSerializers } from "@vueuse/core" import { readonly } from "vue" import { SiyuanDevice } from "zhi-device" import useCommonLocalStorage from "@/stores/common/useCommonLocalStorage.ts" import { useSiyuanDevice } from "$composables/useSiyuanDevice.ts" +import { SiyuanPicgoConfig } from "zhi-siyuan-picgo" const useSiyuanSetting = () => { const filePath = "storage/syp/siyuan-cfg.json" @@ -25,7 +25,7 @@ const useSiyuanSetting = () => { * @author terwer * @since 0.6.0 */ - const getSiyuanSetting = (): RemovableRef => { + const getSiyuanSetting = (): RemovableRef => { const baseUrl = "http://127.0.0.1:6806" const token = "" // PC客户端多个工作空间情况下,自动读取思源地址 @@ -35,9 +35,9 @@ const useSiyuanSetting = () => { origin = win?.location.origin } - const initialValue = new SiyuanConfig(origin ?? baseUrl, token) - const siyuanConfig = useCommonLocalStorage(filePath, storageKey, initialValue, { - serializer: StorageSerializers.object, + const initialValue = new SiyuanPicgoConfig(origin ?? baseUrl, token) + const siyuanConfig = useCommonLocalStorage(filePath, storageKey, initialValue, { + serializer: StorageSerializers.object }) // 更新apiUrl diff --git a/packages/picgo-plugin-app/src/utils/siyuanPicgo.ts b/packages/picgo-plugin-app/src/utils/siyuanPicgo.ts index 4ed4fd2..03e1344 100644 --- a/packages/picgo-plugin-app/src/utils/siyuanPicgo.ts +++ b/packages/picgo-plugin-app/src/utils/siyuanPicgo.ts @@ -11,6 +11,8 @@ import { SiyuanPicgoPostApi } from "zhi-siyuan-picgo" import { isDev } from "@/utils/Constants.ts" import { ElMessage } from "element-plus" import { createAppLogger } from "@/utils/appLogger.ts" +import { useSiyuanSetting } from "@/stores/useSiyuanSetting.ts" +import { toRaw } from "vue" /** * 思源笔记 PicGp 实例 @@ -20,7 +22,9 @@ class SiyuanPicGo { public static async getInstance(): Promise { return new Promise((resolve, reject) => { - const picgo = new SiyuanPicgoPostApi(isDev) + const { getSiyuanSetting } = useSiyuanSetting() + const siyuanConfig = getSiyuanSetting() + const picgo = new SiyuanPicgoPostApi(siyuanConfig.value, isDev) let needUpdate = false const checkConfig = () => { if (picgo.cfgUpdating) { diff --git a/packages/picgo-plugin-app/src/utils/utils.ts b/packages/picgo-plugin-app/src/utils/utils.ts new file mode 100644 index 0000000..0d1f1c9 --- /dev/null +++ b/packages/picgo-plugin-app/src/utils/utils.ts @@ -0,0 +1,79 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { isReactive, isRef, toRaw, unref } from "vue" +import { ElMessage } from "element-plus" + +/** + * 复制网页内容到剪贴板 + * + * @param text - 待复制的文本 + */ +export const copyToClipboardInBrowser = (text: string) => { + if (navigator && navigator.clipboard) { + // Copy the selected text to the clipboard + navigator.clipboard.writeText(text).then( + function () { + // The text has been successfully copied to the clipboard + ElMessage.success("复制成功") + }, + function (e) { + // An error occurred while copying the text + ElMessage.error("复制失败=>" + e) + } + ) + } else { + try { + const input = document.createElement("input") + input.style.position = "fixed" + input.style.opacity = "0" + input.value = text + document.body.appendChild(input) + input.select() + document.execCommand("copy") + document.body.removeChild(input) + ElMessage.success("复制成功") + } catch (e) { + ElMessage.error("复制失败=>" + e) + } + } +} + +/** + * get raw data from reactive or ref + */ +export const getRawData = (args: any): any => { + if (Array.isArray(args)) { + const data = args.map((item: any) => { + if (isRef(item)) { + return unref(item) + } + if (isReactive(item)) { + return toRaw(item) + } + return getRawData(item) + }) + return data + } + if (typeof args === "object") { + const data = {} as any + Object.keys(args).forEach((key) => { + const item = args[key] + if (isRef(item)) { + data[key] = unref(item) + } else if (isReactive(item)) { + data[key] = toRaw(item) + } else { + data[key] = getRawData(item) + } + }) + return data + } + return args +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb7a10e..f60b68a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,12 +82,24 @@ importers: libs/zhi-siyuan-picgo: dependencies: + js-md5: + specifier: ^0.8.3 + version: 0.8.3 universal-picgo: specifier: workspace:* version: link:../Universal-PicGo-Core + zhi-common: + specifier: ^1.31.0 + version: 1.31.0(typescript@5.4.2) + zhi-device: + specifier: ^2.11.0 + version: 2.11.0 zhi-lib-base: specifier: ^0.8.0 version: 0.8.0 + zhi-siyuan-api: + specifier: ^2.18.6 + version: 2.18.6(typescript@5.4.2) devDependencies: '@terwer/eslint-config-custom': specifier: ^1.3.6 @@ -128,9 +140,15 @@ importers: zhi-common: specifier: ^1.31.0 version: 1.31.0(typescript@5.4.2) + zhi-device: + specifier: ^2.11.0 + version: 2.11.0 zhi-lib-base: specifier: ^0.8.0 version: 0.8.0 + zhi-siyuan-api: + specifier: ^2.18.6 + version: 2.18.6(typescript@5.4.2) zhi-siyuan-picgo: specifier: workspace:* version: link:../../libs/zhi-siyuan-picgo @@ -4767,6 +4785,10 @@ packages: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} dev: true + /js-md5@0.8.3: + resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==} + dev: false + /js-string-escape@1.0.1: resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} engines: {node: '>= 0.8'} @@ -7758,6 +7780,15 @@ packages: commander: 9.5.0 dev: true + /zhi-blog-api@1.56.2(typescript@5.4.2): + resolution: {integrity: sha512-Nkly57hLPXvsmW6NWC0ZY70b1HTEOmblTu1eQVZAUw0lo3VcT+efhNp7e2GWaWsI2fw47LAYxDunKo116ndcYA==} + dependencies: + zhi-common: 1.31.0(typescript@5.4.2) + zhi-lib-base: 0.8.0 + transitivePeerDependencies: + - typescript + dev: false + /zhi-common@1.31.0(typescript@5.4.2): resolution: {integrity: sha512-s9x5e3dLhFmh514yvT1JtP7xriWMVxQ11AyIjOgx5Xo2cKcDv5b6nKDaLj2Jkvms9iwZRSinDY8y/FK3jrfbrg==} dependencies: @@ -7777,3 +7808,13 @@ packages: /zhi-lib-base@0.8.0: resolution: {integrity: sha512-3Ky5p6KvLWpXy2tlb/JIfSEKLSe5w43PBLM6g+hNcWwpL9S62emgcpUAHzGWBbC0vlLBLungt2LOsmwAfhrZ0w==} dev: false + + /zhi-siyuan-api@2.18.6(typescript@5.4.2): + resolution: {integrity: sha512-iNekK4hZwW/gpD8Jxau107oGyPcVMgjApCMtW2WiCYBWsdasM2ITgUOLXSP7Jf2YsSuNT2kA5CBcFpPrAZ05vQ==} + dependencies: + zhi-blog-api: 1.56.2(typescript@5.4.2) + zhi-common: 1.31.0(typescript@5.4.2) + zhi-lib-base: 0.8.0 + transitivePeerDependencies: + - typescript + dev: false