mirror of
https://github.com/woodchen-ink/obsidian-lskypro-uploader.git
synced 2025-07-18 05:42:07 +08:00
1.0.0
This commit is contained in:
commit
22d8922363
25
.github/pull_request_template.md
vendored
Normal file
25
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
## Obsidian Plugin Submission
|
||||
|
||||
### Plugin Info
|
||||
|
||||
- Plugin Name: LskyPro Upload
|
||||
- Plugin Author: woodchen
|
||||
- Plugin Description: Auto upload images from clipboard to LskyPro.
|
||||
- Repository URL: https://github.com/woodchen-ink/obsidian-lskypro-upload
|
||||
- Plugin License: MIT
|
||||
|
||||
### Requirements
|
||||
|
||||
- [x] My plugin is tested and works on the latest version of Obsidian
|
||||
- [x] My plugin follows the [Community Plugin Guidelines](https://docs.obsidian.md/Developer+docs/Plugin+guidelines)
|
||||
- [x] My plugin respects Obsidian's [Security Guidelines](https://docs.obsidian.md/Developer+docs/Security+best+practices)
|
||||
- [x] I have added a valid `fundingUrl` field in my `manifest.json` (optional)
|
||||
|
||||
### Additional Information
|
||||
|
||||
- This plugin helps users automatically upload images to LskyPro image hosting service
|
||||
- Features include:
|
||||
- Auto upload on paste
|
||||
- Support for public/private permissions
|
||||
- Album support
|
||||
- Custom domain support
|
84
.github/workflows/release.yml
vendored
Normal file
84
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,84 @@
|
||||
name: Release Obsidian plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: lskypro-upload
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # 获取完整历史记录
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18.x'
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "version=$(node -p "require('./manifest.json').version")" >> $GITHUB_OUTPUT
|
||||
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check Version Match
|
||||
run: |
|
||||
if [ "${{ steps.version.outputs.version }}" != "${{ steps.version.outputs.tag }}" ]; then
|
||||
echo "Version in manifest.json (${{ steps.version.outputs.version }}) does not match tag (${{ steps.version.outputs.tag }})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build plugin
|
||||
run: npm run build
|
||||
|
||||
- name: Create release package
|
||||
run: |
|
||||
mkdir ${{ env.PLUGIN_NAME }}
|
||||
cp main.js manifest.json ${{ env.PLUGIN_NAME }}
|
||||
if [ -f "styles.css" ]; then
|
||||
cp styles.css ${{ env.PLUGIN_NAME }}
|
||||
fi
|
||||
zip -r ${{ env.PLUGIN_NAME }}-${{ steps.version.outputs.version }}.zip ${{ env.PLUGIN_NAME }}
|
||||
|
||||
- name: Create source packages
|
||||
run: |
|
||||
# 创建临时目录
|
||||
mkdir temp_source
|
||||
# 复制所有文件到临时目录,排除不需要的文件
|
||||
rsync -av --exclude='node_modules' --exclude='${{ env.PLUGIN_NAME }}' --exclude='*.zip' . temp_source/
|
||||
|
||||
# 从临时目录创建压缩包
|
||||
cd temp_source
|
||||
zip -r ../source-code.zip .
|
||||
tar czf ../source-code.tar.gz .
|
||||
cd ..
|
||||
|
||||
# 清理临时目录
|
||||
rm -rf temp_source
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create "${{ steps.version.outputs.tag }}" \
|
||||
--title="${{ steps.version.outputs.tag }}" \
|
||||
--notes "Release ${{ steps.version.outputs.tag }}" \
|
||||
${{ env.PLUGIN_NAME }}-${{ steps.version.outputs.version }}.zip \
|
||||
main.js \
|
||||
manifest.json \
|
||||
$([ -f "styles.css" ] && echo "styles.css") \
|
||||
source-code.zip \
|
||||
source-code.tar.gz
|
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
package-lock.json
|
||||
# build
|
||||
*.js.map
|
||||
main.js
|
||||
|
||||
# ignore
|
||||
data.json
|
||||
|
||||
workspace.code-workspace
|
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"arrowParens": "avoid"
|
||||
}
|
51
.workflow/branch-pipeline.yml
Normal file
51
.workflow/branch-pipeline.yml
Normal file
@ -0,0 +1,51 @@
|
||||
version: '1.0'
|
||||
name: branch-pipeline
|
||||
displayName: BranchPipeline
|
||||
stages:
|
||||
- stage:
|
||||
name: compile
|
||||
displayName: 编译
|
||||
steps:
|
||||
- step: build@nodejs
|
||||
name: build_nodejs
|
||||
displayName: Nodejs 构建
|
||||
# 支持8.16.2、10.17.0、12.16.1、14.16.0、15.12.0五个版本
|
||||
nodeVersion: 14.16.0
|
||||
# 构建命令:安装依赖 -> 清除上次打包产物残留 -> 执行构建 【请根据项目实际产出进行填写】
|
||||
commands:
|
||||
- npm install && rm -rf ./dist && npm run build
|
||||
# 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除
|
||||
artifacts:
|
||||
# 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址
|
||||
- name: BUILD_ARTIFACT
|
||||
# 构建产物获取路径,是指代码编译完毕之后构建物的所在路径
|
||||
path:
|
||||
- ./dist
|
||||
- step: publish@general_artifacts
|
||||
name: publish_general_artifacts
|
||||
displayName: 上传制品
|
||||
# 上游构建任务定义的产物名,默认BUILD_ARTIFACT
|
||||
dependArtifact: BUILD_ARTIFACT
|
||||
# 上传到制品库时的制品命名,默认output
|
||||
artifactName: output
|
||||
dependsOn: build_nodejs
|
||||
- stage:
|
||||
name: release
|
||||
displayName: 发布
|
||||
steps:
|
||||
- step: publish@release_artifacts
|
||||
name: publish_release_artifacts
|
||||
displayName: '发布'
|
||||
# 上游上传制品任务的产出
|
||||
dependArtifact: output
|
||||
# 发布制品版本号
|
||||
version: '1.0.0.0'
|
||||
# 是否开启版本号自增,默认开启
|
||||
autoIncrement: true
|
||||
triggers:
|
||||
push:
|
||||
branches:
|
||||
exclude:
|
||||
- master
|
||||
include:
|
||||
- .*
|
49
.workflow/master-pipeline.yml
Normal file
49
.workflow/master-pipeline.yml
Normal file
@ -0,0 +1,49 @@
|
||||
version: '1.0'
|
||||
name: master-pipeline
|
||||
displayName: MasterPipeline
|
||||
stages:
|
||||
- stage:
|
||||
name: compile
|
||||
displayName: 编译
|
||||
steps:
|
||||
- step: build@nodejs
|
||||
name: build_nodejs
|
||||
displayName: Nodejs 构建
|
||||
# 支持8.16.2、10.17.0、12.16.1、14.16.0、15.12.0五个版本
|
||||
nodeVersion: 14.16.0
|
||||
# 构建命令:安装依赖 -> 清除上次打包产物残留 -> 执行构建 【请根据项目实际产出进行填写】
|
||||
commands:
|
||||
- npm install && rm -rf ./dist && npm run build
|
||||
# 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除
|
||||
artifacts:
|
||||
# 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址
|
||||
- name: BUILD_ARTIFACT
|
||||
# 构建产物获取路径,是指代码编译完毕之后构建物的所在路径
|
||||
path:
|
||||
- ./dist
|
||||
- step: publish@general_artifacts
|
||||
name: publish_general_artifacts
|
||||
displayName: 上传制品
|
||||
# 上游构建任务定义的产物名,默认BUILD_ARTIFACT
|
||||
dependArtifact: BUILD_ARTIFACT
|
||||
# 上传到制品库时的制品命名,默认output
|
||||
artifactName: output
|
||||
dependsOn: build_nodejs
|
||||
- stage:
|
||||
name: release
|
||||
displayName: 发布
|
||||
steps:
|
||||
- step: publish@release_artifacts
|
||||
name: publish_release_artifacts
|
||||
displayName: '发布'
|
||||
# 上游上传制品任务的产出
|
||||
dependArtifact: output
|
||||
# 发布制品版本号
|
||||
version: '1.0.0.0'
|
||||
# 是否开启版本号自增,默认开启
|
||||
autoIncrement: true
|
||||
triggers:
|
||||
push:
|
||||
branches:
|
||||
include:
|
||||
- master
|
36
.workflow/pr-pipeline.yml
Normal file
36
.workflow/pr-pipeline.yml
Normal file
@ -0,0 +1,36 @@
|
||||
version: '1.0'
|
||||
name: pr-pipeline
|
||||
displayName: PRPipeline
|
||||
stages:
|
||||
- stage:
|
||||
name: compile
|
||||
displayName: 编译
|
||||
steps:
|
||||
- step: build@nodejs
|
||||
name: build_nodejs
|
||||
displayName: Nodejs 构建
|
||||
# 支持8.16.2、10.17.0、12.16.1、14.16.0、15.12.0五个版本
|
||||
nodeVersion: 14.16.0
|
||||
# 构建命令:安装依赖 -> 清除上次打包产物残留 -> 执行构建 【请根据项目实际产出进行填写】
|
||||
commands:
|
||||
- npm install && rm -rf ./dist && npm run build
|
||||
# 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除
|
||||
artifacts:
|
||||
# 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址
|
||||
- name: BUILD_ARTIFACT
|
||||
# 构建产物获取路径,是指代码编译完毕之后构建物的所在路径
|
||||
path:
|
||||
- ./dist
|
||||
- step: publish@general_artifacts
|
||||
name: publish_general_artifacts
|
||||
displayName: 上传制品
|
||||
# 上游构建任务定义的产物名,默认BUILD_ARTIFACT
|
||||
dependArtifact: BUILD_ARTIFACT
|
||||
# 上传到制品库时的制品命名,默认output
|
||||
artifactName: output
|
||||
dependsOn: build_nodejs
|
||||
triggers:
|
||||
pr:
|
||||
branches:
|
||||
include:
|
||||
- master
|
0
CHANGELOG.md
Normal file
0
CHANGELOG.md
Normal file
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 NekoTarou
|
||||
Copyright (c) 2025 WoodChen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
36
README.md
Normal file
36
README.md
Normal file
@ -0,0 +1,36 @@
|
||||
# LskyPro Upload Plugin
|
||||
|
||||
A plugin for uploading images to LskyPro image hosting service.
|
||||
|
||||
## Features
|
||||
|
||||
- Auto upload images from clipboard
|
||||
- Support uploading to specified album
|
||||
- Support public/private image permissions
|
||||
- Support custom domain
|
||||
- Support token authentication
|
||||
|
||||
## Installation
|
||||
|
||||
1. Open Obsidian Settings
|
||||
2. Go to Community Plugins and disable Safe Mode
|
||||
3. Click Browse and search for "LskyPro Upload"
|
||||
4. Install and enable the plugin
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Get your LskyPro token from your LskyPro instance
|
||||
2. Configure the plugin settings:
|
||||
- Set your LskyPro domain
|
||||
- Add your token
|
||||
- Configure other optional settings
|
||||
|
||||
## Usage
|
||||
|
||||
1. Copy an image
|
||||
2. Paste into your note
|
||||
3. The image will be automatically uploaded to LskyPro
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
45
esbuild.config.mjs
Normal file
45
esbuild.config.mjs
Normal file
@ -0,0 +1,45 @@
|
||||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
|
||||
esbuild.build({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
platform: 'node',
|
||||
inject: [
|
||||
"./node-shims.js",
|
||||
],
|
||||
}).catch(() => process.exit(1));
|
11
manifest.json
Normal file
11
manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "LskyPro-Uploader",
|
||||
"name": "LskyPro Uploader",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Auto upload images from clipboard to LskyPro.",
|
||||
"author": "woodchen",
|
||||
"authorUrl": "https://woodchen.ink",
|
||||
"isDesktopOnly": true,
|
||||
"repo": "woodchen-ink/lskypro-uploader"
|
||||
}
|
5
node-shims.js
Normal file
5
node-shims.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { Buffer } from 'buffer';
|
||||
import process from 'process';
|
||||
import { Stream } from 'stream';
|
||||
|
||||
export { Buffer, process, Stream };
|
41
package.json
Normal file
41
package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "lskypro-uploader",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Auto upload images from clipboard to LskyPro",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [
|
||||
"obsidian.md",
|
||||
"lsky",
|
||||
"upload",
|
||||
"auto"
|
||||
],
|
||||
"author": "woodchen",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^24.1.0",
|
||||
"@rollup/plugin-json": "^6.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||
"@rollup/plugin-typescript": "^11.1.0",
|
||||
"@types/electron": "^1.6.10",
|
||||
"@types/node": "^14.14.2",
|
||||
"buffer": "^6.0.3",
|
||||
"builtin-modules": "^4.0.0",
|
||||
"esbuild": "^0.24.1",
|
||||
"obsidian": "^1.2.8",
|
||||
"process": "^0.11.10",
|
||||
"rollup": "^3.21.6",
|
||||
"stream": "^0.0.3",
|
||||
"tslib": "^2.5.0",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"fix-path": "^4.0.0",
|
||||
"image-type": "^5.2.0"
|
||||
}
|
||||
}
|
58
readme-zh.md
Normal file
58
readme-zh.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Obsidian LskyPro Auto Upload Plugin
|
||||
|
||||
这是一个支持直接上传图片到图床[Lsky](https://github.com/lsky-org/lsky-pro)的工具,基于[renmu123/obsidian-image-auto-upload-plugin](https://github.com/renmu123/obsidian-image-auto-upload-plugin.git)改造。
|
||||
**更新插件后记得重启一下 Obsidian**
|
||||
|
||||
# 开始
|
||||
|
||||
1. 安装 LskyPro 图床,并进行配置,配置参考[官网](https://www.lsky.pro/)
|
||||
2. 开启 LskyPro 的接口
|
||||
3. 使用授权接口获取Token,并记录下来
|
||||
4. 打开插件配置项,设置LskyPro域名(例如:https://lsky.xxx.com)
|
||||
5. 设置LskyPro Token
|
||||
6. 存储策略ID是可选配置,根据 LskyPro 的策略和自己的要求来配置,如果只有一个策略,可以不设置
|
||||
|
||||
# 特性
|
||||
|
||||
## 剪切板上传
|
||||
|
||||
支持黏贴剪切板的图片的时候直接上传,目前支持复制系统内图像直接上传。
|
||||
支持通过设置 `frontmatter` 来控制单个文件的上传,默认值为 `true`,控制关闭请将该值设置为 `false`
|
||||
|
||||
支持 ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".svg", ".tiff"(因为是直接调用LskyPro接口,理论上图床支持的文件都可以)
|
||||
|
||||
```yaml
|
||||
---
|
||||
image-auto-upload: true
|
||||
---
|
||||
```
|
||||
|
||||
## 批量上传一个文件中的所有图像文件
|
||||
|
||||
输入 `ctrl+P` 呼出面板,输入 `upload all images`,点击回车,就会自动开始上传。
|
||||
|
||||
路径解析优先级,会依次按照优先级查找:
|
||||
|
||||
1. 绝对路径,指基于库的绝对路径
|
||||
2. 相对路径,以./或../开头
|
||||
3. 尽可能简短的形式
|
||||
|
||||
## 批量下载网络图片到本地
|
||||
|
||||
输入 `ctrl+P` 呼出面板,输入 `download all images`,点击回车,就会自动开始下载。只在 win 进行过测试
|
||||
|
||||
## 支持右键菜单上传图片
|
||||
|
||||
目前已支持标准 md 以及 wiki 格式。支持相对路径以及绝对路径,需要进行正确设置,不然会引发奇怪的问题
|
||||
|
||||
## 支持拖拽上传
|
||||
|
||||
允许多文件拖拽
|
||||
|
||||
|
||||
|
||||
# TODO
|
||||
|
||||
# 感谢
|
||||
|
||||
[renmu123/obsidian-image-auto-upload-plugin](https://github.com/renmu123/obsidian-image-auto-upload-plugin.git)
|
16
rollup.config.js
Normal file
16
rollup.config.js
Normal file
@ -0,0 +1,16 @@
|
||||
import typescript from "@rollup/plugin-typescript";
|
||||
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import json from "@rollup/plugin-json";
|
||||
|
||||
export default {
|
||||
input: "src/main.ts",
|
||||
output: {
|
||||
dir: ".",
|
||||
sourcemap: "inline",
|
||||
format: "cjs",
|
||||
exports: "default",
|
||||
},
|
||||
external: ["obsidian", "electron"],
|
||||
plugins: [typescript(), nodeResolve({ browser: false }), commonjs(), json()],
|
||||
};
|
118
src/helper.ts
Normal file
118
src/helper.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { MarkdownView, App } from "obsidian";
|
||||
import { parse } from "path";
|
||||
|
||||
interface Image {
|
||||
path: string;
|
||||
obspath: string;
|
||||
name: string;
|
||||
source: string;
|
||||
}
|
||||
//  local image should has ext
|
||||
//  internet image should not has ext
|
||||
//const REGEX_FILE = /\!\[(.*?)\]\((\S+\.\w+)\)|\!\[(.*?)\]\((https?:\/\/.*?)\)/g;
|
||||
const REGEX_FILE = /!\[(.*?)\]\((.*?)\)/g;
|
||||
const REGEX_WIKI_FILE = /\!\[\[(.*?)(\s\|.*?)?\]\]/g;
|
||||
export default class Helper {
|
||||
app: App;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
getFrontmatterValue(key: string, defaultValue: any = undefined) {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) {
|
||||
return undefined;
|
||||
}
|
||||
const path = file.path;
|
||||
const cache = this.app.metadataCache.getCache(path);
|
||||
|
||||
let value = defaultValue;
|
||||
if (cache?.frontmatter && cache.frontmatter.hasOwnProperty(key)) {
|
||||
value = cache.frontmatter[key];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
getEditor() {
|
||||
const mdView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (mdView) {
|
||||
return mdView.editor;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
getValue() {
|
||||
const editor = this.getEditor();
|
||||
return editor.getValue();
|
||||
}
|
||||
|
||||
setValue(value: string) {
|
||||
const editor = this.getEditor();
|
||||
const { left, top } = editor.getScrollInfo();
|
||||
const position = editor.getCursor();
|
||||
|
||||
editor.setValue(value);
|
||||
editor.scrollTo(left, top);
|
||||
editor.setCursor(position);
|
||||
}
|
||||
|
||||
// get all file urls, include local and internet
|
||||
getAllFiles(): Image[] {
|
||||
const editor = this.getEditor();
|
||||
let value = editor.getValue();
|
||||
return this.getImageLink(value);
|
||||
}
|
||||
getImageLink(value: string): Image[] {
|
||||
const matches = value.matchAll(REGEX_FILE);
|
||||
const WikiMatches = value.matchAll(REGEX_WIKI_FILE);
|
||||
|
||||
let fileArray: Image[] = [];
|
||||
|
||||
for (const match of matches) {
|
||||
const source = match[0];
|
||||
|
||||
let name = match[1];
|
||||
let path = match[2];
|
||||
if (!name&&match.length>3) {
|
||||
name = match[3];
|
||||
}
|
||||
if (!path&&match.length>4) {
|
||||
path = match[4];
|
||||
}
|
||||
if (!name) {
|
||||
name = path?.substring(path?.lastIndexOf('/')+1)
|
||||
}
|
||||
|
||||
fileArray.push({
|
||||
path: path,
|
||||
obspath: path,
|
||||
name: name,
|
||||
source: source,
|
||||
});
|
||||
}
|
||||
|
||||
for (const match of WikiMatches) {
|
||||
const name = parse(match[1]).name;
|
||||
const path = match[1];
|
||||
const source = match[0];
|
||||
fileArray.push({
|
||||
path: path,
|
||||
obspath: path,
|
||||
name: name,
|
||||
source: source,
|
||||
});
|
||||
}
|
||||
return fileArray;
|
||||
}
|
||||
|
||||
hasBlackDomain(src: string, blackDomains: string) {
|
||||
if (blackDomains.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
const blackDomainList = blackDomains.split(",").filter(item => item !== "");
|
||||
let url = new URL(src);
|
||||
const domain = url.hostname;
|
||||
|
||||
return blackDomainList.some(blackDomain => domain.includes(blackDomain));
|
||||
}
|
||||
}
|
13
src/lang/README.md
Normal file
13
src/lang/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
## Localization
|
||||
|
||||
The plugin has full localization support, and will attempt to load the current Obsidian locale. If one does not exist, it will fall back to English.
|
||||
|
||||
### Adding a new Locale
|
||||
|
||||
New locales can be added by creating a pull request. Two things should be done in this pull request:
|
||||
|
||||
1. Create the locale in the `locales` folder by copying the `en.ts` file. This file should be given a name matching the string returned by `moment.locale()`.
|
||||
2. Create the translation by editing the value of each property.
|
||||
3. Add the import in `locales.ts`.
|
||||
4. Add the language to the `localeMap` variable.
|
||||
|
57
src/lang/helpers.ts
Normal file
57
src/lang/helpers.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { moment } from 'obsidian';
|
||||
|
||||
import ar from './locale/ar';
|
||||
import cz from './locale/cz';
|
||||
import da from './locale/da';
|
||||
import de from './locale/de';
|
||||
import en from './locale/en';
|
||||
import enGB from './locale/en-gb';
|
||||
import es from './locale/es';
|
||||
import fr from './locale/fr';
|
||||
import hi from './locale/hi';
|
||||
import id from './locale/id';
|
||||
import it from './locale/it';
|
||||
import ja from './locale/ja';
|
||||
import ko from './locale/ko';
|
||||
import nl from './locale/nl';
|
||||
import no from './locale/no';
|
||||
import pl from './locale/pl';
|
||||
import pt from './locale/pt';
|
||||
import ptBR from './locale/pt-br';
|
||||
import ro from './locale/ro';
|
||||
import ru from './locale/ru';
|
||||
import tr from './locale/tr';
|
||||
import zhCN from './locale/zh-cn';
|
||||
import zhTW from './locale/zh-tw';
|
||||
|
||||
const localeMap: { [k: string]: Partial<typeof en> } = {
|
||||
ar,
|
||||
cs: cz,
|
||||
da,
|
||||
de,
|
||||
en,
|
||||
'en-gb': enGB,
|
||||
es,
|
||||
fr,
|
||||
hi,
|
||||
id,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
nl,
|
||||
nn: no,
|
||||
pl,
|
||||
pt,
|
||||
'pt-br': ptBR,
|
||||
ro,
|
||||
ru,
|
||||
tr,
|
||||
'zh-cn': zhCN,
|
||||
'zh-tw': zhTW,
|
||||
};
|
||||
|
||||
const locale = localeMap[moment.locale()];
|
||||
|
||||
export function t(str: keyof typeof en): string {
|
||||
return (locale && locale[str]) || en[str];
|
||||
}
|
3
src/lang/locale/ar.ts
Normal file
3
src/lang/locale/ar.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// العربية
|
||||
|
||||
export default {};
|
3
src/lang/locale/cz.ts
Normal file
3
src/lang/locale/cz.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// čeština
|
||||
|
||||
export default {};
|
3
src/lang/locale/da.ts
Normal file
3
src/lang/locale/da.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Dansk
|
||||
|
||||
export default {};
|
3
src/lang/locale/de.ts
Normal file
3
src/lang/locale/de.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Deutsch
|
||||
|
||||
export default {};
|
3
src/lang/locale/en-gb.ts
Normal file
3
src/lang/locale/en-gb.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// British English
|
||||
|
||||
export default {};
|
33
src/lang/locale/en.ts
Normal file
33
src/lang/locale/en.ts
Normal file
@ -0,0 +1,33 @@
|
||||
// English
|
||||
|
||||
export default {
|
||||
// setting.ts
|
||||
"Plugin Settings": "Plugin Settings",
|
||||
"Auto pasted upload": "Auto pasted upload",
|
||||
"If you set this value true, when you paste image, it will be auto uploaded":
|
||||
"If you set this value true, when you paste image, it will be auto uploaded",
|
||||
"Default uploader": "Default uploader",
|
||||
"PicList desc": "Search PicList on Github to download and install",
|
||||
"Delete image using PicList": "Delete image using PicList",
|
||||
"Delete successfully": "Delete successfully",
|
||||
"Delete failed": "Delete failed",
|
||||
"Image size suffix": "Image size suffix",
|
||||
"Image size suffix Description": "like |300 for resize image in ob.",
|
||||
"Please input image size suffix": "Please input image size suffix",
|
||||
"Error, could not delete": "Error, could not delete",
|
||||
"Work on network": "Work on network",
|
||||
"Work on network Description":
|
||||
"Allow upload network image by 'Upload all' command.\n Or when you paste, md standard image link in your clipboard will be auto upload.",
|
||||
fixPath: "fixPath",
|
||||
"Upload when clipboard has image and text together":
|
||||
"Upload when clipboard has image and text together",
|
||||
"When you copy, some application like Excel will image and text to clipboard, you can upload or not.":
|
||||
"When you copy, some application like Excel will image and text to clipboard, you can upload or not.",
|
||||
"Network Domain Black List": "Network Domain Black List",
|
||||
"Network Domain Black List Description":
|
||||
"Image in the domain list will not be upload,use comma separated",
|
||||
"Delete source file after you upload file":
|
||||
"Delete source file after you upload file",
|
||||
"Delete source file in ob assets after you upload file.":
|
||||
"Delete source file in ob assets after you upload file.",
|
||||
};
|
3
src/lang/locale/es.ts
Normal file
3
src/lang/locale/es.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Español
|
||||
|
||||
export default {};
|
3
src/lang/locale/fr.ts
Normal file
3
src/lang/locale/fr.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// français
|
||||
|
||||
export default {};
|
3
src/lang/locale/hi.ts
Normal file
3
src/lang/locale/hi.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// हिन्दी
|
||||
|
||||
export default {};
|
3
src/lang/locale/id.ts
Normal file
3
src/lang/locale/id.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Bahasa Indonesia
|
||||
|
||||
export default {};
|
3
src/lang/locale/it.ts
Normal file
3
src/lang/locale/it.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Italiano
|
||||
|
||||
export default {};
|
3
src/lang/locale/ja.ts
Normal file
3
src/lang/locale/ja.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// 日本語
|
||||
|
||||
export default {};
|
3
src/lang/locale/ko.ts
Normal file
3
src/lang/locale/ko.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// 한국어
|
||||
|
||||
export default {};
|
3
src/lang/locale/nl.ts
Normal file
3
src/lang/locale/nl.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Nederlands
|
||||
|
||||
export default {};
|
3
src/lang/locale/no.ts
Normal file
3
src/lang/locale/no.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Norsk
|
||||
|
||||
export default {};
|
3
src/lang/locale/pl.ts
Normal file
3
src/lang/locale/pl.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// język polski
|
||||
|
||||
export default {};
|
4
src/lang/locale/pt-br.ts
Normal file
4
src/lang/locale/pt-br.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// Português do Brasil
|
||||
// Brazilian Portuguese
|
||||
|
||||
export default {};
|
3
src/lang/locale/pt.ts
Normal file
3
src/lang/locale/pt.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Português
|
||||
|
||||
export default {};
|
3
src/lang/locale/ro.ts
Normal file
3
src/lang/locale/ro.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Română
|
||||
|
||||
export default {};
|
3
src/lang/locale/ru.ts
Normal file
3
src/lang/locale/ru.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// русский
|
||||
|
||||
export default {};
|
3
src/lang/locale/tr.ts
Normal file
3
src/lang/locale/tr.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Türkçe
|
||||
|
||||
export default {};
|
31
src/lang/locale/zh-cn.ts
Normal file
31
src/lang/locale/zh-cn.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// 简体中文
|
||||
|
||||
export default {
|
||||
// setting.ts
|
||||
"Plugin Settings": "插件设置",
|
||||
"Auto pasted upload": "剪切板自动上传",
|
||||
"If you set this value true, when you paste image, it will be auto uploaded":
|
||||
"启用该选项后,黏贴图片时会自动上传",
|
||||
"Default uploader": "默认上传器",
|
||||
"Delete image using PicList": "使用 PicList 删除图片",
|
||||
"Delete successfully": "删除成功",
|
||||
"Delete failed": "删除失败",
|
||||
"Error, could not delete": "错误,无法删除",
|
||||
"Image size suffix": "图片大小后缀",
|
||||
"Image size suffix Description": "比如:|300 用于调整图片大小",
|
||||
"Please input image size suffix": "请输入图片大小后缀",
|
||||
"Work on network": "应用网络图片",
|
||||
"Work on network Description":
|
||||
"当你上传所有图片时,也会上传网络图片。以及当你进行黏贴时,剪切板中的标准 md 图片会被上传",
|
||||
fixPath: "修正PATH变量",
|
||||
"Upload when clipboard has image and text together":
|
||||
"当剪切板同时拥有文本和图片剪切板数据时是否上传图片",
|
||||
"When you copy, some application like Excel will image and text to clipboard, you can upload or not.":
|
||||
"当你复制时,某些应用例如 Excel 会在剪切板同时文本和图像数据,确认是否上传。",
|
||||
"Network Domain Black List": "网络图片域名黑名单",
|
||||
"Network Domain Black List Description":
|
||||
"黑名单域名中的图片将不会被上传,用英文逗号分割",
|
||||
"Delete source file after you upload file": "上传文件后移除源文件",
|
||||
"Delete source file in ob assets after you upload file.":
|
||||
"上传文件后移除在ob附件文件夹中的文件",
|
||||
};
|
3
src/lang/locale/zh-tw.ts
Normal file
3
src/lang/locale/zh-tw.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// 繁體中文
|
||||
|
||||
export default {};
|
648
src/main.ts
Normal file
648
src/main.ts
Normal file
@ -0,0 +1,648 @@
|
||||
import {
|
||||
MarkdownView,
|
||||
Plugin,
|
||||
FileSystemAdapter,
|
||||
Editor,
|
||||
Menu,
|
||||
Notice,
|
||||
addIcon,
|
||||
requestUrl,
|
||||
MarkdownFileInfo,
|
||||
TFile,
|
||||
} from "obsidian";
|
||||
|
||||
import { join, parse, basename, dirname } from "path";
|
||||
|
||||
import imageType from "image-type";
|
||||
|
||||
import {
|
||||
isAssetTypeAnImage,
|
||||
getUrlAsset,
|
||||
arrayToObject,
|
||||
} from "./utils";
|
||||
import { LskyProUploader } from "./uploader";
|
||||
import Helper from "./helper";
|
||||
|
||||
import { SettingTab, PluginSettings, DEFAULT_SETTINGS } from "./setting";
|
||||
|
||||
interface Image {
|
||||
path: string;
|
||||
obspath: string;
|
||||
name: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export default class imageAutoUploadPlugin extends Plugin {
|
||||
settings: PluginSettings;
|
||||
helper: Helper;
|
||||
editor: Editor;
|
||||
lskyUploader: LskyProUploader;
|
||||
uploader: LskyProUploader;
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
this.initializeUploader();
|
||||
}
|
||||
|
||||
private initializeUploader() {
|
||||
this.lskyUploader = new LskyProUploader(this.settings, this.app);
|
||||
if (this.settings.uploader === "LskyPro") {
|
||||
this.uploader = this.lskyUploader;
|
||||
} else {
|
||||
new Notice("unknown uploader");
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
this.initializeUploader();
|
||||
}
|
||||
|
||||
onunload() { }
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
this.helper = new Helper(this.app);
|
||||
|
||||
addIcon(
|
||||
"upload",
|
||||
`<svg t="1636630783429" class="icon" viewBox="0 0 100 100" version="1.1" p-id="4649" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M 71.638 35.336 L 79.408 35.336 C 83.7 35.336 87.178 38.662 87.178 42.765 L 87.178 84.864 C 87.178 88.969 83.7 92.295 79.408 92.295 L 17.249 92.295 C 12.957 92.295 9.479 88.969 9.479 84.864 L 9.479 42.765 C 9.479 38.662 12.957 35.336 17.249 35.336 L 25.019 35.336 L 25.019 42.765 L 17.249 42.765 L 17.249 84.864 L 79.408 84.864 L 79.408 42.765 L 71.638 42.765 L 71.638 35.336 Z M 49.014 10.179 L 67.326 27.688 L 61.835 32.942 L 52.849 24.352 L 52.849 59.731 L 45.078 59.731 L 45.078 24.455 L 36.194 32.947 L 30.702 27.692 L 49.012 10.181 Z" p-id="4650" fill="#8a8a8a"></path>
|
||||
</svg>`
|
||||
);
|
||||
|
||||
this.addSettingTab(new SettingTab(this.app, this));
|
||||
|
||||
this.addCommand({
|
||||
id: "Upload all images",
|
||||
name: "Upload all images",
|
||||
checkCallback: (checking: boolean) => {
|
||||
let leaf = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (leaf) {
|
||||
if (!checking) {
|
||||
this.uploadAllFile();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "Download all images",
|
||||
name: "Download all images",
|
||||
checkCallback: (checking: boolean) => {
|
||||
let leaf = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (leaf) {
|
||||
if (!checking) {
|
||||
this.downloadAllImageFiles();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.setupPasteHandler();
|
||||
this.registerSelection();
|
||||
|
||||
// 添加文件菜单项
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("file-menu", (menu, file) => {
|
||||
// 检查是否为 TFile 类型且是 markdown 文件
|
||||
if (file instanceof TFile && file.extension === "md") {
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle("上传所有图片到图床")
|
||||
.setIcon("upload")
|
||||
.onClick(async () => {
|
||||
// 获取当前文件的内容
|
||||
const content = await this.app.vault.read(file);
|
||||
|
||||
// 临时设置 helper 的内容
|
||||
this.helper.setValue(content);
|
||||
|
||||
// 调用现有的上传功能
|
||||
this.uploadAllFile();
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
registerSelection() {
|
||||
this.registerEvent(
|
||||
this.app.workspace.on(
|
||||
"editor-menu",
|
||||
(menu: Menu, editor: Editor, info: MarkdownView | MarkdownFileInfo) => {
|
||||
if (this.app.workspace.getLeavesOfType("markdown").length === 0) {
|
||||
return;
|
||||
}
|
||||
// 保留一个基本的空方法,以备将来可能需要添加其他编辑器菜单功能
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async downloadAllImageFiles() {
|
||||
const fileArray = this.helper.getAllFiles();
|
||||
const folderPathAbs = this.getAttachmentFolderPath();
|
||||
if (folderPathAbs==null||!folderPathAbs) {
|
||||
new Notice(
|
||||
`Get attachment folder path faild.`
|
||||
);
|
||||
return ;
|
||||
}
|
||||
let absfolder = this.app.vault.getAbstractFileByPath(folderPathAbs);
|
||||
if (!absfolder) {
|
||||
this.app.vault.createFolder(folderPathAbs);
|
||||
}
|
||||
|
||||
let imageArray = [];
|
||||
let count:number = 0;
|
||||
for (const file of fileArray) {
|
||||
if (!file.path.startsWith("http")) {
|
||||
continue;
|
||||
}
|
||||
count++;
|
||||
const url = file.path;
|
||||
const asset = getUrlAsset(url);
|
||||
let [name, ext] = [
|
||||
decodeURI(parse(asset).name).replaceAll(/[\\\\/:*?\"<>|]/g, "-"),
|
||||
parse(asset).ext,
|
||||
];
|
||||
|
||||
// 如果文件名已存在,则用随机值替换
|
||||
if (this.app.vault.getAbstractFileByPath(folderPathAbs+"/"+asset)) {
|
||||
name = (Math.random() + 1).toString(36).substring(2, 7);
|
||||
}
|
||||
try {
|
||||
const response = await this.download(url, folderPathAbs, name, ext);
|
||||
if (response.ok) {
|
||||
imageArray.push({
|
||||
source: file.source,
|
||||
name: name,
|
||||
path: response.path,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
let value = this.helper.getValue();
|
||||
imageArray.map(image => {
|
||||
value = value.replace(
|
||||
image.source,
|
||||
`})`
|
||||
);
|
||||
});
|
||||
|
||||
this.helper.setValue(value);
|
||||
|
||||
new Notice(
|
||||
`all: ${count}\nsuccess: ${imageArray.length}\nfailed: ${count - imageArray.length
|
||||
}`
|
||||
);
|
||||
}
|
||||
//获取附件路径(相对路径)
|
||||
getAttachmentFolderPath() {
|
||||
// @ts-ignore
|
||||
let assetFolder: string = this.app.vault.config.attachmentFolderPath;
|
||||
if (!assetFolder) {
|
||||
assetFolder = "/"
|
||||
}
|
||||
const activeFile = this.app.vault.getAbstractFileByPath(
|
||||
this.app.workspace.getActiveFile()?.path
|
||||
);
|
||||
if (activeFile==null||!activeFile) {
|
||||
return null;
|
||||
}
|
||||
const parentPath = activeFile.parent.path;
|
||||
// 当前文件夹下的子文件夹
|
||||
if (assetFolder.startsWith("./")) {
|
||||
assetFolder = assetFolder.substring(1);
|
||||
let pathTem = parentPath + (assetFolder==="/"?"":assetFolder);
|
||||
while(pathTem.startsWith("/")) {
|
||||
pathTem = pathTem.substring(1);
|
||||
}
|
||||
return pathTem;
|
||||
} else {
|
||||
return assetFolder;
|
||||
}
|
||||
}
|
||||
|
||||
async download(url: string, folderPath: string, name: string, ext: string) {
|
||||
const response = await requestUrl({ url });
|
||||
const type = await imageType(new Uint8Array(response.arrayBuffer));
|
||||
|
||||
if (response.status !== 200) {
|
||||
return {
|
||||
ok: false,
|
||||
msg: "error",
|
||||
};
|
||||
}
|
||||
if (!type) {
|
||||
return {
|
||||
ok: false,
|
||||
msg: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(response.arrayBuffer);
|
||||
|
||||
try {
|
||||
let path = folderPath+'/'+`${name}${ext}`;
|
||||
|
||||
if (!ext) {
|
||||
path = folderPath +'/'+ `${name}.${type.ext}`;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
msg: "ok",
|
||||
path: path,
|
||||
type,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
msg: err,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
filterFile(fileArray: Image[]) {
|
||||
const imageList: Image[] = [];
|
||||
|
||||
for (const match of fileArray) {
|
||||
if (match.path.startsWith("http")) {
|
||||
if (this.settings.workOnNetWork) {
|
||||
if (
|
||||
!this.helper.hasBlackDomain(
|
||||
match.path,
|
||||
this.settings.newWorkBlackDomains
|
||||
)
|
||||
) {
|
||||
imageList.push({
|
||||
path: match.path,
|
||||
obspath: match.path,
|
||||
name: match.name,
|
||||
source: match.source,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imageList.push({
|
||||
path: match.path,
|
||||
obspath: match.obspath,
|
||||
name: match.name,
|
||||
source: match.source,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return imageList;
|
||||
}
|
||||
getFile(fileName: string, fileMap: any) {
|
||||
if (!fileMap) {
|
||||
fileMap = arrayToObject(this.app.vault.getFiles(), "name");
|
||||
}
|
||||
return fileMap[fileName];
|
||||
}
|
||||
// uploda all file
|
||||
async uploadAllFile() {
|
||||
let content = this.helper.getValue();
|
||||
|
||||
const adapter = this.app.vault.adapter;
|
||||
if (!(adapter instanceof FileSystemAdapter)) {
|
||||
new Notice('Current adapter is not FileSystemAdapter');
|
||||
return;
|
||||
}
|
||||
const basePath = adapter.getBasePath();
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
const fileMap = arrayToObject(this.app.vault.getFiles(), "name");
|
||||
const filePathMap = arrayToObject(this.app.vault.getFiles(), "path");
|
||||
let imageList: Image[] = [];
|
||||
const fileArray = this.filterFile(this.helper.getAllFiles());
|
||||
|
||||
// 收集所有需要上传的图片
|
||||
for (const match of fileArray) {
|
||||
const imageName = match.name;
|
||||
const encodedUri = match.path;
|
||||
|
||||
if (!encodedUri.startsWith("http")) {
|
||||
const matchPath = decodeURI(encodedUri);
|
||||
const fileName = basename(matchPath);
|
||||
let file;
|
||||
// 绝对路径
|
||||
if (filePathMap[matchPath]) {
|
||||
file = filePathMap[matchPath];
|
||||
}
|
||||
|
||||
// 相对路径
|
||||
if (
|
||||
(!file && matchPath.startsWith("./")) ||
|
||||
matchPath.startsWith("../")
|
||||
) {
|
||||
let absoPath = "";
|
||||
//查找相对路径
|
||||
if (matchPath.startsWith("./")) {
|
||||
absoPath = dirname(activeFile.path)+matchPath.substring(1)
|
||||
} else {
|
||||
//对于../../开头的路径,需要向上查找匹配
|
||||
let num = matchPath.split("../").length-1;
|
||||
absoPath = matchPath;
|
||||
for (let i=0;i<num;i++) {
|
||||
absoPath = absoPath.substring(0,absoPath.lastIndexOf("/"))
|
||||
}
|
||||
}
|
||||
file = this.app.vault.getAbstractFileByPath(absoPath);
|
||||
}
|
||||
// 尽可能短路径
|
||||
if (!file) {
|
||||
file = this.getFile(fileName, fileMap);
|
||||
}
|
||||
|
||||
if (file) {
|
||||
const abstractImageFile = join(basePath, file.path);
|
||||
|
||||
if (isAssetTypeAnImage(abstractImageFile)) {
|
||||
let pushObj = {
|
||||
path: abstractImageFile,
|
||||
obspath: file.path,
|
||||
name: imageName,
|
||||
source: match.source,
|
||||
};
|
||||
//如果文件中有重复引用的图片,只上传一次
|
||||
if (!imageList.find(item=>item.path===abstractImageFile&&item.name===imageName&&item.source===match.source)) {
|
||||
imageList.push(pushObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageList.length === 0) {
|
||||
new Notice("没有解析到图像文件");
|
||||
return;
|
||||
}
|
||||
|
||||
new Notice(`共找到${imageList.length}个图像文件,开始上传`);
|
||||
|
||||
// 一个一个上传图片
|
||||
for (const image of imageList) {
|
||||
try {
|
||||
const res = await this.uploader.uploadFilesByPath([image.obspath]);
|
||||
if (res.success) {
|
||||
const uploadUrl = res.result[0];
|
||||
// 替换内容
|
||||
content = content.replaceAll(
|
||||
image.source,
|
||||
``
|
||||
);
|
||||
this.helper.setValue(content);
|
||||
|
||||
// 如果设置了删除源文件
|
||||
if (this.settings.deleteSource) {
|
||||
const fileDel = this.app.vault.getAbstractFileByPath(image.obspath);
|
||||
if (fileDel) {
|
||||
await this.app.fileManager.trashFile(fileDel);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存上传记录
|
||||
if (res.fullResult) {
|
||||
this.settings.uploadedImages = [
|
||||
...(this.settings.uploadedImages || []),
|
||||
...res.fullResult,
|
||||
];
|
||||
await this.saveSettings();
|
||||
}
|
||||
|
||||
new Notice(`图片 ${image.name} 上传成功`);
|
||||
} else {
|
||||
new Notice(`图片 ${image.name} 上传失败`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Upload error for ${image.name}:`, error);
|
||||
new Notice(`图片 ${image.name} 上传出错`);
|
||||
}
|
||||
}
|
||||
|
||||
new Notice("所有图片处理完成");
|
||||
}
|
||||
|
||||
setupPasteHandler() {
|
||||
this.registerEvent(
|
||||
this.app.workspace.on(
|
||||
"editor-paste",
|
||||
(evt: ClipboardEvent, editor: Editor, markdownView: MarkdownView) => {
|
||||
const allowUpload = this.helper.getFrontmatterValue(
|
||||
"image-auto-upload",
|
||||
this.settings.uploadByClipSwitch
|
||||
);
|
||||
|
||||
let files = evt.clipboardData.files;
|
||||
if (!allowUpload) {
|
||||
return;
|
||||
}
|
||||
// 剪贴板内容有md格式的图片时
|
||||
if (this.settings.workOnNetWork) {
|
||||
const clipboardValue = evt.clipboardData.getData("text/plain");
|
||||
const imageList = this.helper
|
||||
.getImageLink(clipboardValue)
|
||||
.filter(image => image.path.startsWith("http"))
|
||||
.filter(
|
||||
image =>
|
||||
!this.helper.hasBlackDomain(
|
||||
image.path,
|
||||
this.settings.newWorkBlackDomains
|
||||
)
|
||||
);
|
||||
|
||||
if (imageList.length !== 0) {
|
||||
this.uploader
|
||||
.uploadFilesByPath(imageList.map(item => item.path))
|
||||
.then(res => {
|
||||
let value = this.helper.getValue();
|
||||
if (res.success) {
|
||||
let uploadUrlList = res.result;
|
||||
imageList.map(item => {
|
||||
const uploadImage = uploadUrlList.shift();
|
||||
value = value.replaceAll(
|
||||
item.source,
|
||||
``
|
||||
);
|
||||
});
|
||||
this.helper.setValue(value);
|
||||
const uploadUrlFullResultList = res.fullResult || [];
|
||||
this.settings.uploadedImages = [
|
||||
...(this.settings.uploadedImages || []),
|
||||
...uploadUrlFullResultList,
|
||||
];
|
||||
this.saveSettings();
|
||||
} else {
|
||||
new Notice("Upload error");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 剪贴板中是图片时进行上传
|
||||
if (this.canUpload(evt.clipboardData)) {
|
||||
this.uploadFileAndEmbedImgurImage(
|
||||
editor,
|
||||
async (editor: Editor, pasteId: string) => {
|
||||
let res = await this.uploader.uploadFileByClipboard(evt);
|
||||
if (res.code !== 0) {
|
||||
this.handleFailedUpload(editor, pasteId, res.msg);
|
||||
return;
|
||||
}
|
||||
const url = res.data;
|
||||
const uploadUrlFullResultList = res.fullResult || [];
|
||||
this.settings.uploadedImages = [
|
||||
...(this.settings.uploadedImages || []),
|
||||
...uploadUrlFullResultList,
|
||||
];
|
||||
await this.saveSettings();
|
||||
return url;
|
||||
},
|
||||
evt.clipboardData
|
||||
).catch();
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
this.registerEvent(
|
||||
this.app.workspace.on(
|
||||
"editor-drop",
|
||||
async (evt: DragEvent, editor: Editor, markdownView: MarkdownView) => {
|
||||
const allowUpload = this.helper.getFrontmatterValue(
|
||||
"image-auto-upload",
|
||||
this.settings.uploadByClipSwitch
|
||||
);
|
||||
let files = evt.dataTransfer.files;
|
||||
if (!allowUpload) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.length !== 0 && files[0].type.startsWith("image")) {
|
||||
let files = evt.dataTransfer.files;
|
||||
evt.preventDefault();
|
||||
|
||||
const data = await this.uploader.uploadFiles(Array.from(files));
|
||||
|
||||
if (data.success) {
|
||||
const uploadUrlFullResultList = data.fullResult ?? [];
|
||||
this.settings.uploadedImages = [
|
||||
...(this.settings.uploadedImages ?? []),
|
||||
...uploadUrlFullResultList,
|
||||
];
|
||||
this.saveSettings();
|
||||
data.result.map((value: string) => {
|
||||
let pasteId = (Math.random() + 1).toString(36).substring(2, 7);
|
||||
this.insertTemporaryText(editor, pasteId);
|
||||
this.embedMarkDownImage(editor, pasteId, value, files[0].name);
|
||||
});
|
||||
} else {
|
||||
new Notice("Upload error");
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
canUpload(clipboardData: DataTransfer) {
|
||||
this.settings.applyImage;
|
||||
const files = clipboardData.files;
|
||||
const text = clipboardData.getData("text");
|
||||
|
||||
const hasImageFile =
|
||||
files.length !== 0 && files[0].type.startsWith("image");
|
||||
if (hasImageFile) {
|
||||
if (!!text) {
|
||||
return this.settings.applyImage;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFileAndEmbedImgurImage(
|
||||
editor: Editor,
|
||||
callback: Function,
|
||||
clipboardData: DataTransfer
|
||||
) {
|
||||
let pasteId = (Math.random() + 1).toString(36).substring(2, 7);
|
||||
this.insertTemporaryText(editor, pasteId);
|
||||
const name = clipboardData.files[0].name;
|
||||
try {
|
||||
const url = await callback(editor, pasteId);
|
||||
this.embedMarkDownImage(editor, pasteId, url, name);
|
||||
} catch (e) {
|
||||
this.handleFailedUpload(editor, pasteId, e);
|
||||
}
|
||||
}
|
||||
|
||||
insertTemporaryText(editor: Editor, pasteId: string) {
|
||||
let progressText = imageAutoUploadPlugin.progressTextFor(pasteId);
|
||||
editor.replaceSelection(progressText + "\n");
|
||||
}
|
||||
|
||||
private static progressTextFor(id: string) {
|
||||
return `![Uploading file...${id}]()`;
|
||||
}
|
||||
|
||||
embedMarkDownImage(
|
||||
editor: Editor,
|
||||
pasteId: string,
|
||||
imageUrl: any,
|
||||
name: string = ""
|
||||
) {
|
||||
let progressText = imageAutoUploadPlugin.progressTextFor(pasteId);
|
||||
const imageSizeSuffix = this.settings.imageSizeSuffix || "";
|
||||
let markDownImage = ``;
|
||||
|
||||
imageAutoUploadPlugin.replaceFirstOccurrence(
|
||||
editor,
|
||||
progressText,
|
||||
markDownImage
|
||||
);
|
||||
}
|
||||
|
||||
handleFailedUpload(editor: Editor, pasteId: string, reason: any) {
|
||||
new Notice(reason);
|
||||
console.error("Failed request: ", reason);
|
||||
let progressText = imageAutoUploadPlugin.progressTextFor(pasteId);
|
||||
imageAutoUploadPlugin.replaceFirstOccurrence(
|
||||
editor,
|
||||
progressText,
|
||||
"⚠️upload failed, check dev console"
|
||||
);
|
||||
}
|
||||
|
||||
static replaceFirstOccurrence(
|
||||
editor: Editor,
|
||||
target: string,
|
||||
replacement: string
|
||||
) {
|
||||
let lines = editor.getValue().split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let ch = lines[i].indexOf(target);
|
||||
if (ch != -1) {
|
||||
let from = { line: i, ch: ch };
|
||||
let to = { line: i, ch: ch + target.length };
|
||||
editor.replaceRange(replacement, from, to);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
194
src/setting.ts
Normal file
194
src/setting.ts
Normal file
@ -0,0 +1,194 @@
|
||||
import { App, PluginSettingTab, Setting, Notice } from "obsidian";
|
||||
import imageAutoUploadPlugin from "./main";
|
||||
import { t } from "./lang/helpers";
|
||||
|
||||
export interface PluginSettings {
|
||||
uploadByClipSwitch: boolean;
|
||||
uploadServer: string;
|
||||
token: string;
|
||||
strategy_id: string;
|
||||
uploader: string;
|
||||
workOnNetWork: boolean;
|
||||
newWorkBlackDomains: string;
|
||||
fixPath: boolean;
|
||||
applyImage: boolean;
|
||||
deleteSource: boolean;
|
||||
isPublic: boolean;
|
||||
albumId: string;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: PluginSettings = {
|
||||
uploadByClipSwitch: true,
|
||||
uploader: "LskyPro",
|
||||
token: "",
|
||||
strategy_id:"",
|
||||
uploadServer: "https://img.czl.net",
|
||||
workOnNetWork: false,
|
||||
fixPath: false,
|
||||
applyImage: true,
|
||||
newWorkBlackDomains: "",
|
||||
deleteSource: false,
|
||||
isPublic: false,
|
||||
albumId: "",
|
||||
};
|
||||
|
||||
export class SettingTab extends PluginSettingTab {
|
||||
plugin: imageAutoUploadPlugin;
|
||||
|
||||
constructor(app: App, plugin: imageAutoUploadPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
let { containerEl } = this;
|
||||
containerEl.empty();
|
||||
new Setting(containerEl)
|
||||
.setName(t("Auto pasted upload"))
|
||||
.setDesc(
|
||||
"启用该选项后,黏贴图片时会自动上传到lsky图床"
|
||||
)
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.uploadByClipSwitch)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.uploadByClipSwitch = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Default uploader"))
|
||||
.setDesc(t("Default uploader"))
|
||||
.addDropdown(cb =>
|
||||
cb
|
||||
.addOption("LskyPro", "LskyPro")
|
||||
.setValue(this.plugin.settings.uploader)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.uploader = value;
|
||||
this.display();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
if (this.plugin.settings.uploader === "LskyPro") {
|
||||
new Setting(containerEl)
|
||||
.setName("LskyPro 域名")
|
||||
.setDesc("LskyPro 域名(不需要填写完整的API路径)")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("请输入LskyPro 域名(例如:https://img.czl.net)")
|
||||
.setValue(this.plugin.settings.uploadServer)
|
||||
.onChange(async key => {
|
||||
this.plugin.settings.uploadServer = key;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("LskyPro Token")
|
||||
.setDesc("LskyPro Token(不需要包含Bearer)")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("请输入LskyPro Token")
|
||||
.setValue(this.plugin.settings.token)
|
||||
.onChange(async key => {
|
||||
this.plugin.settings.token = key;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("LskyPro Strategy id")
|
||||
.setDesc("LskyPro 存储策略ID(非必填)")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("存储策略ID")
|
||||
.setValue(this.plugin.settings.strategy_id)
|
||||
.onChange(async key => {
|
||||
this.plugin.settings.strategy_id = key;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("图片是否公开")
|
||||
.setDesc("设置上传图片的权限(开启=公开,关闭=私有)")
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.isPublic)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.isPublic = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("相册ID")
|
||||
.setDesc("指定上传到的相册ID(可选)")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("相册ID")
|
||||
.setValue(this.plugin.settings.albumId)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.albumId = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Work on network"))
|
||||
.setDesc(t("Work on network Description"))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.workOnNetWork)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.workOnNetWork = value;
|
||||
this.display();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Network Domain Black List"))
|
||||
.setDesc(t("Network Domain Black List Description"))
|
||||
.addTextArea(textArea =>
|
||||
textArea
|
||||
.setValue(this.plugin.settings.newWorkBlackDomains)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.newWorkBlackDomains = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Upload when clipboard has image and text together"))
|
||||
.setDesc(
|
||||
t(
|
||||
"When you copy, some application like Excel will image and text to clipboard, you can upload or not."
|
||||
)
|
||||
)
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.applyImage)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.applyImage = value;
|
||||
this.display();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Delete source file after you upload file"))
|
||||
.setDesc(t("Delete source file in ob assets after you upload file."))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.deleteSource)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.deleteSource = value;
|
||||
this.display();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
201
src/uploader.ts
Normal file
201
src/uploader.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { PluginSettings } from "./setting";
|
||||
import { App, TFile, Notice } from "obsidian";
|
||||
|
||||
// 添加返回值类型接口
|
||||
interface UploadResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: string;
|
||||
fullResult?: Array<any>;
|
||||
}
|
||||
|
||||
//兰空上传器
|
||||
export class LskyProUploader {
|
||||
settings: PluginSettings;
|
||||
lskyUrl: string;
|
||||
app: App;
|
||||
|
||||
constructor(settings: PluginSettings, app: App) {
|
||||
this.settings = settings;
|
||||
this.lskyUrl = this.settings.uploadServer.endsWith("/")
|
||||
? this.settings.uploadServer + "api/v1/upload"
|
||||
: this.settings.uploadServer + "/api/v1/upload";
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
//上传请求配置
|
||||
getRequestOptions(file: File) {
|
||||
let headers = new Headers();
|
||||
// 严格检查 token
|
||||
if (!this.settings.token || this.settings.token.trim() === '') {
|
||||
throw new Error('请先配置 Token');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 设置认证头
|
||||
const authHeader = `Bearer ${this.settings.token.trim()}`;
|
||||
headers.append("Authorization", authHeader);
|
||||
|
||||
|
||||
|
||||
headers.append("Accept", "application/json");
|
||||
|
||||
let formdata = new FormData();
|
||||
formdata.append("file", file);
|
||||
|
||||
// 添加策略ID(如果有)
|
||||
if (this.settings.strategy_id) {
|
||||
formdata.append("strategy_id", this.settings.strategy_id);
|
||||
}
|
||||
|
||||
// 添加权限设置
|
||||
formdata.append("permission", this.settings.isPublic ? "1" : "0");
|
||||
|
||||
// 添加相册ID(如果有)
|
||||
if (this.settings.albumId) {
|
||||
formdata.append("album_id", this.settings.albumId);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: formdata,
|
||||
};
|
||||
}
|
||||
|
||||
// 修改重试逻辑,增加对 401 的处理
|
||||
async retryFetch(url: string, options: RequestInit, maxRetries = 3): Promise<Response> {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
// 如果是认证错误,直接抛出错误不重试
|
||||
if (response.status === 401) {
|
||||
throw new Error('Token 无效或已过期,请检查配置');
|
||||
}
|
||||
|
||||
// 对其他错误进行重试
|
||||
if (response.ok || response.status !== 403) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// 如果是 403 错误,等待一段时间后重试
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1 || error.message.includes('Token')) {
|
||||
throw error;
|
||||
}
|
||||
// 等待后重试
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
|
||||
}
|
||||
}
|
||||
throw new Error(`Failed after ${maxRetries} retries`);
|
||||
}
|
||||
|
||||
//上传文件,返回promise对象
|
||||
async promiseRequest(file: any): Promise<UploadResponse> {
|
||||
try {
|
||||
let requestOptions = this.getRequestOptions(file);
|
||||
const response = await this.retryFetch(this.lskyUrl, requestOptions);
|
||||
const value = await response.json();
|
||||
|
||||
if (!value.status) {
|
||||
const errorMsg = value.message || '上传失败';
|
||||
new Notice(`上传失败: ${errorMsg}`);
|
||||
return {
|
||||
code: -1,
|
||||
msg: errorMsg,
|
||||
data: value.data,
|
||||
fullResult: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: value.data?.links?.url,
|
||||
fullResult: []
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("Upload error:", error);
|
||||
new Notice(error.message || '上传出错');
|
||||
return {
|
||||
code: -1,
|
||||
msg: error.message || '上传失败',
|
||||
data: "",
|
||||
fullResult: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//通过路径创建文件
|
||||
async createFileObjectFromPath(path: string) {
|
||||
return new Promise(resolve => {
|
||||
if(path.startsWith('https://') || path.startsWith('http://')){
|
||||
return fetch(path).then(response => {
|
||||
return response.blob().then(blob => {
|
||||
resolve(new File([blob], path.split("/").pop()));
|
||||
});
|
||||
});
|
||||
}
|
||||
let obsfile = this.app.vault.getAbstractFileByPath(path);
|
||||
//@ts-ignore
|
||||
this.app.vault.readBinary(obsfile).then(data=>{
|
||||
const fileName = path.split("/").pop(); // 获取文件名
|
||||
const fileExtension = fileName.split(".").pop(); // 获取后缀名
|
||||
const blob = new Blob([data], { type: "image/" + fileExtension });
|
||||
const file = new File([blob], fileName);
|
||||
resolve(file);
|
||||
}).catch(err=>{
|
||||
console.error("Error reading file:", err);
|
||||
return;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async uploadFilesByPath(fileList: Array<String>): Promise<any> {
|
||||
let promiseArr = fileList.map(async filepath => {
|
||||
let file = await this.createFileObjectFromPath(filepath.format());
|
||||
return this.promiseRequest(file);
|
||||
});
|
||||
try {
|
||||
let reurnObj = await Promise.all(promiseArr);
|
||||
return {
|
||||
result: reurnObj.map((item: { data: string }) => item.data),
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
async uploadFiles(fileList: Array<File>): Promise<any> {
|
||||
let promiseArr = fileList.map(async file => {
|
||||
return this.promiseRequest(file);
|
||||
});
|
||||
try {
|
||||
let reurnObj = await Promise.all(promiseArr);
|
||||
let failItem:any = reurnObj.find((item: { code: number })=>item.code===-1);
|
||||
if (failItem) {
|
||||
throw {err:failItem.msg}
|
||||
}
|
||||
return {
|
||||
result: reurnObj.map((item: { data: string }) => item.data),
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
async uploadFileByClipboard(evt: ClipboardEvent): Promise<any> {
|
||||
let files = evt.clipboardData.files;
|
||||
let file = files[0];
|
||||
return this.promiseRequest(file);
|
||||
}
|
||||
}
|
79
src/utils.ts
Normal file
79
src/utils.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { extname } from "path";
|
||||
import { Readable } from "stream";
|
||||
|
||||
export interface IStringKeyMap<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
const IMAGE_EXT_LIST = [
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".bmp",
|
||||
".gif",
|
||||
".svg",
|
||||
".tiff",
|
||||
".webp",
|
||||
".avif",
|
||||
];
|
||||
|
||||
export function isAnImage(ext: string) {
|
||||
return IMAGE_EXT_LIST.includes(ext.toLowerCase());
|
||||
}
|
||||
export function isAssetTypeAnImage(path: string): Boolean {
|
||||
return isAnImage(extname(path));
|
||||
}
|
||||
|
||||
export async function streamToString(stream: Readable) {
|
||||
const chunks: Uint8Array[] = [];
|
||||
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(new Uint8Array(chunk));
|
||||
}
|
||||
|
||||
// 计算总长度
|
||||
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
||||
|
||||
// 创建一个新的 Uint8Array 并复制所有数据
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(result);
|
||||
}
|
||||
|
||||
export function getUrlAsset(url: string) {
|
||||
return (url = url.substring(1 + url.lastIndexOf("/")).split("?")[0]).split(
|
||||
"#"
|
||||
)[0];
|
||||
}
|
||||
|
||||
export function getLastImage(list: string[]) {
|
||||
const reversedList = list.reverse();
|
||||
let lastImage;
|
||||
reversedList.forEach(item => {
|
||||
if (item && item.startsWith("http")) {
|
||||
lastImage = item;
|
||||
return item;
|
||||
}
|
||||
});
|
||||
return lastImage;
|
||||
}
|
||||
|
||||
interface AnyObj {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function arrayToObject<T extends AnyObj>(
|
||||
arr: T[],
|
||||
key: string
|
||||
): { [key: string]: T } {
|
||||
const obj: { [key: string]: T } = {};
|
||||
arr.forEach(element => {
|
||||
obj[element[key]] = element;
|
||||
});
|
||||
return obj;
|
||||
}
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "es5",
|
||||
"downlevelIteration": true,
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"lib": ["dom", "es5", "scripthost", "es2015", "es2020", "ESNext"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
3
versions.json
Normal file
3
versions.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"1.0.0": "0.0.1"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user