Skip to content

Commit

Permalink
feat: add clipboard image support
Browse files Browse the repository at this point in the history
  • Loading branch information
terwer committed Mar 19, 2024
1 parent 6789d48 commit 5c131a0
Show file tree
Hide file tree
Showing 25 changed files with 724 additions and 28 deletions.
1 change: 1 addition & 0 deletions libs/Universal-PicGo-Core/.eslintrc.cjs
Expand Up @@ -16,6 +16,7 @@ module.exports = {
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/ban-types": "off",
"turbo/no-undeclared-env-vars": "off",
"prettier/prettier": "error",
},
Expand Down
1 change: 1 addition & 0 deletions libs/Universal-PicGo-Core/package.json
Expand Up @@ -31,6 +31,7 @@
},
"dependencies": {
"@picgo/i18n": "^1.0.0",
"dayjs": "^1.11.10",
"js-yaml": "^4.1.0",
"universal-picgo-store": "workspace:*",
"zhi-lib-base": "^0.8.0"
Expand Down
128 changes: 126 additions & 2 deletions libs/Universal-PicGo-Core/src/core/Lifecycle.ts
Expand Up @@ -8,8 +8,11 @@
*/

import { EventEmitter } from "../utils/nodePolyfill"
import { IPicGo } from "../types"
import { ILifecyclePlugins, IPicGo, IPlugin, Undefinable } from "../types"
import { ILogger } from "zhi-lib-base"
import { createContext } from "../utils/createContext"
import { IBuildInEvent } from "../utils/enums"
import { handleUrlEncode } from "../utils/common"

export class Lifecycle extends EventEmitter {
private readonly ctx: IPicGo
Expand All @@ -23,6 +26,127 @@ export class Lifecycle extends EventEmitter {
}

async start(input: any[]): Promise<IPicGo> {
throw new Error("Lifecycle.start is not implemented")
// ensure every upload process has an unique context
const ctx = createContext(this.ctx)
try {
// images input
if (!Array.isArray(input)) {
throw new Error("Input must be an array.")
}
ctx.input = input
ctx.output = []

// lifecycle main
await this.beforeTransform(ctx)
await this.doTransform(ctx)
await this.beforeUpload(ctx)
await this.doUpload(ctx)
await this.afterUpload(ctx)
return ctx
} catch (e: any) {
ctx.log.warn(IBuildInEvent.FAILED)
ctx.emit(IBuildInEvent.UPLOAD_PROGRESS, -1)
ctx.emit(IBuildInEvent.FAILED, e)
ctx.log.error(e)
if (ctx.getConfig<Undefinable<string>>("debug")) {
throw e
}
return ctx
}
}

private async beforeTransform(ctx: IPicGo): Promise<IPicGo> {
ctx.emit(IBuildInEvent.UPLOAD_PROGRESS, 0)
ctx.emit(IBuildInEvent.BEFORE_TRANSFORM, ctx)
ctx.log.info("Before transform")
await this.handlePlugins(ctx.helper.beforeTransformPlugins, ctx)
return ctx
}

private async doTransform(ctx: IPicGo): Promise<IPicGo> {
ctx.emit(IBuildInEvent.UPLOAD_PROGRESS, 30)
const type = ctx.getConfig<Undefinable<string>>("picBed.transformer") || "path"
let currentTransformer = type
let transformer = ctx.helper.transformer.get(type)
if (!transformer) {
transformer = ctx.helper.transformer.get("path")
currentTransformer = "path"
ctx.log.warn(`Can't find transformer - ${type}, switch to default transformer - path`)
}
ctx.log.info(`Transforming... Current transformer is [${currentTransformer}]`)
await transformer?.handle(ctx)
return ctx
}

private async beforeUpload(ctx: IPicGo): Promise<IPicGo> {
ctx.emit(IBuildInEvent.UPLOAD_PROGRESS, 60)
ctx.log.info("Before upload")
ctx.emit(IBuildInEvent.BEFORE_UPLOAD, ctx)
await this.handlePlugins(ctx.helper.beforeUploadPlugins, ctx)
return ctx
}

private async doUpload(ctx: IPicGo): Promise<IPicGo> {
let type =
ctx.getConfig<Undefinable<string>>("picBed.uploader") ||
ctx.getConfig<Undefinable<string>>("picBed.current") ||
"smms"
let uploader = ctx.helper.uploader.get(type)
let currentTransformer = type
if (!uploader) {
type = "smms"
currentTransformer = "smms"
uploader = ctx.helper.uploader.get("smms")
ctx.log.warn(`Can't find uploader - ${type}, switch to default uploader - smms`)
}
ctx.log.info(`Uploading... Current uploader is [${currentTransformer}]`)
await uploader?.handle(ctx)
for (const outputImg of ctx.output) {
outputImg.type = type
}
return ctx
}

private async afterUpload(ctx: IPicGo): Promise<IPicGo> {
ctx.emit(IBuildInEvent.AFTER_UPLOAD, ctx)
ctx.emit(IBuildInEvent.UPLOAD_PROGRESS, 100)
await this.handlePlugins(ctx.helper.afterUploadPlugins, ctx)
let msg = ""
const length = ctx.output.length
// notice, now picgo builtin uploader will encodeOutputURL by default
const isEncodeOutputURL = ctx.getConfig<Undefinable<boolean>>("settings.encodeOutputURL") === true
for (let i = 0; i < length; i++) {
if (typeof ctx.output[i].imgUrl !== "undefined") {
msg += isEncodeOutputURL ? handleUrlEncode(ctx.output[i].imgUrl!) : ctx.output[i].imgUrl!
if (i !== length - 1) {
msg += "\n"
}
}
delete ctx.output[i].base64Image
delete ctx.output[i].buffer
}
ctx.emit(IBuildInEvent.FINISHED, ctx)
ctx.log.info(`\n${msg}`)
return ctx
}

// ===================================================================================================================

private async handlePlugins(lifeCyclePlugins: ILifecyclePlugins, ctx: IPicGo): Promise<IPicGo> {
const plugins = lifeCyclePlugins.getList()
const pluginNames = lifeCyclePlugins.getIdList()
const lifeCycleName = lifeCyclePlugins.getName()
await Promise.all(
plugins.map(async (plugin: IPlugin, index: number) => {
try {
ctx.log.info(`${lifeCycleName}: ${pluginNames[index]} running`)
await plugin.handle(ctx)
} catch (e) {
ctx.log.error(`${lifeCycleName}: ${pluginNames[index]} error`)
throw e
}
})
)
return ctx
}
}
4 changes: 1 addition & 3 deletions libs/Universal-PicGo-Core/src/i18n/index.ts
Expand Up @@ -90,9 +90,7 @@ class I18nManager implements II18nManager {
const fs = win.fs
const path = win.require("path")
i18nFolder = path.join(this.ctx.baseDir, "i18n-cli")
if (!pathExistsSync(fs, path, i18nFolder)) {
ensureFolderSync(fs, i18nFolder)
}
ensureFolderSync(fs, i18nFolder)
} else {
i18nFolder = browserPathJoin(this.ctx.baseDir, "i18n-cli", "i18n.json")
}
Expand Down
2 changes: 2 additions & 0 deletions libs/Universal-PicGo-Core/src/index.ts
@@ -1,3 +1,5 @@
import { UniversalPicGo } from "./core/UniversalPicGo"
import { win, hasNodeEnv } from "universal-picgo-store"

export { UniversalPicGo }
export { win, hasNodeEnv }
6 changes: 5 additions & 1 deletion libs/Universal-PicGo-Core/src/lib/PluginLoader.ts
Expand Up @@ -13,19 +13,22 @@ import { hasNodeEnv, win } from "universal-picgo-store/src"
import { readJSONSync } from "../utils/nodeUtils"
import { IBuildInEvent } from "../utils/enums"
import { setCurrentPluginName } from "./LifecyclePlugins"
import { ILogger } from "zhi-lib-base"

/**
* Local plugin loader, file system is required
*/
export class PluginLoader implements IPluginLoader {
private readonly ctx: IPicGo
private readonly logger: ILogger
private db: PluginLoaderDb
private list: string[] = []
private readonly fullList: Set<string> = new Set()
private readonly pluginMap: Map<string, IPicGoPluginInterface> = new Map()

constructor(ctx: IPicGo) {
this.ctx = ctx
this.logger = ctx.getLogger("plugin-loader")
this.db = new PluginLoaderDb(this.ctx)
this.init()
}
Expand Down Expand Up @@ -54,7 +57,8 @@ export class PluginLoader implements IPluginLoader {
}
return true
} else {
throw new Error("load is not supported in browser")
this.logger.warn("load is not supported in browser")
return false
}
}

Expand Down
16 changes: 16 additions & 0 deletions libs/Universal-PicGo-Core/src/utils/clipboard/browser.ts
@@ -0,0 +1,16 @@
/*
* GNU GENERAL PUBLIC LICENSE
* Version 3, 29 June 2007
*
* Copyright (C) 2024 Terwer, Inc. <https://terwer.space/>
* Everyone is permitted to copy and distribute verbatim copies
* of this license document, but changing it is not allowed.
*/

import { IClipboardImage, IPicGo } from "../../types"

const getClipboardImageBrowser = async (ctx: IPicGo): Promise<IClipboardImage> => {
throw new Error("getClipboardImage is not supported in browser")
}

export { getClipboardImageBrowser }
72 changes: 72 additions & 0 deletions libs/Universal-PicGo-Core/src/utils/clipboard/electron.ts
@@ -0,0 +1,72 @@
/*
* GNU GENERAL PUBLIC LICENSE
* Version 3, 29 June 2007
*
* Copyright (C) 2024 Terwer, Inc. <https://terwer.space/>
* Everyone is permitted to copy and distribute verbatim copies
* of this license document, but changing it is not allowed.
*/

import { IClipboardImage, IPicGo } from "../../types"
import { win } from "universal-picgo-store"
import { CLIPBOARD_IMAGE_FOLDER } from "../constants"
import { ensureFolderSync } from "../nodeUtils"
import dayjs from "dayjs"
import { getCurrentPlatform, Platform } from "../os"
import macClipboardScript from "./script/mac.applescript?raw"
import windowsClipboardScript from "./script/windows.ps1?raw"
import windows10ClipboardScript from "./script/windows10.ps1?raw"
import linuxClipboardScript from "./script/linux.sh?raw"
import wslClipboardScript from "./script/wsl.sh?raw"

const platform2ScriptContent: {
[key in Platform]: string
} = {
darwin: macClipboardScript,
win32: windowsClipboardScript,
win10: windows10ClipboardScript,
linux: linuxClipboardScript,
wsl: wslClipboardScript,
}

/**
* powershell will report error if file does not have a '.ps1' extension,
* so we should keep the extension name consistent with corresponding shell
*/
const platform2ScriptFilename: {
[key in Platform]: string
} = {
darwin: "mac.applescript",
win32: "windows.ps1",
win10: "windows10.ps1",
linux: "linux.sh",
wsl: "wsl.sh",
}

const createImageFolder = (ctx: IPicGo): void => {
const fs = win.fs
const path = win.require("path")
const imagePath = path.join(ctx.baseDir, CLIPBOARD_IMAGE_FOLDER)
ensureFolderSync(fs, imagePath)
}

const getClipboardImageElectron = async (ctx: IPicGo): Promise<IClipboardImage> => {
const fs = win.fs
const path = win.require("path")

createImageFolder(ctx)
// add an clipboard image folder to control the image cache file
const imagePath = path.join(ctx.baseDir, CLIPBOARD_IMAGE_FOLDER, `${dayjs().format("YYYYMMDDHHmmss")}.png`)
return await new Promise<IClipboardImage>((resolve: Function, reject: Function): void => {
const platform = getCurrentPlatform()
const scriptPath = path.join(ctx.baseDir, platform2ScriptFilename[platform])
// If the script does not exist yet, we need to write the content to the script file
if (!fs.existsSync(scriptPath)) {
fs.writeFileSync(scriptPath, platform2ScriptContent[platform], "utf8")
}

throw new Error("开发中...")
})
}

export { getClipboardImageElectron }
49 changes: 49 additions & 0 deletions libs/Universal-PicGo-Core/src/utils/clipboard/script/linux.sh
@@ -0,0 +1,49 @@
#!/bin/sh

if [ -z "$DISPLAY" ]; then
echo "no support" >&2
exit 1
fi

case "$XDG_SESSION_TYPE" in
wayland)
command -v wl-copy >/dev/null 2>&1 || {
echo >&2 "no wl-clipboard"
exit 1
}
filePath=$(wl-copy -o 2>/dev/null | grep ^file:// | cut -c8-)
if [ -z "$filePath" ]; then
if
wl-copy -t image/png image/png -o >"$1" 2>/dev/null
then
echo "$1"
else
rm -f "$1"
echo "no image"
fi
else
echo "$filePath"
fi
;;
x11 | tty)
# require xclip(see http://stackoverflow.com/questions/592620/check-if-a-program-exists-from-a-bash-script/677212#677212)
command -v xclip >/dev/null 2>&1 || {
echo >&2 "no xclip"
exit 1
}
# write image in clipboard to file (see http://unix.stackexchange.com/questions/145131/copy-image-from-clipboard-to-file)
filePath=$(xclip -selection clipboard -o 2>/dev/null | grep ^file:// | cut -c8-)
if [ -z "$filePath" ]; then
if
xclip -selection clipboard -target image/png -o >"$1" 2>/dev/null
then
echo "$1"
else
rm -f "$1"
echo "no image"
fi
else
echo "$filePath"
fi
;;
esac
@@ -0,0 +1,41 @@
-- From https://github.com/mushanshitiancai/vscode-paste-image
property fileTypes : {{«class PNGf», ".png"}}

on run argv
if argv is {} then
return ""
end if

if ((clipboard info) as string) contains "«class furl»" then
return POSIX path of (the clipboard as «class furl»)
else
set imagePath to (item 1 of argv)
set theType to getType()

if theType is not missing value then
try
set myFile to (open for access imagePath with write permission)
set eof myFile to 0
write (the clipboard as (first item of theType)) to myFile
close access myFile
return (POSIX path of imagePath)
on error
try
close access myFile
end try
return "no image"
end try
else
return "no image"
end if
end if
end run

on getType()
repeat with aType in fileTypes
repeat with theInfo in (clipboard info)
if (first item of theInfo) is equal to (first item of aType) then return aType
end repeat
end repeat
return missing value
end getType

0 comments on commit 5c131a0

Please sign in to comment.