Compare commits

...
56 Commits
Author SHA1 Message Date
PiEgg ce8f41516c 🎉 Release: v2.3.0-beta.5 2021-04-04 17:53:41 +08:00
PiEgg f0e1fa12d7 Feature: add local plugin support && npm registry/proxy support 2021-04-04 17:52:50 +08:00
PiEgg 3cad5f378d 🔨 Refactor: upload module && notification module 2021-03-28 17:44:20 +08:00
f9d6191d5e 🔨 Refactor: improve others picbed config ui to support scroll (#635)
Co-authored-by: jiawei <jiawei@parim.net>
2021-03-01 11:25:26 +08:00
PiEgg 51d80a6a1d 🐛 Fix: default github placeholder 2021-01-13 20:28:03 +08:00
刘沛源andGitHub 0fe3ade5c3 Feature: 为Linux系统适配桌面图标栏(Tray) (#603) 2020-12-29 09:59:50 +08:00
PiEgg 2129f86266 🔨 Refactor: plugin name -> fullName to support scope packages 2020-12-26 23:39:48 +08:00
PiEgg 064f37d1a6 📦 Chore: change travis-ci -> GitHub Actions 2020-12-20 11:44:38 +08:00
PiEgg 48592ec9b5 🎉 Release: v2.3.0-beta.4 2020-12-19 20:59:56 +08:00
PiEgg 33fdb16ded 🐛 Fix: disabled plugin won't be shown in plugin page 2020-12-19 20:52:01 +08:00
Huiyi.FYJandGitHub bd2311bf50 📝 Docs: update install command by Homebrew (#599) 2020-12-16 10:59:58 +08:00
PiEgg e793599360 📝 Docs: update discussions in doc 2020-12-09 11:50:00 +08:00
PiEgg ce2b5cdeab 🐛 Fix: url encode before uploading by url
ISSUES CLOSED: #581
2020-11-10 10:47:35 +08:00
偕臧andGitHub 2151857c89 📝 Docs: update README.md (#555)
aurman is easy to cause misunderstanding, but yay is a command that can be run directly (aur's most popular assistant)
2020-09-30 17:39:05 +08:00
PiEgg 32b8b976be Feature(config): auto configuration backup & fallback to avoid main process crash
ISSUES CLOSED: #568
2020-09-30 17:32:35 +08:00
PiEgg eab014dc88 Feature: add global value for PicGo get GUI_VERSION & CORE_VERSION 2020-09-28 19:41:02 +08:00
PiEgg bb90e174f3 📝 Docs: rm gitads 2020-09-02 14:27:20 +08:00
PiEgg 7b5e5ef2ed 🐛 Fix: shortKeyConfig maybe undefined
ISSUES CLOSED: #557
2020-08-22 22:15:46 +08:00
PiEgg 92ff282d2f 📝 Docs: add flutter-picgo 2020-08-02 11:48:22 +08:00
PiEgg 2d42381f47 📝 Docs: add gitads 2020-07-13 19:27:51 +08:00
PiEgg 6e855384e0 🎉 Release: v2.3.0-beta.3 2020-07-12 22:02:58 +08:00
PiEgg 21d3942d2b 🐛 Fix: choose default picBed failure
ISSUES CLOSED: #537
2020-07-12 22:01:37 +08:00
PiEgg 4f0809e6b3 🐛 Fix: shortkey disabled failure
ISSUES CLOSED: #534
2020-07-12 18:19:31 +08:00
PiEgg f40a1bb981 🎉 Release: v2.3.0-beta.2 2020-07-12 15:14:15 +08:00
PiEgg 5f87018af8 🐛 Fix: initialize db bugs 2020-07-12 15:13:21 +08:00
Molunerfinn 7fabc47bd2 Feature: add qrcode for picbeds' config 2020-06-30 16:17:18 +08:00
Molunerfinn db71139f5f 🐛 Fix: encoding the result of picgo-server 2020-06-30 09:53:35 +08:00
Molunerfinn 1c5880ab7c 📝 Docs: update readme 2020-06-28 15:57:36 +08:00
Molunerfinn b93dd05fd3 🎉 Release: v2.3.0-beta.1 2020-06-28 15:31:36 +08:00
Molunerfinn 59d3eba86d 🐛 Fix: paste url encoding bug
ISSUES CLOSED: #454
2020-06-28 15:30:58 +08:00
Molunerfinn a693544291 ⬆️ Upgrade: picgo version to latest
ISSUES CLOSED: #299,#525
2020-06-28 15:28:59 +08:00
Molunerfinn 2c57a27284 📝 Docs: update FAQ && ISSUE_TEMPLATE 2020-05-05 11:21:27 +08:00
Molunerfinn b6e3adb7af 🐛 Fix: auto-copy option && copy style
auto-copy option not work && clear rules for line breaks when copying
2020-05-04 16:50:29 +08:00
Molunerfinn 2adff1e788 📝 Docs: update readme 2020-05-01 08:34:41 +08:00
Molunerfinn 18ad54251e 🐛 Fix: beta version update bug 2020-04-30 18:54:48 +08:00
Molunerfinn 72387baf55 🎉 Release: v2.3.0-beta.0 2020-04-30 17:35:36 +08:00
Molunerfinn b357dfb243 🐛 Fix: travis-ci bug 2020-04-30 17:34:59 +08:00
Molunerfinn ad6acd8c4f Feature: add beta-version update support 2020-04-30 16:47:09 +08:00
Molunerfinn 2aeca509a8 Feature: finish all-select && shift multi-select
ISSUES CLOSED: #342
2020-04-28 18:35:59 +08:00
Molunerfinn b7cbd50b17 🔨 Refactor: lifeCycle codes 2020-04-25 11:34:04 +08:00
Molunerfinn b5ceaeda1e 🔨 Refactor: windowManager 2020-04-13 11:35:48 +08:00
Molunerfinn 7e866185e9 🐛 Fix: give a hint when node.js is not installed 2020-04-11 20:53:29 +08:00
Molunerfinn 96cdfea497 🐛 Fix: right-click menu upload fails with PicGo open
ISSUES CLOSED: #415
2020-04-11 11:05:58 +08:00
Molunerfinn 3df2253ae9 🔨 Refactor: improved code structure for greater clarity and understanding 2020-04-10 23:28:46 +08:00
Molunerfinn cc182b08f7 🐛 Fix: some win10 upload clipboard image crash 2020-04-08 22:50:24 +08:00
Molunerfinn 67e526f163 Feature: add autoCopy option for users to use or not 2020-03-19 21:03:21 +08:00
Molunerfinn cd70a1a5cc 🐛 Fix: confused port number auto increasing when opening a new PicGo app 2020-03-19 19:25:11 +08:00
Molunerfinn f3575575ac 📝 Docs: update choco install picgo 2020-03-10 19:29:13 +08:00
Molunerfinn 3f3ea69cd6 Feature: add smms-v2 support 2020-03-07 20:10:32 +08:00
Molunerfinn 32334e9bc1 🐛 Fix: correct inputbox value && remove listener 2020-02-25 20:58:39 +08:00
Molunerfinn 96544f5475 🐛 Fix: url uploader bug 2020-02-23 17:48:12 +08:00
Molunerfinn a28c67848d Feature: add uploading image from URL support 2020-02-23 15:20:07 +08:00
Molunerfinn 23ba2338c6 🚧 WIP: add support for URL uploading 2020-02-21 22:25:48 +08:00
Molunerfinn 024d9cf41e 📦 Chore: update funding url 2020-01-31 11:01:56 +08:00
Molunerfinn fd7673e4d1 📝 Docs: update docs 2020-01-31 11:00:42 +08:00
Molunerfinn fb014ddc9e 📝 Docs: update readme 2020-01-20 21:27:42 +08:00
70 changed files with 2078 additions and 1153 deletions
+1
View File
@@ -0,0 +1 @@
custom: ["https://paypal.me/Molunerfinn"]
+11 -7
View File
@@ -1,30 +1,34 @@
<!--
PicGo Issue 模板
请依照该模板来提交,否则将会被关闭。
**提问之前请注意你看过FAQ、配置手册以及那些被关闭的issues。否则同样的提问也会被关闭!**
**提问之前请注意你看过 FAQ、配置手册以及那些被关闭的 issues。否则同样的提问也会被关闭!**
-->
**声明:我已经仔细看过 [文档](https://picgo.github.io/PicGo-Doc/)、[FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md)、和搜索过已经关闭的 [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed) 后依然没有找到答案,所以才发了一个新的 issue。**
## 问题类型
<!-- 你要提交的是Bug Report还是Feature Request-->
<!-- 你要提交的是 Bug Report 还是 Feature Request-->
## PicGo的相关信息
## PicGo 的相关信息
<!-- 版本、所在平台(Mac或者Windows -->
<!-- PicGo 的版本、所在平台(Mac 或者 Windows 或者 Linux -->
## 问题重现
<!-- 如果是Bug Report请填写本项 -->
<!-- 如果是 Bug Report 请填写本项 -->
<!-- 请附上相关截图 -->
<!-- 请附上 PicGo 的相关报错日志(用文本的形式)否则无法判断原因-->
<!-- 报错日志可以在 PicGo 设置 -> 设置日志文件 -> 点击打开 后找到 -->
## 功能请求
<!-- 如果是Feature Request请填写本项 -->
<!-- 如果是 Feature Request 请填写本项 -->
<!-- 详细描述你所预想的功能或者是现有功能的改进。 -->
---
<!--
最后,喜欢PicGo的话不妨给它点个star~
最后,喜欢 PicGo 的话不妨给它点个 star~
如果可以的话,请我喝杯咖啡?首页有赞助二维码,谢谢你的支持!
-->
+55
View File
@@ -0,0 +1,55 @@
# main.yml
# Workflow's name
name: Build
# Workflow's trigger
on:
push:
branches:
- master
# Workflow's jobs
jobs:
# job's id
release:
# job's name
name: build and release electron app
# the type of machine to run the job on
runs-on: ${{ matrix.os }}
# create a build matrix for jobs
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-10.15]
# create steps
steps:
# step1: check out repository
- name: Check out git repository
uses: actions/checkout@v2
# step2: install node env
- name: Install Node.js
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- name: Install system deps
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils
# step3: yarn
- name: Yarn install
run: |
yarn
yarn global add xvfb-maybe
- name: Build & release app
run: |
npm run release
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
+1
View File
@@ -16,3 +16,4 @@ docs/dist/
.env.local
.env.*.local
dist_electron/
test.js
+3 -3
View File
@@ -22,9 +22,7 @@ addons:
- icnsutils
#- xvfb
before_install:
- mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v1.2.1/git-lfs-$([
"$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-1.2.1.tar.gz
| tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install git-lfs; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi
install:
#- export DISPLAY=':99.0'
@@ -39,6 +37,8 @@ script:
#- yarn run pack && xvfb-maybe node_modules/.bin/mocha test/e2e
- npm run release
# - yarn run build:docs
before_script:
- git lfs pull
branches:
only:
- master
+130
View File
@@ -1,3 +1,133 @@
# :tada: 2.3.0-beta.5 (2021-04-04)
### :sparkles: Features
* add local plugin support && npm registry/proxy support ([f0e1fa1](https://github.com/Molunerfinn/PicGo/commit/f0e1fa1))
* 为Linux系统适配桌面图标栏(Tray) ([#603](https://github.com/Molunerfinn/PicGo/issues/603)) ([0fe3ade](https://github.com/Molunerfinn/PicGo/commit/0fe3ade))
### :bug: Bug Fixes
* default github placeholder ([51d80a6](https://github.com/Molunerfinn/PicGo/commit/51d80a6))
### :package: Chore
* change travis-ci -> GitHub Actions ([064f37d](https://github.com/Molunerfinn/PicGo/commit/064f37d))
# :tada: 2.3.0-beta.4 (2020-12-19)
### :sparkles: Features
* add global value for PicGo get GUI_VERSION & CORE_VERSION ([eab014d](https://github.com/Molunerfinn/PicGo/commit/eab014d))
* **config:** auto configuration backup & fallback to avoid main process crash ([32b8b97](https://github.com/Molunerfinn/PicGo/commit/32b8b97)), closes [#568](https://github.com/Molunerfinn/PicGo/issues/568)
### :bug: Bug Fixes
* disabled plugin won't be shown in plugin page ([33fdb16](https://github.com/Molunerfinn/PicGo/commit/33fdb16))
* shortKeyConfig maybe undefined ([7b5e5ef](https://github.com/Molunerfinn/PicGo/commit/7b5e5ef)), closes [#557](https://github.com/Molunerfinn/PicGo/issues/557)
* url encode before uploading by url ([ce2b5cd](https://github.com/Molunerfinn/PicGo/commit/ce2b5cd)), closes [#581](https://github.com/Molunerfinn/PicGo/issues/581)
### :pencil: Documentation
* add flutter-picgo ([92ff282](https://github.com/Molunerfinn/PicGo/commit/92ff282))
* add gitads ([2d42381](https://github.com/Molunerfinn/PicGo/commit/2d42381))
* rm gitads ([bb90e17](https://github.com/Molunerfinn/PicGo/commit/bb90e17))
* update discussions in doc ([e793599](https://github.com/Molunerfinn/PicGo/commit/e793599))
* update install command by Homebrew ([#599](https://github.com/Molunerfinn/PicGo/issues/599)) ([bd2311b](https://github.com/Molunerfinn/PicGo/commit/bd2311b))
* update README.md ([#555](https://github.com/Molunerfinn/PicGo/issues/555)) ([2151857](https://github.com/Molunerfinn/PicGo/commit/2151857))
# :tada: 2.3.0-beta.3 (2020-07-12)
### :bug: Bug Fixes
* choose default picBed failure ([21d3942](https://github.com/Molunerfinn/PicGo/commit/21d3942)), closes [#537](https://github.com/Molunerfinn/PicGo/issues/537)
* shortkey disabled failure ([4f0809e](https://github.com/Molunerfinn/PicGo/commit/4f0809e)), closes [#534](https://github.com/Molunerfinn/PicGo/issues/534)
# :tada: 2.3.0-beta.2 (2020-07-12)
### :sparkles: Features
* add qrcode for picbeds' config ([7fabc47](https://github.com/Molunerfinn/PicGo/commit/7fabc47))
### :bug: Bug Fixes
* encoding the result of picgo-server ([db71139](https://github.com/Molunerfinn/PicGo/commit/db71139))
* initialize db bugs ([5f87018](https://github.com/Molunerfinn/PicGo/commit/5f87018))
### :pencil: Documentation
* update readme ([1c5880a](https://github.com/Molunerfinn/PicGo/commit/1c5880a))
# :tada: 2.3.0-beta.1 (2020-06-28)
### :bug: Bug Fixes
* auto-copy option && copy style ([b6e3adb](https://github.com/Molunerfinn/PicGo/commit/b6e3adb))
* beta version update bug ([18ad542](https://github.com/Molunerfinn/PicGo/commit/18ad542))
* paste url encoding bug ([59d3eba](https://github.com/Molunerfinn/PicGo/commit/59d3eba)), closes [#454](https://github.com/Molunerfinn/PicGo/issues/454)
### :pencil: Documentation
* update FAQ && ISSUE_TEMPLATE ([2c57a27](https://github.com/Molunerfinn/PicGo/commit/2c57a27))
* update readme ([2adff1e](https://github.com/Molunerfinn/PicGo/commit/2adff1e))
# :tada: 2.3.0-beta.0 (2020-04-30)
### :sparkles: Features
* add autoCopy option for users to use or not ([67e526f](https://github.com/Molunerfinn/PicGo/commit/67e526f))
* add beta-version update support ([ad6acd8](https://github.com/Molunerfinn/PicGo/commit/ad6acd8))
* add smms-v2 support ([3f3ea69](https://github.com/Molunerfinn/PicGo/commit/3f3ea69))
* add uploading image from URL support ([a28c678](https://github.com/Molunerfinn/PicGo/commit/a28c678))
* finish all-select && shift multi-select ([2aeca50](https://github.com/Molunerfinn/PicGo/commit/2aeca50)), closes [#342](https://github.com/Molunerfinn/PicGo/issues/342)
### :bug: Bug Fixes
* confused port number auto increasing when opening a new PicGo app ([cd70a1a](https://github.com/Molunerfinn/PicGo/commit/cd70a1a))
* correct inputbox value && remove listener ([32334e9](https://github.com/Molunerfinn/PicGo/commit/32334e9))
* give a hint when node.js is not installed ([7e86618](https://github.com/Molunerfinn/PicGo/commit/7e86618))
* right-click menu upload fails with PicGo open ([96cdfea](https://github.com/Molunerfinn/PicGo/commit/96cdfea)), closes [#415](https://github.com/Molunerfinn/PicGo/issues/415)
* some win10 upload clipboard image crash ([cc182b0](https://github.com/Molunerfinn/PicGo/commit/cc182b0))
* travis-ci bug ([b357dfb](https://github.com/Molunerfinn/PicGo/commit/b357dfb))
* url uploader bug ([96544f5](https://github.com/Molunerfinn/PicGo/commit/96544f5))
### :pencil: Documentation
* update choco install picgo ([f357557](https://github.com/Molunerfinn/PicGo/commit/f357557))
* update docs ([fd7673e](https://github.com/Molunerfinn/PicGo/commit/fd7673e))
* update readme ([fb014dd](https://github.com/Molunerfinn/PicGo/commit/fb014dd))
### :package: Chore
* update funding url ([024d9cf](https://github.com/Molunerfinn/PicGo/commit/024d9cf))
## :tada: 2.2.2 (2020-01-20)
+24 -23
View File
@@ -1,58 +1,59 @@
## 常见问题
> 在使用PicGo期间你会遇到很多问题,不过很多问题其实之前就有人提问过,也被解决,所以你可以先看看[使用文档](https://github.com/Molunerfinn/PicGo/wiki/%E8%AF%A6%E7%BB%86%E7%AA%97%E5%8F%A3%E7%9A%84%E4%BD%BF%E7%94%A8#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%BF%AB%E6%8D%B7%E9%94%AE),这份FAQ,以及那些被关闭的[issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed),应该能找到答案。
> 在使用 PicGo 期间你会遇到很多问题,不过很多问题其实之前就有人提问过,也被解决,所以你可以先看看 [使用文档](https://picgo.github.io/PicGo-Doc/zh/guide/getting-started.html#%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B),这份 FAQ,以及那些被关闭的 [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed),应该能找到答案。
## 1.七牛图床上传图片成功后无法显示或图片无`http://`前缀
## 1. 七牛图床上传图片成功后,相册里无法显示或图片无`http://`前缀
通常是你的七牛图床配置里的`设定访问网址`没有加上`http://`或者`https//`头。
参考:[issue#79](https://github.com/Molunerfinn/PicGo/issues/79)
## 2.能否支持图床远端同步删除
## 2. 能否支持图床远端同步删除
不能。有些图床(比如微博图床、SM.MS、Imgur等)不支持后台管理,为了架构统一不支持远端删除。
不能。有些图床(比如微博图床、SM.MS、Imgur 等)不支持后台管理,为了架构统一不支持远端删除。
## 3.能否支持上传视频文件
## 3. 能否支持上传视频文件
目前不能。2.0版本出来后将会支持插件系统,届时如果有人开发了相应的插件理论可以支持任意文件上传。
目前不能。如果有人开发了相应的插件理论可以支持任意文件上传。
## 4.微博图床上传之后无法显示预览图
## 4. 微博图床上传之后无法显示预览图
通常是挂了全局代理导致的。
参考:[issue36](https://github.com/Molunerfinn/PicGo/issues/36)
## 5.能否支持某某某图床
## 5. 能否支持某某某图床
截止v1.6,PicGo支持了如下图床:
截止 v1.6PicGo 支持了如下图床:
- `微博图床` v1.0
- `七牛图床` v1.0
- `腾讯云COS v4\v5版本` v1.1 & v1.5.0
- `腾讯云 COS v4\v5 版本` v1.1 & v1.5.0
- `又拍云` v1.2.0
- `GitHub` v1.5.0
- `SM.MS` v1.5.1
- `阿里云OSS` v1.6.0
- `阿里云 OSS` v1.6.0
- `Imgur` v1.6.0
所以本体内将不会再支持其他图床。待2.0版本出来支持插件系统后可以由其他开发者开发相应的图床插件用以支持其他图床
所以本体内将不会再支持其他图床。需要其他图床支持可以参考目前已有的三方 [插件](https://github.com/PicGo/Awesome-PicGo),如果还是没有你所需要的图床欢迎开发一个插件供大家使用
## 6.一个图床设置多个信息
## 6. 一个图床设置多个信息
不能。因为目前的架构只支持一个图床一份信息。
## 7.v2.0版本有什么新功能
## 7. GitHub 图床有时能上传,有时上传失败
在不改变PicGo现有使用界面和使用习惯的基础上,将会重构整个底层`传输->转换->上传`的架构,使其能够支持插件系统,方便其他开发者基于PicGo的底层进行二次开发
1. GitHub 图床不支持上传同名文件,如果有同名文件上传,会报错。建议开启 `时间戳重命名` 避免同名文件
2. GitHub 服务器和国内 GFW 的问题会导致有时上传成功,有时上传失败,无解。想要稳定请使用付费云存储,如阿里云、腾讯云等,价格也不会贵。
理论上有了插件系统之后,其他开发者能够支持如下功能:
## 8. Mac 上无法打开 PicGo 的主窗口界面
1. 能够实现上传前图片压缩
2. 能够通过图片url实现上传图片 -> 更深入地可以批量地实现图床的迁移
3. 能够支持其他图床甚至是自己的图床
PicGo 在 Mac 上是一个顶部栏应用,在 dock 栏是不会有图标的。要打开主窗口,请右键或者双指点按顶部栏 PicGo 图标,选择「打开详细窗口」即可打开主窗口。
等等。
## 9. 上传失败,或者是服务器出错
届时底层架构将会独立出来成为一个独立的npm包,能够直接在命令行里调用
目前开发进度可以参考[PicGo-Core](https://github.com/Molunerfinn/PicGo-Core)
1. PicGo 自带的图床都经过测试,上传出错一般都不是 PicGo 自身的原因。如果你用的是 GitHub 图床请参考上面的第 7 点
2. 检查 PicGo 的日志(报错日志可以在 PicGo 设置 -> 设置日志文件 -> 点击打开 后找到),看看 `[PicGo Error]` 的报错信息里有什么关键信息
1. 先自行搜索 error 里的报错信息,往往你能百度或者谷歌出问题原因,不必开 issue
2. 如果有带有 `401``403``40X` 状态码字样的,不用怀疑,就是你配置写错了,仔细检查配置,看看是否多了空格之类的。
3. 如果带有 `HttpError``RequestError``socket hang up` 等字样的说明这是网络问题,我无法帮你解决网络问题,请检查你自己的网络,是否有代理,DNS 设置是否正常等。
+53 -30
View File
@@ -2,11 +2,11 @@
<img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/New%20LOGO-150.png" alt="">
<h1>PicGo</h1>
<blockquote>图片上传+管理新体验 </blockquote>
<a href="https://github.com/feross/standard">
<a href="https://github.com/Molunerfinn/PicGo/actions">
<img src="https://img.shields.io/badge/code%20style-standard-green.svg?style=flat-square" alt="">
</a>
<a href="https://travis-ci.org/Molunerfinn/PicGo/builds">
<img src="https://img.shields.io/travis/Molunerfinn/PicGo.svg?style=flat-square" alt="">
<a href="https://github.com/Molunerfinn/PicGo/actions">
<img src="https://github.com/Molunerfinn/PicGo/workflows/Build/badge.svg" alt="">
</a>
<a href="https://github.com/Molunerfinn/PicGo/releases">
<img src="https://img.shields.io/github/downloads/Molunerfinn/PicGo/total.svg?style=flat-square" alt="">
@@ -17,47 +17,67 @@
<a href="https://github.com/PicGo/bump-version">
<img src="https://img.shields.io/badge/picgo-convention-blue.svg?style=flat-square" alt="">
</a>
<a href="https://gitter.im/picgo-all/PicGo?utm_source=share-link&utm_medium=link&utm_campaign=share-link">
<img src="https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-66ae93.svg?style=flat-square" alt="">
</a>
</div>
## 应用说明
## 应用概述
**PicGo 在上传图片之后自动会将图片链接复制到你的剪贴板里,可选 5 种复制的链接格式。**
**PicGo: 一个用于快速上传图片并获取图片 URL 链接的工具**
PicGo 目前支持了
PicGo 本体支持如下图床:
- ~~`微博图床` v1.0~~ **微博图床从 2019 年 4 月开始进行防盗链,不建议继续使用**
- `七牛图床` v1.0
- `腾讯云 COS v4\v5版本` v1.1 & v1.5.0
- `腾讯云 COS v4\v5 版本` v1.1 & v1.5.0
- `又拍云` v1.2.0
- `GitHub` v1.5.0
- `SM.MS` v1.5.1
- `SM.MS V2` v2.3.0-beta.0
- `阿里云 OSS` v1.6.0
- `Imgur` v1.6.0
**本体不再增加默认的图床支持。你可以自行开发第三方图床插件。详见 [PicGo-Core](https://picgo.github.io/PicGo-Core-Doc/)**
第三方插件以及使用了 PicGo 底层的应用可以在 [Awesome-PicGo](https://github.com/PicGo/Awesome-PicGo) 找到。欢迎贡献!
## 特色功能
PicGo 支持 macOS、Windows 64位(>= v1.3.1),Linux>= v1.6.0)。
- 支持拖拽图片上传
- 支持快捷键上传剪贴板里第一张图片
- Windows 和 macOS 支持右键图片文件通过菜单上传 (v2.1.0+)
- 上传图片后自动复制链接到剪贴板
- 支持自定义复制到剪贴板的链接格式
- 支持修改快捷键,默认快速上传快捷键:`command+shift+p`macOS| `control+shift+p`Windows\Linux)
- 支持插件系统,已有插件支持 Gitee、青云等第三方图床
- 更多第三方插件以及使用了 PicGo 底层的应用可以在 [Awesome-PicGo](https://github.com/PicGo/Awesome-PicGo) 找到。欢迎贡献!
- 支持通过发送 HTTP 请求调用 PicGo 上传(v2.2.0+)
- 更多功能等你自己去发现,同时也会不断开发新功能
- 开发进度可以查看 [Projects](https://github.com/Molunerfinn/PicGo/projects),会同步更新开发进度
- 欢迎加入 [官方讨论区](https://github.com/Molunerfinn/PicGo/discussions) 与我交流
支持快捷键`command+shift+p`macOS)或者`control+shift+p`Windows\Linux)用以支持快捷上传剪贴板里的图片(第一张)。
PicGo 支持自定义快捷键,使用方法见[配置手册](https://picgo.github.io/PicGo-Doc/zh/guide/config.html)。
开发进度可以查看 [Projects](https://github.com/Molunerfinn/PicGo/projects),会同步更新开发进度。
**如果第一次使用,请参考应用使用[快速上手](https://picgo.github.io/PicGo-Doc/zh/guide/getting-started.html)。遇到问题了还可以看看 [FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md) 以及被关闭的 [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed)。**
**如果第一次使用,请参考应用 [使用文档](https://picgo.github.io/PicGo-Doc/zh/guide/getting-started.html)。遇到问题了还可以看看 [FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md) 以及被关闭的 [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed)。**
## 下载安装
点击此处下载[应用](https://github.com/Molunerfinn/PicGo/releases)。
点击此处下载 [应用](https://github.com/Molunerfinn/PicGo/releases)。
macOS 用户请下载最新版本的 `dmg` 文件,Windows 用户请下载最新版本的 `exe` 文件,Linux用户请下载 `AppImage` 文件。
### Windows
**如果你是 Arch 类的 Linux 用户,可以直接通过 `aurman -S picgo-appimage` 来安装 PicGo。感谢 @houbaron 的贡献!**
Windows 用户请下载最新版本的 `exe` 文件。
**如果你是 macOS 用户,可以使用 `brew cask` 来安装 PicGo: `brew cask install picgo`。感谢 @womeimingzi11 的贡献!**
**还可以使用 [Scoop](https://scoop.sh/) 来安装 PicGo: `scoop bucket add helbing https://github.com/helbing/scoop-bucket` & `scoop install picgo`。 感谢 @helbing 的贡献!**
**如果你是 Windows 用户,还可以使用 [Scoop](https://scoop.sh/) 来安装 PicGo: `scoop bucket add helbing https://github.com/helbing/scoop-bucket` & `scoop install picgo`。 感谢 @helbing 的贡献!**
**还可以使用 [Chocolatey](https://chocolatey.org/) 来安装 PicGo: `choco install picgo`。 感谢 @iYato 的贡献!**
### macOS
macOS 用户请下载最新版本的 `dmg` 文件。
**还可以使用 [Homebrew](https://brew.sh/) 来安装 PicGo: `brew install picgo --cask`。感谢 @womeimingzi11 的贡献!**
### Linux
Linux 用户请下载 `AppImage` 文件。
**如果你是 Arch 类的 Linux 用户,可以直接通过 `yay -S picgo-appimage` 来安装 PicGo (若是没有则先安装 yay ,是 aur 仓库助手)。感谢 @houbaron 的贡献!**
## 应用截图
@@ -73,9 +93,9 @@ macOS 用户请下载最新版本的 `dmg` 文件,Windows 用户请下载最
> 如果想学习 Electron-vue 的开发,可以查看我写的系列教程——[Electron-vue 开发实战](https://molunerfinn.com/tags/Electron-vue/)
1. 你需要有 Node、Git环境,了解 npm 的相关知识。
2. `git clone https://github.com/Molunerfinn/PicGo.git` 并进入项目
3. `npm install` 下载依赖
1. 你需要有 Node、Git 环境,了解 npm 的相关知识。
2. `git clone https://github.com/Molunerfinn/PicGo.git` 并进入项目
3. `yarn` 下载依赖。注意如果你没有 `yarn`,请去 [官网](https://classic.yarnpkg.com/en/docs/install) 下载安装后再使用。 **用 `npm install` 将导致未知错误!**
4. Mac 需要有 Xcode 环境,Windows 需要有 VS 环境。
### 开发模式
@@ -84,26 +104,29 @@ macOS 用户请下载最新版本的 `dmg` 文件,Windows 用户请下载最
```bash
ctrl+c # 退出开发模式
npm run dev # 重新进入开发模式
npm run electron:serve # 重新进入开发模式
```
**注:Windows 开发模式运行之后会在底部任务栏的右下角应用区出现 PicGo 的应用图标。**
### 生产模式
如果你需要自行构建,可以 `npm run electron:build` 开始进行构建。构建成功后,会在 `dist_electron` 目录里出现构建成功的相应安装文件。
**注意**:如果你的网络环境不太好,可能会出现 `electron-builder` 下载 `electron` 二进制文件失败的情况。这个时候需要在 `npm run build` 之前指定一下 `electron` 的源为国内源:
**注意**:如果你的网络环境不太好,可能会出现 `electron-builder` 下载 `electron` 二进制文件失败的情况。这个时候需要在 `npm run electron:build` 之前指定一下 `electron` 的源为国内源:
```bash
export ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/"
# 在 Windows 上,则可以使用 set ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/ (无需引号)
npm run build
npm run electron:build
```
只需第一次构建的时候指定一下国内源即可。后续构建不需要特地指定。二进制文件下载在 `~/.electron/` 目录下。如果想要更新 `electron` 构建版本,可以删除 `~/.electron/` 目录,然后重新运行上一步,让 `electron-builder `去下载最新的 `electron` 二进制文件。
## 其他相关
- [vs-picgo](https://github.com/Spades-S/vs-picgo)PicGo 的 VS Code 版。
- [vs-picgo](https://github.com/PicGo/vs-picgo)PicGo 的 VS Code 版。
- [flutter-picgo](https://github.com/PicGo/flutter-picgo)PicGo 的手机版(支持 Android 和 iOS )。
## 赞助
@@ -121,4 +144,4 @@ npm run build
[MIT](http://opensource.org/licenses/MIT)
Copyright (c) 2017 - 2019 Molunerfinn
Copyright (c) 2017 - 2019 Molunerfinn
+5 -4
View File
@@ -1,10 +1,10 @@
{
"name": "picgo",
"version": "2.2.2",
"version": "2.3.0-beta.5",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"dev": "vue-cli-service electron:serve",
"build": "vue-cli-service electron:build",
"lint": "vue-cli-service lint",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
@@ -42,7 +42,8 @@
"keycode": "^2.2.0",
"lodash-id": "^0.14.0",
"lowdb": "^1.0.0",
"picgo": "^1.4.4",
"picgo": "^1.4.19",
"qrcode.vue": "^1.7.0",
"vue": "^2.6.10",
"vue-gallery": "^2.0.1",
"vue-lazyload": "^1.2.6",
+1
View File
@@ -27,6 +27,7 @@ function main {
$stream.Dispose() | out-null
$imagePath
Exit 1
}
try {
+2 -154
View File
@@ -1,158 +1,6 @@
import {
app,
globalShortcut,
protocol
} from 'electron'
import {
createProtocol,
installVueDevtools
} from 'vue-cli-plugin-electron-builder/lib'
import db from '#/datastore'
import { IWindowList } from '~/main/apis/window/constants'
import windowManager from '~/main/apis/window/windowManager'
import {
uploadChoosedFiles,
uploadClipboardFiles
} from '~/main/apis/uploader/api'
import beforeOpen from '~/main/utils/beforeOpen'
import updateChecker from '~/main/utils/updateChecker'
import ipcList from '~/main/apis/eventCenter/ipcList'
import busEventList from '~/main/apis/eventCenter/busEventList'
import fixPath from 'fix-path'
import { getUploadFiles } from '~/main/utils/handleArgv'
import bus from '~/main/utils/eventBus'
import {
updateShortKeyFromVersion212
} from '~/main/migrate'
import shortKeyHandler from '~/main/apis/shortKey/shortKeyHandler'
import server from '~/main/server/index'
import {
createTray
} from '~/main/apis/app'
import { bootstrap } from '~/main/lifeCycle'
const isDevelopment = process.env.NODE_ENV !== 'production'
protocol.registerSchemesAsPrivileged([{ scheme: 'picgo', privileges: { secure: true, standard: true } }])
beforeOpen()
// fix the $PATH in macOS
fixPath()
ipcList.listen()
busEventList.listen()
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
let files = getUploadFiles(commandLine, workingDirectory)
if (files === null || files.length > 0) { // 如果有文件列表作为参数,说明是命令行启动
if (files === null) {
uploadClipboardFiles()
} else {
const win = windowManager.getAvailableWindow()
uploadChoosedFiles(win.webContents, files)
}
} else {
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
if (settingWindow.isMinimized()) {
settingWindow.restore()
}
settingWindow.focus()
}
}
})
}
if (process.platform === 'win32') {
app.setAppUserModelId('com.molunerfinn.picgo')
}
if (process.env.XDG_CURRENT_DESKTOP && process.env.XDG_CURRENT_DESKTOP.includes('Unity')) {
process.env.XDG_CURRENT_DESKTOP = 'Unity'
}
app.on('ready', async () => {
createProtocol('picgo')
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
try {
await installVueDevtools()
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString())
}
}
windowManager.create(IWindowList.TRAY_WINDOW)
windowManager.create(IWindowList.SETTING_WINDOW)
if (process.platform === 'darwin' || process.platform === 'win32') {
createTray()
}
db.set('needReload', false)
updateChecker()
// 不需要阻塞
process.nextTick(() => {
updateShortKeyFromVersion212(db, db.get('settings.shortKey'))
shortKeyHandler.init()
})
server.startup()
if (process.env.NODE_ENV !== 'development') {
let files = getUploadFiles()
if (files === null || files.length > 0) { // 如果有文件列表作为参数,说明是命令行启动
if (files === null) {
uploadClipboardFiles()
} else {
const win = windowManager.getAvailableWindow()
uploadChoosedFiles(win.webContents, files)
}
}
}
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
createProtocol('picgo')
if (!windowManager.has(IWindowList.TRAY_WINDOW)) {
windowManager.create(IWindowList.TRAY_WINDOW)
}
if (!windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.create(IWindowList.SETTING_WINDOW)
}
})
app.on('will-quit', () => {
globalShortcut.unregisterAll()
bus.removeAllListeners()
server.shutdown()
})
app.setLoginItemSettings({
openAtLogin: db.get('settings.autoStart') || false
})
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', data => {
if (data === 'graceful-exit') {
app.quit()
server.shutdown()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
server.shutdown()
})
}
}
bootstrap.launchApp()
/**
* Auto Updater
+2
View File
@@ -8,6 +8,7 @@ import 'element-ui/lib/theme-chalk/index.css'
import VueLazyLoad from 'vue-lazyload'
import axios from 'axios'
import mainMixin from './renderer/utils/mainMixin'
import bus from '@/utils/bus'
webFrame.setVisualZoomLevelLimits(1, 1)
webFrame.setLayoutZoomLevelLimits(0, 0)
@@ -25,6 +26,7 @@ Vue.prototype.$builtInPicBed = [
]
Vue.prototype.$db = db
Vue.prototype.$http = axios
Vue.prototype.$bus = bus
Vue.use(ElementUI)
Vue.use(VueLazyLoad)
+13
View File
@@ -0,0 +1,13 @@
# apis folder
## core
The lowest level APIs that are not dependent on each other. The upper APIs depend on them.
## app
Provide key API interfaces for PicGo application, including uploader, window management, shortcut key system, etc
## gui
GuiApi for PicGo plugins.
-206
View File
@@ -1,206 +0,0 @@
import {
app,
Menu,
Tray,
dialog,
clipboard,
systemPreferences,
Notification
} from 'electron'
import uploader from '~/main/apis/uploader'
import getPicBeds from '~/main/utils/getPicBeds'
import db from '#/datastore'
import windowManager from '~/main/apis/window/windowManager'
import { IWindowList } from '~/main/apis/window/constants'
import picgo from '~/main/apis/picgo'
import pasteTemplate from '#/utils/pasteTemplate'
import pkg from 'root/package.json'
let contextMenu: Menu | null
let menu: Menu | null
let tray: Tray | null
export function createContextMenu () {
const picBeds = getPicBeds()
const submenu = picBeds.filter(item => item.visible).map(item => {
return {
label: item.name,
type: 'radio',
checked: db.get('picBed.current') === item.type,
click () {
picgo.saveConfig({
'picBed.current': item.type
})
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')
}
}
}
})
contextMenu = Menu.buildFromTemplate([
{
label: '关于',
click () {
dialog.showMessageBox({
title: 'PicGo',
message: 'PicGo',
detail: `Version: ${pkg.version}\nAuthor: Molunerfinn\nGithub: https://github.com/Molunerfinn/PicGo`
})
}
},
{
label: '打开详细窗口',
click () {
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)
settingWindow!.show()
settingWindow!.focus()
if (windowManager.has(IWindowList.MINI_WINDOW)) {
windowManager.get(IWindowList.MINI_WINDOW)!.hide()
}
}
},
{
label: '选择默认图床',
type: 'submenu',
// @ts-ignore
submenu
},
// @ts-ignore
{
label: '打开更新助手',
type: 'checkbox',
checked: db.get('settings.showUpdateTip'),
click () {
const value = db.get('settings.showUpdateTip')
db.set('settings.showUpdateTip', !value)
}
},
{
label: '重启应用',
click () {
app.relaunch()
app.exit(0)
}
},
// @ts-ignore
{
role: 'quit',
label: '退出'
}
])
}
export function createTray () {
const menubarPic = process.platform === 'darwin' ? `${__static}/menubar.png` : `${__static}/menubar-nodarwin.png`
tray = new Tray(menubarPic)
tray.on('right-click', () => {
if (windowManager.has(IWindowList.TRAY_WINDOW)) {
windowManager.get(IWindowList.TRAY_WINDOW)!.hide()
}
createContextMenu()
tray!.popUpContextMenu(contextMenu!)
})
tray.on('click', (event, bounds) => {
if (process.platform === 'darwin') {
toggleWindow(bounds)
setTimeout(() => {
let img = clipboard.readImage()
let obj: ImgInfo[] = []
if (!img.isEmpty()) {
// 从剪贴板来的图片默认转为png
// @ts-ignore
const imgUrl = 'data:image/png;base64,' + Buffer.from(img.toPNG(), 'binary').toString('base64')
obj.push({
width: img.getSize().width,
height: img.getSize().height,
imgUrl
})
}
windowManager.get(IWindowList.TRAY_WINDOW)!.webContents.send('clipboardFiles', obj)
}, 0)
} else {
if (windowManager.has(IWindowList.TRAY_WINDOW)) {
windowManager.get(IWindowList.TRAY_WINDOW)!.hide()
}
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)
settingWindow!.show()
settingWindow!.focus()
if (windowManager.has(IWindowList.MINI_WINDOW)) {
windowManager.get(IWindowList.MINI_WINDOW)!.hide()
}
}
})
tray.on('drag-enter', () => {
if (systemPreferences.isDarkMode()) {
tray!.setImage(`${__static}/upload-dark.png`)
} else {
tray!.setImage(`${__static}/upload.png`)
}
})
tray.on('drag-end', () => {
tray!.setImage(`${__static}/menubar.png`)
})
tray.on('drop-files', async (event: Event, files: string[]) => {
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!
const imgs = await uploader
.setWebContents(trayWindow.webContents)
.upload(files)
if (imgs !== false) {
for (let i = 0; i < imgs.length; i++) {
clipboard.writeText(pasteTemplate(pasteStyle, imgs[i]))
const notification = new Notification({
title: '上传成功',
body: imgs[i].imgUrl!,
icon: files[i]
})
setTimeout(() => {
notification.show()
}, i * 100)
db.insert('uploaded', imgs[i])
}
trayWindow.webContents.send('dragFiles', imgs)
}
})
// toggleWindow()
}
export function createMenu () {
if (process.env.NODE_ENV !== 'development') {
const template = [{
label: 'Edit',
submenu: [
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },
{ label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' },
{ type: 'separator' },
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' },
{
label: 'Quit',
accelerator: 'CmdOrCtrl+Q',
click () {
app.quit()
}
}
]
}]
// @ts-ignore
menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
}
const toggleWindow = (bounds: IBounds) => {
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!
if (trayWindow.isVisible()) {
trayWindow.hide()
} else {
trayWindow.setPosition(bounds.x - 98 + 11, bounds.y, false)
trayWindow.webContents.send('updateFiles')
trayWindow.show()
trayWindow.focus()
}
}
@@ -1,21 +1,18 @@
import bus from '../../utils/eventBus'
import PicGoCore from '~/universal/types/picgo'
import path from 'path'
import bus from '@core/bus'
import {
app,
globalShortcut,
BrowserWindow
globalShortcut
} from 'electron'
import logger from '../../utils/logger'
import GuiApi from '../gui'
import logger from '@core/picgo/logger'
import GuiApi from '../../gui'
import db from '#/datastore'
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
import shortKeyService from './shortKeyService'
import picgo from '../picgo'
import picgo from '@core/picgo'
class ShortKeyHandler {
private isInModifiedMode: boolean = false
constructor () {
bus.on('toggleShortKeyModifiedMode', flag => {
bus.on(TOGGLE_SHORTKEY_MODIFIED_MODE, flag => {
this.isInModifiedMode = flag
})
}
@@ -27,14 +24,18 @@ class ShortKeyHandler {
const commands = db.get('settings.shortKey') as IShortKeyConfigs
Object.keys(commands)
.filter(item => item.includes('picgo:'))
.map(command => {
.forEach(command => {
const config = commands[command]
globalShortcut.register(config.key, () => {
this.handler(command)
})
// if disabled, don't register #534
if (config.enable) {
globalShortcut.register(config.key, () => {
this.handler(command)
})
}
})
}
private initPluginsShortKey () {
// get enabled plugin
const pluginList = picgo.pluginLoader.getList()
for (let item of pluginList) {
const plugin = picgo.pluginLoader.getPlugin(item)
@@ -49,7 +50,10 @@ class ShortKeyHandler {
const command = `${item}:${cmd.name}`
if (db.has(`settings.shortKey[${command}]`)) {
const commandConfig = db.get(`settings.shortKey.${command}`) as IShortKeyConfig
this.registerShortKey(commandConfig, command, cmd.handle, false)
// if disabled, don't register #534
if (commandConfig.enable) {
this.registerShortKey(commandConfig, command, cmd.handle, false)
}
} else {
this.registerShortKey(cmd, command, cmd.handle, true)
}
@@ -68,6 +72,8 @@ class ShortKeyHandler {
} else {
logger.warn(`${command} do not provide a key to bind`)
}
// if the configfile already had this command
// then writeFlag -> false
if (writeFlag) {
picgo.saveConfig({
[`settings.shortKey.${command}`]: {
@@ -1,4 +1,4 @@
import logger from '../../utils/logger'
import logger from '@core/picgo/logger'
class ShortKeyService {
private commandList: Map<string, IShortKeyHandler> = new Map()
registerCommand (command: string, handler: IShortKeyHandler) {
+267
View File
@@ -0,0 +1,267 @@
import {
app,
Menu,
Tray,
dialog,
clipboard,
systemPreferences,
Notification
} from 'electron'
import uploader from 'apis/app/uploader'
import getPicBeds from '~/main/utils/getPicBeds'
import db from '#/datastore'
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from 'apis/app/window/constants'
import picgo from '@core/picgo'
import pasteTemplate from '#/utils/pasteTemplate'
import pkg from 'root/package.json'
import { handleCopyUrl } from '~/main/utils/common'
let contextMenu: Menu | null
let menu: Menu | null
let tray: Tray | null
export function createContextMenu () {
const picBeds = getPicBeds()
if (process.platform === 'darwin' || process.platform === 'win32') {
const submenu = picBeds.filter(item => item.visible).map(item => {
return {
label: item.name,
type: 'radio',
checked: db.get('picBed.current') === item.type,
click () {
picgo.saveConfig({
'picBed.current': item.type,
'picBed.uploader': item.type
})
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')
}
}
}
})
contextMenu = Menu.buildFromTemplate([
{
label: '关于',
click () {
dialog.showMessageBox({
title: 'PicGo',
message: 'PicGo',
detail: `Version: ${pkg.version}\nAuthor: Molunerfinn\nGithub: https://github.com/Molunerfinn/PicGo`
})
}
},
{
label: '打开详细窗口',
click () {
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)
settingWindow!.show()
settingWindow!.focus()
if (windowManager.has(IWindowList.MINI_WINDOW)) {
windowManager.get(IWindowList.MINI_WINDOW)!.hide()
}
}
},
{
label: '选择默认图床',
type: 'submenu',
// @ts-ignore
submenu
},
// @ts-ignore
{
label: '打开更新助手',
type: 'checkbox',
checked: db.get('settings.showUpdateTip'),
click () {
const value = db.get('settings.showUpdateTip')
db.set('settings.showUpdateTip', !value)
}
},
{
label: '重启应用',
click () {
app.relaunch()
app.exit(0)
}
},
// @ts-ignore
{
role: 'quit',
label: '退出'
}
])
} else if (process.platform === 'linux') {
// TODO 图床选择功能
// 由于在Linux难以像在Mac和Windows上那样在点击时构造ContextMenu
// 暂时取消这个选单,避免引起和设置中启用的图床不一致
// TODO 重启应用功能
// 目前的实现无法正常工作
contextMenu = Menu.buildFromTemplate([
{
label: '打开详细窗口',
click () {
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)
settingWindow!.show()
settingWindow!.focus()
if (windowManager.has(IWindowList.MINI_WINDOW)) {
windowManager.get(IWindowList.MINI_WINDOW)!.hide()
}
}
},
// @ts-ignore
{
label: '打开更新助手',
type: 'checkbox',
checked: db.get('settings.showUpdateTip'),
click () {
const value = db.get('settings.showUpdateTip')
db.set('settings.showUpdateTip', !value)
}
},
{
label: '关于应用',
click () {
dialog.showMessageBox({
title: 'PicGo',
message: 'PicGo',
buttons: ['Ok'],
detail: `Version: ${pkg.version}\nAuthor: Molunerfinn\nGithub: https://github.com/Molunerfinn/PicGo`
})
}
},
// @ts-ignore
{
role: 'quit',
label: '退出'
}
])
}
}
export function createTray () {
const menubarPic = process.platform === 'darwin' ? `${__static}/menubar.png` : `${__static}/menubar-nodarwin.png`
tray = new Tray(menubarPic)
// click事件在Mac和Windows上可以触发(在Ubuntu上无法触发,Unity不支持)
if (process.platform === 'darwin' || process.platform === 'win32') {
tray.on('right-click', () => {
if (windowManager.has(IWindowList.TRAY_WINDOW)) {
windowManager.get(IWindowList.TRAY_WINDOW)!.hide()
}
createContextMenu()
tray!.popUpContextMenu(contextMenu!)
})
tray.on('click', (event, bounds) => {
if (process.platform === 'darwin') {
toggleWindow(bounds)
setTimeout(() => {
let img = clipboard.readImage()
let obj: ImgInfo[] = []
if (!img.isEmpty()) {
// 从剪贴板来的图片默认转为png
// @ts-ignore
const imgUrl = 'data:image/png;base64,' + Buffer.from(img.toPNG(), 'binary').toString('base64')
obj.push({
width: img.getSize().width,
height: img.getSize().height,
imgUrl
})
}
windowManager.get(IWindowList.TRAY_WINDOW)!.webContents.send('clipboardFiles', obj)
}, 0)
} else {
if (windowManager.has(IWindowList.TRAY_WINDOW)) {
windowManager.get(IWindowList.TRAY_WINDOW)!.hide()
}
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)
settingWindow!.show()
settingWindow!.focus()
if (windowManager.has(IWindowList.MINI_WINDOW)) {
windowManager.get(IWindowList.MINI_WINDOW)!.hide()
}
}
})
tray.on('drag-enter', () => {
if (systemPreferences.isDarkMode()) {
tray!.setImage(`${__static}/upload-dark.png`)
} else {
tray!.setImage(`${__static}/upload.png`)
}
})
tray.on('drag-end', () => {
tray!.setImage(`${__static}/menubar.png`)
})
tray.on('drop-files', async (event: Event, files: string[]) => {
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!
const imgs = await uploader
.setWebContents(trayWindow.webContents)
.upload(files)
if (imgs !== false) {
const pasteText: string[] = []
for (let i = 0; i < imgs.length; i++) {
pasteText.push(pasteTemplate(pasteStyle, imgs[i]))
const notification = new Notification({
title: '上传成功',
body: imgs[i].imgUrl!,
icon: files[i]
})
setTimeout(() => {
notification.show()
}, i * 100)
db.insert('uploaded', imgs[i])
}
handleCopyUrl(pasteText.join('\n'))
trayWindow.webContents.send('dragFiles', imgs)
}
})
// toggleWindow()
} else if (process.platform === 'linux') {
// click事件在Ubuntu上无法触发,Unity不支持(在Mac和Windows上可以触发)
// 需要使用 setContextMenu 设置菜单
createContextMenu()
tray!.setContextMenu(contextMenu)
}
}
export function createMenu () {
if (process.env.NODE_ENV !== 'development') {
const template = [{
label: 'Edit',
submenu: [
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },
{ label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' },
{ type: 'separator' },
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' },
{
label: 'Quit',
accelerator: 'CmdOrCtrl+Q',
click () {
app.quit()
}
}
]
}]
// @ts-ignore
menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
}
const toggleWindow = (bounds: IBounds) => {
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!
if (trayWindow.isVisible()) {
trayWindow.hide()
} else {
trayWindow.setPosition(bounds.x - 98 + 11, bounds.y, false)
trayWindow.webContents.send('updateFiles')
trayWindow.show()
trayWindow.focus()
}
}
@@ -1,13 +1,14 @@
import {
clipboard,
Notification,
WebContents
} from 'electron'
import windowManager from '~/main/apis/window/windowManager'
import { IWindowList } from '~/main/apis/window/constants'
import uploader from './'
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from 'apis/app/window/constants'
import uploader from '.'
import pasteTemplate from '#/utils/pasteTemplate'
import db from '#/datastore'
import { handleCopyUrl } from '~/main/utils/common'
import { handleUrlEncode } from '#/utils/common'
export const uploadClipboardFiles = async (): Promise<string> => {
const win = windowManager.getAvailableWindow()
let img = await uploader.setWebContents(win!.webContents).upload()
@@ -15,7 +16,7 @@ export const uploadClipboardFiles = async (): Promise<string> => {
if (img.length > 0) {
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
clipboard.writeText(pasteTemplate(pasteStyle, img[0]))
handleCopyUrl(pasteTemplate(pasteStyle, img[0]))
const notification = new Notification({
title: '上传成功',
body: img[0].imgUrl!,
@@ -28,7 +29,7 @@ export const uploadClipboardFiles = async (): Promise<string> => {
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('updateGallery')
}
return img[0].imgUrl as string
return handleUrlEncode(img[0].imgUrl as string)
} else {
const notification = new Notification({
title: '上传不成功',
@@ -48,9 +49,9 @@ export const uploadChoosedFiles = async (webContents: WebContents, files: IFileW
const result = []
if (imgs !== false) {
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
let pasteText = ''
const pasteText: string[] = []
for (let i = 0; i < imgs.length; i++) {
pasteText += pasteTemplate(pasteStyle, imgs[i]) + '\r\n'
pasteText.push(pasteTemplate(pasteStyle, imgs[i]))
const notification = new Notification({
title: '上传成功',
body: imgs[i].imgUrl!,
@@ -60,9 +61,9 @@ export const uploadChoosedFiles = async (webContents: WebContents, files: IFileW
notification.show()
}, i * 100)
db.insert('uploaded', imgs[i])
result.push(imgs[i].imgUrl!)
result.push(handleUrlEncode(imgs[i].imgUrl!))
}
clipboard.writeText(pasteText)
handleCopyUrl(pasteText.join('\n'))
windowManager.get(IWindowList.TRAY_WINDOW)!.webContents.send('uploadFiles', imgs)
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('updateGallery')
+138
View File
@@ -0,0 +1,138 @@
import {
Notification,
BrowserWindow,
ipcMain,
WebContents
} from 'electron'
import dayjs from 'dayjs'
import picgo from '@core/picgo'
import db from '#/datastore'
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from 'apis/app/window/constants'
import util from 'util'
import { IPicGo } from 'picgo/dist/src/types'
import { showNotification } from '~/main/utils/common'
const waitForShow = (webcontent: WebContents) => {
return new Promise<void>((resolve, reject) => {
webcontent.on('did-finish-load', () => {
resolve()
})
})
}
const waitForRename = (window: BrowserWindow, id: number): Promise<string|null> => {
return new Promise((resolve, reject) => {
const windowId = window.id
ipcMain.once(`rename${id}`, (evt: Event, newName: string) => {
resolve(newName)
window.close()
})
window.on('close', () => {
resolve(null)
ipcMain.removeAllListeners(`rename${id}`)
windowManager.deleteById(windowId)
})
})
}
class Uploader {
private webContents: WebContents | null = null
private uploading: boolean = false
constructor () {
this.init()
}
init () {
picgo.on('notification', message => {
const notification = new Notification(message)
notification.show()
})
picgo.on('uploadProgress', progress => {
this.webContents?.send('uploadProgress', progress)
})
picgo.on('beforeTransform', ctx => {
if (db.get('settings.uploadNotification')) {
const notification = new Notification({
title: '上传进度',
body: '正在上传'
})
notification.show()
}
})
picgo.helper.beforeUploadPlugins.register('renameFn', {
handle: async (ctx: IPicGo) => {
const rename = db.get('settings.rename')
const autoRename = db.get('settings.autoRename')
if (autoRename || rename) {
await Promise.all(ctx.output.map(async (item, index) => {
let name: undefined | string | null
let fileName: string | undefined
if (autoRename) {
fileName = dayjs().add(index, 'second').format('YYYYMMDDHHmmss') + item.extname
} else {
fileName = item.fileName
}
if (rename) {
const window = windowManager.create(IWindowList.RENAME_WINDOW)!
await waitForShow(window.webContents)
window.webContents.send('rename', fileName, window.webContents.id)
name = await waitForRename(window, window.webContents.id)
}
item.fileName = name || fileName
}))
}
}
})
}
setWebContents (webContents: WebContents) {
this.webContents = webContents
return this
}
upload (img?: IUploadOption): Promise<ImgInfo[]|false> {
if (this.uploading) {
showNotification({
title: '上传失败',
body: '前序上传还在继续,请稍后再试'
})
return Promise.resolve(false)
}
return new Promise((resolve) => {
try {
this.uploading = true
picgo.upload(img)
picgo.once('finished', ctx => {
this.uploading = false
if (ctx.output.every((item: ImgInfo) => item.imgUrl)) {
resolve(ctx.output)
} else {
resolve(false)
}
picgo.removeAllListeners('failed')
})
picgo.once('failed', (e: Error) => {
this.uploading = false
setTimeout(() => {
showNotification({
title: '上传失败',
body: util.format(e.stack),
clickToCopy: true
})
}, 500)
picgo.removeAllListeners('finished')
resolve(false)
})
} catch (e) {
this.uploading = false
picgo.removeAllListeners('failed')
picgo.removeAllListeners('finished')
resolve([])
}
})
}
}
export default new Uploader()
@@ -12,8 +12,8 @@ export const TRAY_WINDOW_URL = isDevelopment
: `picgo://./index.html`
export const SETTING_WINDOW_URL = isDevelopment
? `${(process.env.WEBPACK_DEV_SERVER_URL as string)}#setting/upload`
: `picgo://./index.html#setting/upload`
? `${(process.env.WEBPACK_DEV_SERVER_URL as string)}#main-page/upload`
: `picgo://./index.html#main-page/upload`
export const MINI_WINDOW_URL = isDevelopment
? `${(process.env.WEBPACK_DEV_SERVER_URL as string)}#mini-page`
@@ -22,6 +22,3 @@ export const MINI_WINDOW_URL = isDevelopment
export const RENAME_WINDOW_URL = process.env.NODE_ENV === 'development'
? `${(process.env.WEBPACK_DEV_SERVER_URL as string)}#rename-page`
: `picgo://./index.html#rename-page`
export const DELETE_WINDOW_EVENT = 'DELETE_WINDOW_EVENT'
export const CREATE_WINDOW_EVENT = 'CREATE_WINDOW_EVENT'
@@ -1,18 +1,16 @@
import {
IWindowList,
DELETE_WINDOW_EVENT,
CREATE_WINDOW_EVENT,
SETTING_WINDOW_URL,
TRAY_WINDOW_URL,
MINI_WINDOW_URL,
RENAME_WINDOW_URL
} from './constants'
import { IWindowListItem } from '#/types/electron'
import bus from '~/main/utils/eventBus'
import bus from '@core/bus'
import { CREATE_APP_MENU } from '@core/bus/constants'
import db from '#/datastore'
import { getWindowId } from '~/main/apis/bus'
import { BrowserWindow, app } from 'electron'
import { CREATE_APP_MENU } from '~/main/apis/bus/constants'
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
import { app } from 'electron'
const windowList = new Map<IWindowList, IWindowListItem>()
@@ -37,14 +35,9 @@ windowList.set(IWindowList.TRAY_WINDOW, {
}
},
callback (window) {
const id = window!.id
window!.loadURL(TRAY_WINDOW_URL)
window!.on('closed', () => {
bus.emit(DELETE_WINDOW_EVENT, id)
})
window!.on('blur', () => {
window!.hide()
window.loadURL(TRAY_WINDOW_URL)
window.on('blur', () => {
window.hide()
})
}
})
@@ -81,12 +74,10 @@ windowList.set(IWindowList.SETTING_WINDOW, {
}
return options
},
callback (window) {
const id = window!.id
window!.loadURL(SETTING_WINDOW_URL)
window!.on('closed', () => {
bus.emit('toggleShortKeyModifiedMode', false)
bus.emit(DELETE_WINDOW_EVENT, id)
callback (window, windowManager) {
window.loadURL(SETTING_WINDOW_URL)
window.on('closed', () => {
bus.emit(TOGGLE_SHORTKEY_MODIFIED_MODE, false)
if (process.platform === 'linux') {
process.nextTick(() => {
app.quit()
@@ -94,7 +85,7 @@ windowList.set(IWindowList.SETTING_WINDOW, {
}
})
bus.emit(CREATE_APP_MENU)
bus.emit(CREATE_WINDOW_EVENT, IWindowList.MINI_WINDOW)
windowManager.create(IWindowList.MINI_WINDOW)
}
})
@@ -125,11 +116,7 @@ windowList.set(IWindowList.MINI_WINDOW, {
return obj
},
callback (window) {
const id = window!.id
window!.loadURL(MINI_WINDOW_URL)
window!.on('closed', () => {
bus.emit(DELETE_WINDOW_EVENT, id)
})
window.loadURL(MINI_WINDOW_URL)
}
})
@@ -158,10 +145,9 @@ windowList.set(IWindowList.RENAME_WINDOW, {
}
return options
},
async callback (window) {
window!.loadURL(RENAME_WINDOW_URL)
const currentWindowId = await getWindowId()
const currentWindow = BrowserWindow.fromId(currentWindowId)
async callback (window, windowManager) {
window.loadURL(RENAME_WINDOW_URL)
const currentWindow = windowManager.getAvailableWindow()
if (currentWindow && currentWindow.isVisible()) {
// bounds: { x: 821, y: 75, width: 800, height: 450 }
const bounds = currentWindow.getBounds()
@@ -173,7 +159,7 @@ windowList.set(IWindowList.RENAME_WINDOW, {
} else { // if is the miniWindow
positionY = bounds.y + bounds.height / 2
}
window!.setPosition(positionX, positionY, false)
window.setPosition(positionX, positionY, false)
}
}
})
@@ -4,26 +4,20 @@ import {
import { IWindowManager, IWindowListItem } from '#/types/electron'
import windowList from './windowList'
import {
IWindowList,
DELETE_WINDOW_EVENT,
CREATE_WINDOW_EVENT
IWindowList
} from './constants'
import bus from '~/main/utils/eventBus'
class WindowManager implements IWindowManager {
private windowMap: Map<IWindowList | string, BrowserWindow> = new Map()
private windowIdMap: Map<number, IWindowList | string> = new Map()
constructor () {
bus.on(DELETE_WINDOW_EVENT, this.deleteById)
bus.on(CREATE_WINDOW_EVENT, this.create)
}
create = (name: IWindowList) => {
create (name: IWindowList) {
const windowConfig: IWindowListItem = windowList.get(name)!
if (windowConfig.isValid) {
if (!windowConfig.multiple) {
if (this.windowMap.has(name)) return this.windowMap.get(name)!
if (this.has(name)) return this.windowMap.get(name)!
}
const window = new BrowserWindow(windowConfig.options())
const id = window.id
if (windowConfig.multiple) {
this.windowMap.set(`${name}_${window.id}`, window)
this.windowIdMap.set(window.id, `${name}_${window.id}`)
@@ -31,14 +25,17 @@ class WindowManager implements IWindowManager {
this.windowMap.set(name, window)
this.windowIdMap.set(window.id, name)
}
windowConfig.callback(window)
windowConfig.callback(window, this)
window.on('close', () => {
this.deleteById(id)
})
return window
} else {
return null
}
}
get (name: IWindowList) {
if (this.windowMap.has(name)) {
if (this.has(name)) {
return this.windowMap.get(name)!
} else {
const window = this.create(name)
@@ -48,13 +45,14 @@ class WindowManager implements IWindowManager {
has (name: IWindowList) {
return this.windowMap.has(name)
}
delete = (name: IWindowList) => {
const window = this.windowMap.get(name)
if (window) {
this.windowIdMap.delete(window.id)
this.windowMap.delete(name)
}
}
// useless
// delete (name: IWindowList) {
// const window = this.windowMap.get(name)
// if (window) {
// this.windowIdMap.delete(window.id)
// this.windowMap.delete(name)
// }
// }
deleteById = (id: number) => {
const name = this.windowIdMap.get(id)
if (name) {
@@ -1,4 +1,4 @@
import bus from '../../utils/eventBus'
import bus from '.'
import {
UPLOAD_WITH_FILES,
UPLOAD_WITH_FILES_RESPONSE,
@@ -16,4 +16,7 @@ picgo.saveConfig({
PICGO_ENV: 'GUI'
})
export default picgo as PicGoCore
// @ts-ignore
picgo.GUI_VERSION = global.PICGO_GUI_VERSION
export default picgo! as PicGoCore
+5
View File
@@ -0,0 +1,5 @@
import picgo from '.'
const logger = picgo.log
export default logger
-225
View File
@@ -1,225 +0,0 @@
import path from 'path'
import GuiApi from '../gui'
import {
dialog,
shell,
IpcMainEvent,
ipcMain,
app
} from 'electron'
import PicGoCore from '~/universal/types/picgo'
import { IPicGoHelperType } from '#/types/enum'
import shortKeyHandler from '../shortKey/shortKeyHandler'
import picgo from '~/main/apis/picgo'
// eslint-disable-next-line
const requireFunc = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require
// const PluginHandler = requireFunc('picgo/dist/lib/PluginHandler').default
const STORE_PATH = app.getPath('userData')
// const CONFIG_PATH = path.join(STORE_PATH, '/data.json')
type PicGoNotice = {
title: string,
body: string[]
}
interface GuiMenuItem {
label: string
handle: (arg0: PicGoCore, arg1: GuiApi) => Promise<void>
}
// get uploader or transformer config
const getConfig = (name: string, type: IPicGoHelperType, ctx: PicGoCore) => {
let config: any[] = []
if (name === '') {
return config
} else {
const handler = ctx.helper[type].get(name)
if (handler) {
if (handler.config) {
config = handler.config(ctx)
}
}
return config
}
}
const handleConfigWithFunction = (config: any[]) => {
for (let i in config) {
if (typeof config[i].default === 'function') {
config[i].default = config[i].default()
}
if (typeof config[i].choices === 'function') {
config[i].choices = config[i].choices()
}
}
return config
}
const handleGetPluginList = () => {
ipcMain.on('getPluginList', (event: IpcMainEvent) => {
const pluginList = picgo.pluginLoader.getList()
const list = []
for (let i in pluginList) {
const plugin = picgo.pluginLoader.getPlugin(pluginList[i])
const pluginPath = path.join(STORE_PATH, `/node_modules/${pluginList[i]}`)
const pluginPKG = requireFunc(path.join(pluginPath, 'package.json'))
const uploaderName = plugin.uploader || ''
const transformerName = plugin.transformer || ''
let menu = []
if (plugin.guiMenu) {
menu = plugin.guiMenu(picgo)
}
let gui = false
if (pluginPKG.keywords && pluginPKG.keywords.length > 0) {
if (pluginPKG.keywords.includes('picgo-gui-plugin')) {
gui = true
}
}
const obj: IPicGoPlugin = {
name: pluginList[i].replace(/picgo-plugin-/, ''),
author: pluginPKG.author.name || pluginPKG.author,
description: pluginPKG.description,
logo: 'file://' + path.join(pluginPath, 'logo.png').split(path.sep).join('/'),
version: pluginPKG.version,
gui,
config: {
plugin: {
name: pluginList[i].replace(/picgo-plugin-/, ''),
config: plugin.config ? handleConfigWithFunction(plugin.config(picgo)) : []
},
uploader: {
name: uploaderName,
config: handleConfigWithFunction(getConfig(uploaderName, IPicGoHelperType.uploader, picgo))
},
transformer: {
name: transformerName,
config: handleConfigWithFunction(getConfig(uploaderName, IPicGoHelperType.transformer, picgo))
}
},
enabled: picgo.getConfig(`picgoPlugins.${pluginList[i]}`),
homepage: pluginPKG.homepage ? pluginPKG.homepage : '',
guiMenu: menu,
ing: false
}
list.push(obj)
}
event.sender.send('pluginList', list)
picgo.cmd.program.removeAllListeners()
})
}
const handlePluginInstall = () => {
ipcMain.on('installPlugin', async (event: IpcMainEvent, msg: string) => {
picgo.once('installSuccess', (notice: PicGoNotice) => {
event.sender.send('installSuccess', notice.body[0].replace(/picgo-plugin-/, ''))
shortKeyHandler.registerPluginShortKey(notice.body[0])
picgo.removeAllListeners('installFailed')
})
picgo.once('installFailed', () => {
handleNPMError()
picgo.removeAllListeners('installSuccess')
})
await picgo.pluginHandler.install([msg])
picgo.cmd.program.removeAllListeners()
})
}
const handlePluginUninstall = () => {
ipcMain.on('uninstallPlugin', async (event: IpcMainEvent, msg: string) => {
picgo.once('uninstallSuccess', (notice: PicGoNotice) => {
event.sender.send('uninstallSuccess', notice.body[0].replace(/picgo-plugin-/, ''))
shortKeyHandler.unregisterPluginShortKey(notice.body[0])
picgo.removeAllListeners('uninstallFailed')
})
picgo.once('uninstallFailed', () => {
handleNPMError()
picgo.removeAllListeners('uninstallSuccess')
})
await picgo.pluginHandler.uninstall([msg])
picgo.cmd.program.removeAllListeners()
})
}
const handlePluginUpdate = () => {
ipcMain.on('updatePlugin', async (event: IpcMainEvent, msg: string) => {
picgo.once('updateSuccess', (notice: { body: string[], title: string }) => {
event.sender.send('updateSuccess', notice.body[0].replace(/picgo-plugin-/, ''))
picgo.removeAllListeners('updateFailed')
})
picgo.once('updateFailed', () => {
handleNPMError()
picgo.removeAllListeners('updateSuccess')
})
await picgo.pluginHandler.update([msg])
picgo.cmd.program.removeAllListeners()
})
}
const handleNPMError = () => {
dialog.showMessageBox({
title: '发生错误',
message: '请安装Node.js并重启PicGo再继续操作',
buttons: ['Yes']
}).then((res) => {
if (res.response === 0) {
shell.openExternal('https://nodejs.org/')
}
})
}
const handleGetPicBedConfig = () => {
ipcMain.on('getPicBedConfig', (event: IpcMainEvent, type: string) => {
const name = picgo.helper.uploader.get(type).name || type
if (picgo.helper.uploader.get(type).config) {
const config = handleConfigWithFunction(picgo.helper.uploader.get(type).config(picgo))
event.sender.send('getPicBedConfig', config, name)
} else {
event.sender.send('getPicBedConfig', [], name)
}
picgo.cmd.program.removeAllListeners()
})
}
const handlePluginActions = () => {
ipcMain.on('pluginActions', (event: IpcMainEvent, name: string, label: string) => {
const plugin = picgo.pluginLoader.getPlugin(`picgo-plugin-${name}`)
const guiApi = new GuiApi()
if (plugin.guiMenu && plugin.guiMenu(picgo).length > 0) {
const menu: GuiMenuItem[] = plugin.guiMenu(picgo)
menu.forEach(item => {
if (item.label === label) {
item.handle(picgo, guiApi)
}
})
}
})
}
const handleRemoveFiles = () => {
ipcMain.on('removeFiles', (event: IpcMainEvent, files: ImgInfo[]) => {
const guiApi = new GuiApi()
setTimeout(() => {
picgo.emit('remove', files, guiApi)
}, 500)
})
}
const handlePicGoSaveData = () => {
ipcMain.on('picgoSaveData', (event: IpcMainEvent, data: IObj) => {
picgo.saveConfig(data)
})
}
export default {
listen () {
handleGetPluginList()
handlePluginInstall()
handlePluginUninstall()
handlePluginUpdate()
handleGetPicBedConfig()
handlePluginActions()
handleRemoveFiles()
handlePicGoSaveData()
}
}
+13 -9
View File
@@ -1,18 +1,22 @@
import {
dialog,
BrowserWindow,
clipboard,
Notification,
ipcMain
} from 'electron'
import db from '#/datastore'
import uploader from '../uploader'
import uploader from 'apis/app/uploader'
import pasteTemplate from '#/utils/pasteTemplate'
import { handleCopyUrl } from '~/main/utils/common'
import {
getWindowId,
getSettingWindowId
} from '~/main/apis/bus'
} from '@core/bus/apis'
import {
SHOW_INPUT_BOX
} from '~/universal/events/constants'
// Cross-process support may be required in the future
class GuiApi implements IGuiApi {
private windowId: number = -1
private settingWindowId: number = -1
@@ -23,7 +27,7 @@ class GuiApi implements IGuiApi {
return true
}
settingWindow.show()
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
resolve()
}, 1000) // TODO: a better way to wait page loaded.
@@ -40,9 +44,9 @@ class GuiApi implements IGuiApi {
}) {
await this.showSettingWindow()
this.getWebcontentsByWindowId(this.settingWindowId)
.send('showInputBox', options)
.send(SHOW_INPUT_BOX, options)
return new Promise<string>((resolve, reject) => {
ipcMain.once('showInputBox', (event: Event, value: string) => {
ipcMain.once(SHOW_INPUT_BOX, (event: Event, value: string) => {
resolve(value)
})
})
@@ -63,9 +67,9 @@ class GuiApi implements IGuiApi {
const imgs = await uploader.setWebContents(webContents).upload(input)
if (imgs !== false) {
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
let pasteText = ''
const pasteText: string[] = []
for (let i = 0; i < imgs.length; i++) {
pasteText += pasteTemplate(pasteStyle, imgs[i]) + '\r\n'
pasteText.push(pasteTemplate(pasteStyle, imgs[i]))
const notification = new Notification({
title: '上传成功',
body: imgs[i].imgUrl as string,
@@ -76,7 +80,7 @@ class GuiApi implements IGuiApi {
}, i * 100)
db.insert('uploaded', imgs[i])
}
clipboard.writeText(pasteText)
handleCopyUrl(pasteText.join('\n'))
webContents.send('uploadFiles', imgs)
webContents.send('updateGallery')
return imgs
-117
View File
@@ -1,117 +0,0 @@
import {
app,
Notification,
BrowserWindow,
ipcMain,
WebContents
} from 'electron'
import dayjs from 'dayjs'
import picgo from '~/main/apis/picgo'
import db from '#/datastore'
import windowManager from '~/main/apis/window/windowManager'
import { IWindowList } from '~/main/apis/window/constants'
const waitForShow = (webcontent: WebContents) => {
return new Promise((resolve, reject) => {
webcontent.on('did-finish-load', () => {
resolve()
})
})
}
const waitForRename = (window: BrowserWindow, id: number): Promise<string|null> => {
return new Promise((resolve, reject) => {
const windowId = window.id
ipcMain.once(`rename${id}`, (evt: Event, newName: string) => {
resolve(newName)
window.close()
})
window.on('close', () => {
resolve(null)
ipcMain.removeAllListeners(`rename${id}`)
windowManager.deleteById(windowId)
})
})
}
class Uploader {
private webContents: WebContents | null = null
constructor () {
this.init()
}
init () {
picgo.on('notification', message => {
const notification = new Notification(message)
notification.show()
})
picgo.on('uploadProgress', progress => {
this.webContents?.send('uploadProgress', progress)
})
picgo.on('beforeTransform', ctx => {
if (db.get('settings.uploadNotification')) {
const notification = new Notification({
title: '上传进度',
body: '正在上传'
})
notification.show()
}
})
picgo.helper.beforeUploadPlugins.register('renameFn', {
handle: async ctx => {
const rename = db.get('settings.rename')
const autoRename = db.get('settings.autoRename')
await Promise.all(ctx.output.map(async (item, index) => {
let name: undefined | string | null
let fileName: string | undefined
if (autoRename) {
fileName = dayjs().add(index, 'second').format('YYYYMMDDHHmmss') + item.extname
} else {
fileName = item.fileName
}
if (rename) {
const window = windowManager.create(IWindowList.RENAME_WINDOW)!
await waitForShow(window.webContents)
window.webContents.send('rename', fileName, window.webContents.id)
name = await waitForRename(window, window.webContents.id)
}
item.fileName = name || fileName
}))
}
})
}
setWebContents (webContents: WebContents) {
this.webContents = webContents
return this
}
upload (img?: IUploadOption): Promise<ImgInfo[]|false> {
picgo.upload(img)
return new Promise((resolve) => {
picgo.once('finished', ctx => {
if (ctx.output.every((item: ImgInfo) => item.imgUrl)) {
resolve(ctx.output)
} else {
resolve(false)
}
picgo.removeAllListeners('failed')
})
picgo.once('failed', ctx => {
setTimeout(() => {
const notification = new Notification({
title: '上传失败',
body: '请检查配置和上传的文件是否符合要求'
})
notification.show()
}, 500)
picgo.removeAllListeners('finished')
resolve(false)
})
})
}
}
export default new Uploader()
@@ -1,13 +1,13 @@
import bus from '~/main/utils/eventBus'
import bus from '@core/bus'
import {
uploadClipboardFiles,
uploadChoosedFiles
} from '~/main/apis/uploader/api'
} from 'apis/app/uploader/apis'
import {
createMenu
} from '~/main/apis/app'
import { IWindowList } from '~/main/apis/window/constants'
import windowManager from '~/main/apis/window/windowManager'
} from 'apis/app/system'
import { IWindowList } from 'apis/app/window/constants'
import windowManager from 'apis/app/window/windowManager'
import {
UPLOAD_WITH_FILES,
UPLOAD_WITH_FILES_RESPONSE,
@@ -18,7 +18,7 @@ import {
GET_SETTING_WINDOW_ID,
GET_SETTING_WINDOW_ID_RESPONSE,
CREATE_APP_MENU
} from '~/main/apis/bus/constants'
} from '@core/bus/constants'
function initEventCenter () {
const eventList: any = {
'picgo:upload': uploadClipboardFiles,
@@ -1,24 +1,25 @@
import {
app,
ipcMain,
clipboard,
Notification,
IpcMainEvent
} from 'electron'
import windowManager from '~/main/apis/window/windowManager'
import { IWindowList } from '~/main/apis/window/constants'
import uploader from '~/main/apis/uploader'
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from 'apis/app/window/constants'
import uploader from 'apis/app/uploader'
import pasteTemplate from '#/utils/pasteTemplate'
import db from '#/datastore'
import server from '~/main/server'
import getPicBeds from '~/main/utils/getPicBeds'
import shortKeyHandler from '~/main/apis/shortKey/shortKeyHandler'
import bus from '~/main/utils/eventBus'
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
import bus from '@core/bus'
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
import {
uploadClipboardFiles,
uploadChoosedFiles
} from '~/main/apis/uploader/api'
} from '~/main/apis/app/uploader/apis'
import picgoCoreIPC from './picgoCoreIPC'
import { handleCopyUrl } from '~/main/utils/common'
export default {
listen () {
@@ -29,7 +30,7 @@ export default {
const img = await uploader.setWebContents(trayWindow.webContents).upload()
if (img !== false) {
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
clipboard.writeText(pasteTemplate(pasteStyle, img[0]))
handleCopyUrl(pasteTemplate(pasteStyle, img[0]))
const notification = new Notification({
title: '上传成功',
body: img[0].imgUrl!,
@@ -131,8 +132,8 @@ export default {
evt.returnValue = picBeds
})
ipcMain.on('toggleShortKeyModifiedMode', (evt: IpcMainEvent, val: boolean) => {
bus.emit('toggleShortKeyModifiedMode', val)
ipcMain.on(TOGGLE_SHORTKEY_MODIFIED_MODE, (evt: IpcMainEvent, val: boolean) => {
bus.emit(TOGGLE_SHORTKEY_MODIFIED_MODE, val)
})
ipcMain.on('updateServer', () => {
+260
View File
@@ -0,0 +1,260 @@
import path from 'path'
import GuiApi from 'apis/gui'
import {
dialog,
shell,
IpcMainEvent,
ipcMain,
app
} from 'electron'
import PicGoCore from '~/universal/types/picgo'
import { IPicGoHelperType } from '#/types/enum'
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
import picgo from '@core/picgo'
import { handleStreamlinePluginName } from '~/universal/utils/common'
import { IGuiMenuItem } from 'picgo/dist/src/types'
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from 'apis/app/window/constants'
import { showNotification } from '~/main/utils/common'
// eslint-disable-next-line
const requireFunc = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require
// const PluginHandler = requireFunc('picgo/dist/lib/PluginHandler').default
const STORE_PATH = app.getPath('userData')
// const CONFIG_PATH = path.join(STORE_PATH, '/data.json')
type PicGoNotice = {
title: string,
body: string[]
}
interface GuiMenuItem {
label: string
handle: (arg0: PicGoCore, arg1: GuiApi) => Promise<void>
}
// get uploader or transformer config
const getConfig = (name: string, type: IPicGoHelperType, ctx: PicGoCore) => {
let config: any[] = []
if (name === '') {
return config
} else {
const handler = ctx.helper[type].get(name)
if (handler) {
if (handler.config) {
config = handler.config(ctx)
}
}
return config
}
}
const handleConfigWithFunction = (config: any[]) => {
for (let i in config) {
if (typeof config[i].default === 'function') {
config[i].default = config[i].default()
}
if (typeof config[i].choices === 'function') {
config[i].choices = config[i].choices()
}
}
return config
}
const getPluginList = (): IPicGoPlugin[] => {
const pluginList = picgo.pluginLoader.getFullList()
const list = []
for (let i in pluginList) {
const plugin = picgo.pluginLoader.getPlugin(pluginList[i])!
const pluginPath = path.join(STORE_PATH, `/node_modules/${pluginList[i]}`)
const pluginPKG = requireFunc(path.join(pluginPath, 'package.json'))
const uploaderName = plugin.uploader || ''
const transformerName = plugin.transformer || ''
let menu: IGuiMenuItem[] = []
if (plugin.guiMenu) {
menu = plugin.guiMenu(picgo)
}
let gui = false
if (pluginPKG.keywords && pluginPKG.keywords.length > 0) {
if (pluginPKG.keywords.includes('picgo-gui-plugin')) {
gui = true
}
}
const obj: IPicGoPlugin = {
name: handleStreamlinePluginName(pluginList[i]),
fullName: pluginList[i],
author: pluginPKG.author.name || pluginPKG.author,
description: pluginPKG.description,
logo: 'file://' + path.join(pluginPath, 'logo.png').split(path.sep).join('/'),
version: pluginPKG.version,
gui,
config: {
plugin: {
fullName: pluginList[i],
name: handleStreamlinePluginName(pluginList[i]),
config: plugin.config ? handleConfigWithFunction(plugin.config(picgo)) : []
},
uploader: {
name: uploaderName,
config: handleConfigWithFunction(getConfig(uploaderName, IPicGoHelperType.uploader, picgo))
},
transformer: {
name: transformerName,
config: handleConfigWithFunction(getConfig(uploaderName, IPicGoHelperType.transformer, picgo))
}
},
enabled: picgo.getConfig(`picgoPlugins.${pluginList[i]}`),
homepage: pluginPKG.homepage ? pluginPKG.homepage : '',
guiMenu: menu,
ing: false
}
list.push(obj)
}
return list
}
const handleGetPluginList = () => {
ipcMain.on('getPluginList', (event: IpcMainEvent) => {
const list = getPluginList()
event.sender.send('pluginList', list)
})
}
const handlePluginInstall = () => {
ipcMain.on('installPlugin', async (event: IpcMainEvent, msg: string) => {
const dispose = handleNPMError()
const res = await picgo.pluginHandler.install([msg])
if (res.success) {
event.sender.send('installSuccess', res.body[0])
shortKeyHandler.registerPluginShortKey(res.body[0])
}
event.sender.send('hideLoading')
dispose()
})
}
const handlePluginUninstall = () => {
ipcMain.on('uninstallPlugin', async (event: IpcMainEvent, msg: string) => {
const dispose = handleNPMError()
const res = await picgo.pluginHandler.uninstall([msg])
if (res.success) {
event.sender.send('uninstallSuccess', res.body[0])
shortKeyHandler.unregisterPluginShortKey(res.body[0])
}
event.sender.send('hideLoading')
dispose()
})
}
const handlePluginUpdate = () => {
ipcMain.on('updatePlugin', async (event: IpcMainEvent, msg: string) => {
const dispose = handleNPMError()
const res = await picgo.pluginHandler.update([msg])
if (res.success) {
event.sender.send('updateSuccess', res.body[0])
}
event.sender.send('hideLoading')
dispose()
})
}
const handleNPMError = (): IDispose => {
const handler = (msg: string) => {
if (msg === 'NPM is not installed') {
dialog.showMessageBox({
title: '发生错误',
message: '请安装Node.js并重启PicGo再继续操作',
buttons: ['Yes']
}).then((res) => {
if (res.response === 0) {
shell.openExternal('https://nodejs.org/')
}
})
}
}
picgo.once('failed', handler)
return () => picgo.off('failed', handler)
}
const handleGetPicBedConfig = () => {
ipcMain.on('getPicBedConfig', (event: IpcMainEvent, type: string) => {
const name = picgo.helper.uploader.get(type)?.name || type
if (picgo.helper.uploader.get(type)?.config) {
const config = handleConfigWithFunction(picgo.helper.uploader.get(type)!.config(picgo))
event.sender.send('getPicBedConfig', config, name)
} else {
event.sender.send('getPicBedConfig', [], name)
}
})
}
const handlePluginActions = () => {
ipcMain.on('pluginActions', (event: IpcMainEvent, name: string, label: string) => {
const plugin = picgo.pluginLoader.getPlugin(name)
const guiApi = new GuiApi()
if (plugin?.guiMenu?.(picgo)?.length) {
const menu: GuiMenuItem[] = plugin.guiMenu(picgo)
menu.forEach(item => {
if (item.label === label) {
item.handle(picgo, guiApi)
}
})
}
})
}
const handleRemoveFiles = () => {
ipcMain.on('removeFiles', (event: IpcMainEvent, files: ImgInfo[]) => {
const guiApi = new GuiApi()
setTimeout(() => {
picgo.emit('remove', files, guiApi)
}, 500)
})
}
const handlePicGoSaveData = () => {
ipcMain.on('picgoSaveData', (event: IpcMainEvent, data: IObj) => {
picgo.saveConfig(data)
})
}
const handleImportLocalPlugin = () => {
ipcMain.on('importLocalPlugin', (event: IpcMainEvent) => {
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
dialog.showOpenDialog(settingWindow, {
properties: ['openDirectory']
}, async (filePath: string[]) => {
if (filePath.length > 0) {
const res = await picgo.pluginHandler.install(filePath)
if (res.success) {
const list = getPluginList()
event.sender.send('pluginList', list)
showNotification({
title: '导入插件成功',
body: ''
})
} else {
showNotification({
title: '导入插件失败',
body: res.body as string
})
}
}
event.sender.send('hideLoading')
})
})
}
export default {
listen () {
handleGetPluginList()
handlePluginInstall()
handlePluginUninstall()
handlePluginUpdate()
handleGetPicBedConfig()
handlePluginActions()
handleRemoveFiles()
handlePicGoSaveData()
handleImportLocalPlugin()
}
}
+174
View File
@@ -0,0 +1,174 @@
import {
app,
globalShortcut,
protocol,
dialog,
Notification
} from 'electron'
import {
createProtocol,
installVueDevtools
} from 'vue-cli-plugin-electron-builder/lib'
import beforeOpen from '~/main/utils/beforeOpen'
import fixPath from 'fix-path'
import ipcList from '~/main/events/ipcList'
import busEventList from '~/main/events/busEventList'
import { IWindowList } from 'apis/app/window/constants'
import windowManager from 'apis/app/window/windowManager'
import {
updateShortKeyFromVersion212
} from '~/main/migrate'
import {
uploadChoosedFiles,
uploadClipboardFiles
} from 'apis/app/uploader/apis'
import {
createTray
} from 'apis/app/system'
import server from '~/main/server/index'
import updateChecker from '~/main/utils/updateChecker'
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
import { getUploadFiles } from '~/main/utils/handleArgv'
import db from '#/datastore'
import bus from '@core/bus'
const isDevelopment = process.env.NODE_ENV !== 'production'
class LifeCycle {
private beforeReady () {
protocol.registerSchemesAsPrivileged([{ scheme: 'picgo', privileges: { secure: true, standard: true } }])
// fix the $PATH in macOS
fixPath()
beforeOpen()
ipcList.listen()
busEventList.listen()
updateShortKeyFromVersion212(db, db.get('settings.shortKey'))
}
private onReady () {
app.on('ready', async () => {
createProtocol('picgo')
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
try {
await installVueDevtools()
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString())
}
}
windowManager.create(IWindowList.TRAY_WINDOW)
windowManager.create(IWindowList.SETTING_WINDOW)
createTray()
db.set('needReload', false)
updateChecker()
// 不需要阻塞
process.nextTick(() => {
shortKeyHandler.init()
})
server.startup()
if (process.env.NODE_ENV !== 'development') {
let files = getUploadFiles()
if (files === null || files.length > 0) { // 如果有文件列表作为参数,说明是命令行启动
if (files === null) {
uploadClipboardFiles()
} else {
const win = windowManager.getAvailableWindow()
uploadChoosedFiles(win.webContents, files)
}
}
}
if (global.notificationList?.length > 0) {
while (global.notificationList.length) {
const option = global.notificationList.pop()
const notice = new Notification(option!)
notice.show()
}
}
})
}
private onRunning () {
app.on('second-instance', (event, commandLine, workingDirectory) => {
let files = getUploadFiles(commandLine, workingDirectory)
if (files === null || files.length > 0) { // 如果有文件列表作为参数,说明是命令行启动
if (files === null) {
uploadClipboardFiles()
} else {
const win = windowManager.getAvailableWindow()
uploadChoosedFiles(win.webContents, files)
}
} else {
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
if (settingWindow.isMinimized()) {
settingWindow.restore()
}
settingWindow.focus()
}
}
})
app.on('activate', () => {
createProtocol('picgo')
if (!windowManager.has(IWindowList.TRAY_WINDOW)) {
windowManager.create(IWindowList.TRAY_WINDOW)
}
if (!windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.create(IWindowList.SETTING_WINDOW)
}
})
app.setLoginItemSettings({
openAtLogin: db.get('settings.autoStart') || false
})
if (process.platform === 'win32') {
app.setAppUserModelId('com.molunerfinn.picgo')
}
if (process.env.XDG_CURRENT_DESKTOP && process.env.XDG_CURRENT_DESKTOP.includes('Unity')) {
process.env.XDG_CURRENT_DESKTOP = 'Unity'
}
}
private onQuit () {
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('will-quit', () => {
globalShortcut.unregisterAll()
bus.removeAllListeners()
server.shutdown()
})
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', data => {
if (data === 'graceful-exit') {
app.quit()
server.shutdown()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
server.shutdown()
})
}
}
}
launchApp () {
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
} else {
this.beforeReady()
this.onReady()
this.onRunning()
this.onQuit()
}
}
}
const bootstrap = new LifeCycle()
export {
bootstrap
}
+13 -7
View File
@@ -1,9 +1,18 @@
import DB from '#/datastore'
// from v2.1.2
const updateShortKeyFromVersion212 = (db: typeof DB, shortKeyConfig: IShortKeyConfigs | IOldShortKeyConfigs) => {
let needUpgrade = false
// #557 极端情况可能会出现配置不存在,需要重新写入
if (shortKeyConfig === undefined) {
const defaultShortKeyConfig = {
enable: true,
key: 'CommandOrControl+Shift+P',
name: 'upload',
label: '快捷上传'
}
db.set('settings.shortKey[picgo:upload]', defaultShortKeyConfig)
return true
}
if (shortKeyConfig.upload) {
needUpgrade = true
// @ts-ignore
shortKeyConfig['picgo:upload'] = {
enable: true,
@@ -12,13 +21,10 @@ const updateShortKeyFromVersion212 = (db: typeof DB, shortKeyConfig: IShortKeyCo
label: '快捷上传'
}
delete shortKeyConfig.upload
}
if (needUpgrade) {
db.set('settings.shortKey', shortKeyConfig)
return shortKeyConfig
} else {
return false
return true
}
return false
}
export {
+26 -14
View File
@@ -1,16 +1,18 @@
import http from 'http'
import routers from './routerManager'
import {
handleResponse
handleResponse,
ensureHTTPLink
} from './utils'
import picgo from '~/main/apis/picgo'
import logger from '~/main/utils/logger'
import picgo from '@core/picgo'
import logger from '@core/picgo/logger'
import axios from 'axios'
class Server {
private httpServer: http.Server
private config: IServerConfig
constructor () {
let config = picgo.getConfig('settings.server')
let config = picgo.getConfig<IServerConfig>('settings.server')
const result = this.checkIfConfigIsValid(config)
if (result) {
this.config = config
@@ -75,16 +77,24 @@ class Server {
response.end()
}
}
private listen = (port: number) => {
// port as string is a bug
private listen = (port: number | string) => {
logger.info(`[PicGo Server] is listening at ${port}`)
this.httpServer.listen(port, this.config.host).on('error', (err: ErrnoException) => {
if (typeof port === 'string') {
port = parseInt(port, 10)
}
this.httpServer.listen(port, this.config.host).on('error', async (err: ErrnoException) => {
if (err.errno === 'EADDRINUSE') {
logger.warn(`[PicGo Server] ${port} is busy, trying with port ${port + 1}`)
this.config.port += 1
picgo.saveConfig({
'settings.server': this.config
})
this.listen(this.config.port)
try {
// make sure the system has a PicGo Server instance
await axios.post(ensureHTTPLink(`${this.config.host}:${port}/heartbeat`))
this.shutdown(true)
} catch (e) {
logger.warn(`[PicGo Server] ${port} is busy, trying with port ${(port as number) + 1}`)
// fix a bug: not write an increase number to config file
// to solve the auto number problem
this.listen((port as number) + 1)
}
}
})
}
@@ -93,9 +103,11 @@ class Server {
this.listen(this.config.port)
}
}
shutdown () {
shutdown (hasStarted?: boolean) {
this.httpServer.close()
logger.info('[PicGo Server] shutdown')
if (!hasStarted) {
logger.info('[PicGo Server] shutdown')
}
}
restart () {
this.config = picgo.getConfig('settings.server')
+17 -3
View File
@@ -2,13 +2,13 @@ import router from './router'
import {
uploadWithClipboardFiles,
uploadWithFiles
} from '~/main/apis/bus/index'
} from '@core/bus/apis'
import {
handleResponse
} from './utils'
import logger from '../utils/logger'
import logger from '@core/picgo/logger'
router.get('/upload', async ({
router.post('/upload', async ({
response,
list = []
} : {
@@ -66,4 +66,18 @@ router.get('/upload', async ({
}
})
router.post('/heartbeat', async ({
response
} : {
response: IHttpResponse,
}) => {
handleResponse({
response,
body: {
success: true,
result: 'alive'
}
})
})
export default router
+6
View File
@@ -17,3 +17,9 @@ export const handleResponse = ({
response.write(JSON.stringify(body))
response.end()
}
export const ensureHTTPLink = (url: string): string => {
return url.startsWith('http')
? url
: `http://${url}`
}
+7
View File
@@ -2,11 +2,18 @@ import fs from 'fs-extra'
import path from 'path'
import os from 'os'
import { remote, app } from 'electron'
import pkg from 'root/package.json'
const APP = process.type === 'renderer' ? remote.app : app
const STORE_PATH = APP.getPath('userData')
function injectPicGoVersion () {
global.PICGO_GUI_VERSION = pkg.version
global.PICGO_CORE_VERSION = pkg.dependencies.picgo.replace('^', '')
}
function beforeOpen () {
injectPicGoVersion()
if (process.platform === 'darwin') {
resolveMacWorkFlow()
}
+33
View File
@@ -0,0 +1,33 @@
import db from '#/datastore'
import { clipboard, Notification } from 'electron'
export const handleCopyUrl = (str: string): void => {
if (db.get('settings.autoCopy') !== false) {
clipboard.writeText(str)
}
}
/**
* show notification
* @param options
*/
export const showNotification = (options: IPrivateShowNotificationOption = {
title: '',
body: '',
clickToCopy: false
}) => {
const notification = new Notification({
title: options.title,
body: options.body
})
const handleClick = () => {
if (options.clickToCopy) {
clipboard.writeText(options.body)
}
}
notification.once('click', handleClick)
notification.once('close', () => {
notification.removeListener('click', handleClick)
})
notification.show()
}
+3 -3
View File
@@ -1,13 +1,13 @@
import picgo from '../apis/picgo'
import picgo from '@core/picgo'
const getPicBeds = () => {
const picBedTypes = picgo.helper.uploader.getIdList()
const picBedFromDB = picgo.getConfig('picBed.list') || []
const picBedFromDB = picgo.getConfig<IPicBedType[]>('picBed.list') || []
const picBeds = picBedTypes.map((item: string) => {
const visible = picBedFromDB.find((i: IPicBedType) => i.type === item) // object or undefined
return {
type: item,
name: picgo.helper.uploader.get(item).name || item,
name: picgo.helper.uploader.get(item)!.name || item,
visible: visible ? visible.visible : true
}
}) as IPicBedType[]
-76
View File
@@ -1,76 +0,0 @@
import chalk from 'chalk'
import dayjs from 'dayjs'
import fs from 'fs-extra'
import path from 'path'
import util from 'util'
import db from '#/datastore'
import { app } from 'electron'
import { IChalkType } from '#/types/enum'
const baseDir = app.getPath('userData')
class Logger {
private level = {
success: IChalkType.success,
info: IChalkType.info,
warn: IChalkType.warn,
error: IChalkType.error
}
protected handleLog (type: ILogType, msg: ILoggerType): ILoggerType {
// if configPath is invalid then this.ctx.config === undefined
// if not then check config.silent
const log = chalk[this.level[type]](`[PicGo ${type.toUpperCase()}]:`)
console.log(log, msg)
process.nextTick(() => {
this.handleWriteLog(type, msg)
})
return msg
}
protected handleWriteLog (type: string, msg: ILoggerType): void {
try {
const logLevel = db.get('settings.logLevel')
const logPath = db.get('settings.logPath') || path.join(baseDir, './picgo.log')
if (this.checkLogLevel(type, logLevel)) {
const picgoLog = fs.createWriteStream(logPath, { flags: 'a', encoding: 'utf8' })
let log = `${dayjs().format('YYYY-MM-DD HH:mm:ss')} [PicGo ${type.toUpperCase()}] ${msg}`
const logger = new console.Console(picgoLog)
if (typeof msg === 'object' && type === 'error') {
log += `\n------Error Stack Begin------\n${util.format(msg.stack)}\n-------Error Stack End-------`
}
logger.log(log)
picgoLog.destroy()
}
} catch (e) {
console.log(e)
}
}
protected checkLogLevel (type: string, level: undefined | string | string[]): boolean {
if (level === undefined || level === 'all') {
return true
}
if (Array.isArray(level)) {
return level.some((item: string) => (item === type || item === 'all'))
} else {
return type === level
}
}
success (msg: ILoggerType): ILoggerType {
return this.handleLog('success', msg)
}
info (msg: ILoggerType): ILoggerType {
return this.handleLog('info', msg)
}
error (msg: ILoggerType): ILoggerType {
return this.handleLog('error', msg)
}
warn (msg: ILoggerType): ILoggerType {
return this.handleLog('warn', msg)
}
}
export default new Logger()
+10 -10
View File
@@ -2,6 +2,7 @@ import { dialog, shell } from 'electron'
import db from '#/datastore'
import axios from 'axios'
import pkg from 'root/package.json'
import { lt } from 'semver'
const version = pkg.version
const releaseUrl = 'https://api.github.com/repos/Molunerfinn/PicGo/releases/latest'
const releaseUrlBackup = 'https://cdn.jsdelivr.net/gh/Molunerfinn/PicGo@latest/package.json'
@@ -51,18 +52,17 @@ const checkVersion = async () => {
// if true -> update else return false
const compareVersion2Update = (current: string, latest: string) => {
const currentVersion = current.split('.').map(item => parseInt(item))
const latestVersion = latest.split('.').map(item => parseInt(item))
for (let i = 0; i < 3; i++) {
if (currentVersion[i] < latestVersion[i]) {
return true
}
if (currentVersion[i] > latestVersion[i]) {
return false
try {
if (latest.includes('beta')) {
const isCheckBetaUpdate = db.get('settings.checkBetaUpdate') !== false
if (!isCheckBetaUpdate) {
return false
}
}
return lt(current, latest)
} catch (e) {
return false
}
return false
}
export default checkVersion
+2 -1
View File
@@ -28,7 +28,8 @@ export default {
methods: {
choosePicBed (val) {
this.letPicGoSaveData({
'picBed.current': this.type
'picBed.current': this.type,
'picBed.uploader': this.type
})
this.$emit('update:choosed', this.type)
}
@@ -0,0 +1,64 @@
<template>
<el-dialog
:title="inputBoxOptions.title || '输入框'"
:visible.sync="showInputBoxVisible"
:modal-append-to-body="false"
>
<el-input
v-model="inputBoxValue"
:placeholder="inputBoxOptions.placeholder"></el-input>
<span slot="footer">
<el-button @click="handleInputBoxCancel" round>取消</el-button>
<el-button type="primary" @click="handleInputBoxConfirm" round>确定</el-button>
</span>
</el-dialog>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { remote, ipcRenderer, IpcRendererEvent } from 'electron'
import {
SHOW_INPUT_BOX,
SHOW_INPUT_BOX_RESPONSE
} from '~/universal/events/constants'
@Component({
name: 'input-box-dialog'
})
export default class extends Vue {
inputBoxValue = ''
showInputBoxVisible = false
inputBoxOptions = {
title: '',
placeholder: ''
}
created () {
ipcRenderer.on(SHOW_INPUT_BOX, this.ipcEventHandler)
this.$bus.$on(SHOW_INPUT_BOX, this.initInputBoxValue)
}
ipcEventHandler (evt: IpcRendererEvent, options: IShowInputBoxOption) {
this.initInputBoxValue(options)
}
initInputBoxValue (options: IShowInputBoxOption) {
this.inputBoxValue = options.value || ''
this.inputBoxOptions.title = options.title || ''
this.inputBoxOptions.placeholder = options.placeholder || ''
this.showInputBoxVisible = true
}
handleInputBoxCancel () {
// TODO: RPCServer
this.showInputBoxVisible = false
ipcRenderer.send(SHOW_INPUT_BOX, '')
this.$bus.$emit(SHOW_INPUT_BOX_RESPONSE, '')
}
handleInputBoxConfirm () {
this.showInputBoxVisible = false
ipcRenderer.send(SHOW_INPUT_BOX, this.inputBoxValue)
this.$bus.$emit(SHOW_INPUT_BOX_RESPONSE, this.inputBoxValue)
}
beforeDestroy () {
ipcRenderer.removeListener(SHOW_INPUT_BOX, this.ipcEventHandler)
this.$bus.$off(SHOW_INPUT_BOX)
}
}
</script>
<style lang='stylus'>
</style>
@@ -1,5 +1,5 @@
<template>
<div id="setting-page">
<div id="main-page">
<div class="fake-title-bar" :class="{ 'darwin': os === 'darwin' }">
<div class="fake-title-bar__title">
PicGo - {{ version }}
@@ -121,28 +121,71 @@
</span>
</el-dialog>
<el-dialog
:title="inputBoxOptions.title || '输入框'"
:visible.sync="showInputBoxVisible"
class="qrcode-dialog"
top="3vh"
width="60%"
title="图床配置二维码"
:visible.sync="qrcodeVisible"
:modal-append-to-body="false"
@close="handleInputBoxClose"
lock-scroll
>
<el-input
v-model="inputBoxValue"
:placeholder="inputBoxOptions.placeholder"></el-input>
<span slot="footer">
<el-button @click="showInputBoxVisible = false" round>取消</el-button>
<el-button type="primary" @click="showInputBoxVisible = false" round>确定</el-button>
</span>
<el-form
label-position="left"
label-width="70px"
size="mini"
>
<el-form-item
label="选择图床"
>
<el-select
v-model="choosedPicBedForQRCode"
multiple
collapse-tags
>
<el-option
v-for="item in picBed"
:key="item.type"
:label="item.name"
:value="item.type"
></el-option>
</el-select>
<el-button
v-show="choosedPicBedForQRCode.length > 0"
type="primary"
round
class="copy-picbed-config"
@click="handleCopyPicBedConfig"
>
复制图床配置
</el-button>
</el-form-item>
</el-form>
<div class="qrcode-container">
<qrcode-vue
v-show="choosedPicBedForQRCode.length > 0"
:size="280"
:value="picBedConfigString"
/>
</div>
</el-dialog>
<input-box-dialog />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { Component, Vue, Watch } from 'vue-property-decorator'
import QrcodeVue from 'qrcode.vue'
import pick from 'lodash/pick'
import pkg from 'root/package.json'
import keyDetect from '@/utils/key-binding'
import { remote, ipcRenderer, IpcRendererEvent } from 'electron'
import {
remote,
ipcRenderer,
IpcRendererEvent,
clipboard
} from 'electron'
import db from '#/datastore'
import mixin from '@/utils/mixin'
import InputBoxDialog from '@/components/InputBoxDialog.vue'
const { Menu, dialog, BrowserWindow } = remote
const customLinkRule = (rule: string, value: string, callback: (arg0?: Error) => void) => {
if (!/\$url/.test(value)) {
@@ -152,8 +195,12 @@ const customLinkRule = (rule: string, value: string, callback: (arg0?: Error) =>
}
}
@Component({
name: 'setting-page',
mixins: [mixin]
name: 'main-page',
mixins: [mixin],
components: {
InputBoxDialog,
QrcodeVue
}
})
export default class extends Vue {
version = process.env.NODE_ENV === 'production' ? pkg.version : 'Dev'
@@ -175,25 +222,27 @@ export default class extends Vue {
upload: db.get('shortKey.upload')
}
picBed: IPicBedType[] = []
// for showInputBox
showInputBoxVisible = false
inputBoxValue = ''
inputBoxOptions = {
title: '',
placeholder: ''
}
qrcodeVisible = false
picBedConfigString = ''
choosedPicBedForQRCode: string[] = []
created () {
this.os = process.platform
this.buildMenu()
ipcRenderer.send('getPicBeds')
ipcRenderer.on('getPicBeds', this.getPicBeds)
ipcRenderer.on('showInputBox', (evt: IpcRendererEvent, options: IShowInputBoxOption) => {
this.inputBoxValue = ''
this.inputBoxOptions.title = options.title || ''
this.inputBoxOptions.placeholder = options.placeholder || ''
this.showInputBoxVisible = true
})
}
@Watch('choosedPicBedForQRCode')
choosedPicBedForQRCodeChange (val: string[], oldVal: string[]) {
if (val.length > 0) {
this.$nextTick(() => {
const picBedConfig = db.get('picBed')
const config = pick(picBedConfig, ...this.choosedPicBedForQRCode)
this.picBedConfigString = JSON.stringify(config)
})
}
}
handleSelect (index: string) {
const type = index.match(/picbeds-/)
if (type === null) {
@@ -222,7 +271,11 @@ export default class extends Vue {
}
closeWindow () {
const window = BrowserWindow.getFocusedWindow()
window!.close()
if (process.platform === 'linux') {
window!.hide()
} else {
window!.close()
}
}
buildMenu () {
const _this = this
@@ -242,6 +295,12 @@ export default class extends Vue {
click () {
_this.visible = true
}
},
{
label: '生成图床配置二维码',
click () {
_this.qrcodeVisible = true
}
}
]
this.menu = Menu.buildFromTemplate(template)
@@ -276,12 +335,13 @@ export default class extends Vue {
openMiniWindow () {
ipcRenderer.send('openMiniWindow')
}
handleCopyPicBedConfig () {
clipboard.writeText(this.picBedConfigString)
this.$message.success('图床配置复制成功')
}
getPicBeds (event: IpcRendererEvent, picBeds: IPicBedType[]) {
this.picBed = picBeds
}
handleInputBoxClose () {
ipcRenderer.send('showInputBox', this.inputBoxValue)
}
beforeRouteEnter (to: any, from: any, next: any) {
next((vm: this) => {
vm.defaultActive = to.name
@@ -289,7 +349,6 @@ export default class extends Vue {
}
beforeDestroy () {
ipcRenderer.removeListener('getPicBeds', this.getPicBeds)
ipcRenderer.removeAllListeners('showInputBox')
}
}
</script>
@@ -308,7 +367,17 @@ $darwinBg = transparentify(#172426, #000, 0.7)
font-size 20px
text-align center
margin 10px auto
#setting-page
#main-page
.qrcode-dialog
.qrcode-container
display flex
justify-content center
.el-dialog__body
padding-top 10px
.copy-picbed-config
margin-left 10px
.el-input__inner
border-radius 14px
.fake-title-bar
-webkit-app-region drag
height h = 22px
+86 -21
View File
@@ -48,14 +48,19 @@
<i slot="suffix" class="el-input__icon el-icon-close" v-if="searchText" @click="cleanSearch" style="cursor: pointer"></i>
</el-input>
</el-col>
<el-col :span="6">
<el-col :span="4">
<div class="item-base copy round" :class="{ active: isMultiple(choosedList)}" @click="multiCopy">
<i class="el-icon-document"></i> 批量复制
复制
</div>
</el-col>
<el-col :span="6">
<el-col :span="4">
<div class="item-base delete round" :class="{ active: isMultiple(choosedList)}" @click="multiRemove">
<i class="el-icon-delete"></i> 批量删除
删除
</div>
</el-col>
<el-col :span="4">
<div class="item-base all-pick round" :class="{ active: filterList.length > 0}" @click="toggleSelectAll">
{{ isAllSelected ? '取消' : '全选' }}
</div>
</el-col>
</el-row>
@@ -82,7 +87,7 @@
<i class="el-icon-document" @click="copy(item)"></i>
<i class="el-icon-edit-outline" @click="openDialog(item)"></i>
<i class="el-icon-delete" @click="remove(item.id)"></i>
<el-checkbox v-model="choosedList[item.id]" class="pull-right" @change=" handleBarActive = true"></el-checkbox>
<el-checkbox v-model="choosedList[item.id]" class="pull-right" @change="(val) => handleChooseImage(val, index)"></el-checkbox>
</div>
</el-col>
</el-row>
@@ -106,7 +111,7 @@
// @ts-ignore
import gallerys from 'vue-gallery'
import pasteStyle from '#/utils/pasteTemplate'
import { Component, Vue } from 'vue-property-decorator'
import { Component, Vue, Watch } from 'vue-property-decorator'
import {
ipcRenderer,
clipboard,
@@ -128,11 +133,13 @@ export default class extends Vue {
}
dialogVisible = false
imgInfo = {
id: null,
id: '',
imgUrl: ''
}
choosedList: IObjT<boolean> = {}
choosedPicBed: string[] = []
lastChoosed: number = -1
isShiftKeyPress: boolean = false
searchText = ''
handleBarActive = false
pasteStyle = ''
@@ -144,12 +151,11 @@ export default class extends Vue {
Custom: 'Custom'
}
picBed: IPicBedType[] = []
beforeRouteEnter (to: any, from: any, next: any) {
next((vm: any) => {
vm.getGallery()
vm.getPasteStyle()
vm.getPicBeds()
})
@Watch('$route')
handleRouteUpdate (to: any, from: any) {
if (from.name === 'gallery') {
this.clearChoosedList()
}
}
created () {
ipcRenderer.on('updateGallery', (event: IpcRendererEvent) => {
@@ -160,12 +166,31 @@ export default class extends Vue {
ipcRenderer.send('getPicBeds')
ipcRenderer.on('getPicBeds', this.getPicBeds)
}
mounted () {
document.addEventListener('keydown', this.handleDetectShiftKey)
document.addEventListener('keyup', this.handleDetectShiftKey)
}
handleDetectShiftKey (event: KeyboardEvent) {
if (event.keyCode === 16) {
this.isShiftKeyPress = !this.isShiftKeyPress
}
}
get filterList () {
return this.getGallery()
}
set filterList (val) {
this.images = val
}
get isAllSelected () {
const values = Object.values(this.choosedList)
if (values.length === 0) {
return false
} else {
return this.filterList.every(item => {
return this.choosedList[item.id!]
})
}
}
getPicBeds (event: IpcRendererEvent, picBeds: IPicBedType[]) {
this.picBed = picBeds
}
@@ -200,6 +225,31 @@ export default class extends Vue {
}
return this.images
}
@Watch('filterList')
handleFilterListChange () {
this.clearChoosedList()
}
handleChooseImage (val: boolean, index: number) {
if (val === true) {
this.handleBarActive = true
if (this.lastChoosed !== -1 && this.isShiftKeyPress) {
let min = Math.min(this.lastChoosed, index)
let max = Math.max(this.lastChoosed, index)
for (let i = min + 1; i < max; i++) {
const id = this.filterList[i].id!
this.$set(this.choosedList, id, true)
}
}
this.lastChoosed = index
}
}
clearChoosedList () {
this.isShiftKeyPress = false
Object.keys(this.choosedList).forEach(key => {
this.choosedList[key] = false
})
this.lastChoosed = -1
}
zoomImage (index: number) {
this.idx = index
this.changeZIndexForGallery(true)
@@ -255,7 +305,7 @@ export default class extends Vue {
})
}
openDialog (item: ImgInfo) {
this.imgInfo.id = item.id
this.imgInfo.id = item.id!
this.imgInfo.imgUrl = item.imgUrl as string
this.dialogVisible = true
}
@@ -291,10 +341,17 @@ export default class extends Vue {
isMultiple (obj: IObj) {
return Object.values(obj).some(item => item)
}
toggleSelectAll () {
const result = !this.isAllSelected
this.filterList.forEach(item => {
this.$set(this.choosedList, item.id!, result)
})
}
multiRemove () {
// choosedList -> { [id]: true or false }; true means choosed. false means not choosed.
if (Object.values(this.choosedList).some(item => item)) {
this.$confirm('将删除刚才选中的图片,是否继续?', '提示', {
const multiRemoveNumber = Object.values(this.choosedList).filter(item => item).length
if (multiRemoveNumber) {
this.$confirm(`将在相册中移除刚才选中的 ${multiRemoveNumber} 张图片,是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
@@ -307,7 +364,8 @@ export default class extends Vue {
this.$db.removeById('uploaded', key)
}
})
this.choosedList = {}
this.clearChoosedList()
this.choosedList = {} // 只有删除才能将这个置空
this.getGallery()
const obj = {
title: '操作结果',
@@ -325,22 +383,22 @@ export default class extends Vue {
}
multiCopy () {
if (Object.values(this.choosedList).some(item => item)) {
let copyString = ''
const copyString: string[] = []
const style = this.$db.get('settings.pasteStyle') || 'markdown'
// choosedList -> { [id]: true or false }; true means choosed. false means not choosed.
Object.keys(this.choosedList).forEach(key => {
if (this.choosedList[key]) {
const item = this.$db.getById('uploaded', key)
copyString += pasteStyle(style, item) + '\n'
copyString.push(pasteStyle(style, item))
this.choosedList[key] = false
}
})
const obj = {
title: '批量复制链接成功',
body: copyString
body: copyString.join('\n')
}
const myNotification = new Notification(obj.title, obj)
clipboard.writeText(copyString)
clipboard.writeText(copyString.join('\n'))
myNotification.onclick = () => {
return true
}
@@ -400,6 +458,13 @@ export default class extends Vue {
cursor pointer
background #F15140
color #fff
&.all-pick
cursor not-allowed
background #69C282
&.active
cursor pointer
background #44B363
color #fff
#gallery-view
position relative
.round
+2 -1
View File
@@ -145,7 +145,8 @@ export default class extends Vue {
checked: this.$db.get('picBed.current') === item.type,
click () {
_this.letPicGoSaveData({
'picBed.current': item.type
'picBed.current': item.type,
'picBed.uploader': item.type
})
ipcRenderer.send('syncPicBed')
}
+67 -8
View File
@@ -4,10 +4,10 @@
PicGo设置 - <i class="el-icon-document" @click="goConfigPage"></i>
</div>
<el-row class="setting-list">
<el-col :span="15" :offset="4">
<el-col :span="16" :offset="4">
<el-row>
<el-form
label-width="120px"
label-width="160px"
label-position="right"
size="small"
>
@@ -32,7 +32,7 @@
<el-button type="primary" round size="mini" @click="customLinkVisible = true">点击设置</el-button>
</el-form-item>
<el-form-item
label="设置代理"
label="设置代理和镜像地址"
>
<el-button type="primary" round size="mini" @click="proxyVisible = true">点击设置</el-button>
</el-form-item>
@@ -56,6 +56,17 @@
@change="updateHelperChange"
></el-switch>
</el-form-item>
<el-form-item
v-show="form.updateHelper"
label="接受Beta版本更新"
>
<el-switch
v-model="form.checkBetaUpdate"
active-text=""
inactive-text=""
@change="checkBetaUpdateChange"
></el-switch>
</el-form-item>
<el-form-item
label="开机自启"
>
@@ -107,6 +118,16 @@
@change="handleMiniWindowOntop"
></el-switch>
</el-form-item>
<el-form-item
label="上传后自动复制URL"
>
<el-switch
v-model="form.autoCopyUrl"
active-text=""
inactive-text=""
@change="handleAutoCopyUrl"
></el-switch>
</el-form-item>
<el-form-item
label="选择显示的图床"
>
@@ -162,19 +183,20 @@
</span>
</el-dialog>
<el-dialog
title="设置代理"
title="设置代理和镜像地址"
:visible.sync="proxyVisible"
:modal-append-to-body="false"
width="70%"
>
<el-form
label-position="right"
:model="customLink"
ref="customLink"
:rules="rules"
label-width="80px"
label-width="120px"
>
<el-form-item
label="代理地址"
label="上传代理"
>
<el-input
v-model="proxy"
@@ -182,6 +204,24 @@
placeholder="例如:http://127.0.0.1:1080"
></el-input>
</el-form-item>
<el-form-item
label="插件安装代理"
>
<el-input
v-model="npmProxy"
:autofocus="true"
placeholder="例如:http://127.0.0.1:1080"
></el-input>
</el-form-item>
<el-form-item
label="插件镜像地址"
>
<el-input
v-model="npmRegistry"
:autofocus="true"
placeholder="例如:https://registry.npm.taobao.org/"
></el-input>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="cancelProxy" round>取消</el-button>
@@ -336,7 +376,9 @@ export default class extends Vue {
autoRename: db.get('settings.autoRename') || false,
uploadNotification: db.get('settings.uploadNotification') || false,
miniWindowOntop: db.get('settings.miniWindowOntop') || false,
logLevel
logLevel,
autoCopyUrl: db.get('settings.autoCopy') === undefined ? true : db.get('settings.autoCopy'),
checkBetaUpdate: db.get('settings.checkBetaUpdate') === undefined ? true : db.get('settings.checkBetaUpdate')
}
picBed: IPicBedType[] = []
logFileVisible = false
@@ -352,6 +394,8 @@ export default class extends Vue {
upload: db.get('settings.shortKey.upload')
}
proxy = db.get('picBed.proxy') || ''
npmRegistry = db.get('settings.registry') || ''
npmProxy = db.get('settings.proxy') || ''
rules = {
value: [
{ validator: customLinkRule, trigger: 'blur' }
@@ -429,7 +473,9 @@ export default class extends Vue {
confirmProxy () {
this.proxyVisible = false
this.letPicGoSaveData({
'picBed.proxy': this.proxy
'picBed.proxy': this.proxy,
'settings.proxy': this.npmProxy,
'settings.registry': this.npmRegistry
})
const successNotification = new Notification('设置代理', {
body: '设置成功'
@@ -441,6 +487,9 @@ export default class extends Vue {
updateHelperChange (val: boolean) {
db.set('settings.showUpdateTip', val)
}
checkBetaUpdateChange (val: boolean) {
db.set('settings.checkBetaUpdate', val)
}
handleShowPicBedListChange (val: string[]) {
const list = this.picBed.map(item => {
if (!val.includes(item.name)) {
@@ -513,6 +562,15 @@ export default class extends Vue {
db.set('settings.miniWindowOntop', val)
this.$message.info('需要重启生效')
}
handleAutoCopyUrl (val: boolean) {
db.set('settings.autoCopy', val)
const successNotification = new Notification('设置自动复制链接', {
body: '设置成功'
})
successNotification.onclick = () => {
return true
}
}
confirmLogLevelSetting () {
if (this.form.logLevel.length === 0) {
return this.$message.error('请选择日志记录等级')
@@ -541,6 +599,7 @@ export default class extends Vue {
this.form.logLevel = logLevel
}
confirmServerSetting () {
this.server.port = parseInt(this.server.port, 10)
this.letPicGoSaveData({
'settings.server': this.server
})
+51 -19
View File
@@ -1,7 +1,13 @@
<template>
<div id="plugin-view">
<div class="view-title">
插件设置 - <i class="el-icon-goods" @click="goAwesomeList"></i>
插件设置 -
<el-tooltip :content="pluginListToolTip" placement="right">
<i class="el-icon-goods" @click="goAwesomeList"></i>
</el-tooltip>
<el-tooltip :content="importLocalPluginToolTip" placement="left">
<i class="el-icon-download" @click="handleImportLocalPlugin"/>
</el-tooltip>
</div>
<el-row class="handle-bar" :class="{ 'cut-width': pluginList.length > 6 }">
<el-input
@@ -13,7 +19,7 @@
</el-input>
</el-row>
<el-row :gutter="10" class="plugin-list" v-loading="loading">
<el-col :span="12" v-for="item in pluginList" :key="item.name">
<el-col :span="12" v-for="item in pluginList" :key="item.fullName">
<div class="plugin-item" :class="{ 'darwin': os === 'darwin' }">
<div class="cli-only-badge" v-if="!item.gui" title="CLI only">CLI</div>
<img class="plugin-item__logo" :src="item.logo"
@@ -106,6 +112,7 @@ import {
remote,
IpcRendererEvent
} from 'electron'
import { handleStreamlinePluginName } from '~/universal/utils/common'
const { Menu } = remote
@Component({
@@ -125,6 +132,8 @@ export default class extends Vue {
pluginNameList: string[] = []
loading = true
needReload = false
pluginListToolTip = '插件列表'
importLocalPluginToolTip = '导入本地插件'
id = ''
os = ''
defaultLogo: string = 'this.src="https://cdn.jsdelivr.net/gh/Molunerfinn/PicGo@dev/public/roundLogo.png"'
@@ -157,15 +166,18 @@ export default class extends Vue {
}
created () {
this.os = process.platform
ipcRenderer.on('hideLoading', () => {
this.loading = false
})
ipcRenderer.on('pluginList', (evt: IpcRendererEvent, list: IPicGoPlugin[]) => {
this.pluginList = list
this.pluginNameList = list.map(item => item.name)
this.pluginNameList = list.map(item => item.fullName)
this.loading = false
})
ipcRenderer.on('installSuccess', (evt: IpcRendererEvent, plugin: string) => {
this.loading = false
this.pluginList.forEach(item => {
if (item.name === plugin) {
if (item.fullName === plugin) {
item.ing = false
item.hasInstall = true
}
@@ -174,7 +186,7 @@ export default class extends Vue {
ipcRenderer.on('updateSuccess', (evt: IpcRendererEvent, plugin: string) => {
this.loading = false
this.pluginList.forEach(item => {
if (item.name === plugin) {
if (item.fullName === plugin) {
item.ing = false
item.hasInstall = true
}
@@ -186,7 +198,7 @@ export default class extends Vue {
ipcRenderer.on('uninstallSuccess', (evt: IpcRendererEvent, plugin: string) => {
this.loading = false
this.pluginList = this.pluginList.filter(item => {
if (item.name === plugin) { // restore Uploader & Transformer after uninstalling
if (item.fullName === plugin) { // restore Uploader & Transformer after uninstalling
if (item.config.transformer.name) {
this.handleRestoreState('transformer', item.config.transformer.name)
}
@@ -195,7 +207,7 @@ export default class extends Vue {
}
this.getPicBeds()
}
return item.name !== plugin
return item.fullName !== plugin
})
this.pluginNameList = this.pluginNameList.filter(item => item !== plugin)
})
@@ -210,7 +222,7 @@ export default class extends Vue {
enabled: !plugin.enabled,
click () {
_this.letPicGoSaveData({
[`picgoPlugins.picgo-plugin-${plugin.name}`]: true
[`picgoPlugins.${plugin.fullName}`]: true
})
plugin.enabled = true
_this.getPicBeds()
@@ -220,7 +232,7 @@ export default class extends Vue {
enabled: plugin.enabled,
click () {
_this.letPicGoSaveData({
[`picgoPlugins.picgo-plugin-${plugin.name}`]: false
[`picgoPlugins.${plugin.fullName}`]: false
})
plugin.enabled = false
_this.getPicBeds()
@@ -234,21 +246,21 @@ export default class extends Vue {
}, {
label: '卸载插件',
click () {
_this.uninstallPlugin(plugin.name)
_this.uninstallPlugin(plugin.fullName)
}
}, {
label: '更新插件',
click () {
_this.updatePlugin(plugin.name)
_this.updatePlugin(plugin.fullName)
}
}]
for (let i in plugin.config) {
if (plugin.config[i].config.length > 0) {
const obj = {
label: `配置${i} - ${plugin.config[i].name}`,
label: `配置${i} - ${plugin.config[i].fullName || plugin.config[i].name}`,
click () {
_this.currentType = i
_this.configName = plugin.config[i].name
_this.configName = plugin.config[i].fullName || plugin.config[i].name
_this.dialogVisible = true
_this.config = plugin.config[i].config
}
@@ -280,7 +292,7 @@ export default class extends Vue {
menu.push({
label: i.label,
click () {
ipcRenderer.send('pluginActions', plugin.name, i.label)
ipcRenderer.send('pluginActions', plugin.fullName, i.label)
}
})
}
@@ -318,14 +330,16 @@ export default class extends Vue {
item.ing = true
}
})
this.loading = true
ipcRenderer.send('uninstallPlugin', val)
}
updatePlugin (val: string) {
this.pluginList.forEach(item => {
if (item.name === val) {
if (item.fullName === val) {
item.ing = true
}
})
this.loading = true
ipcRenderer.send('updatePlugin', val)
}
reloadApp () {
@@ -364,7 +378,7 @@ export default class extends Vue {
switch (this.currentType) {
case 'plugin':
this.letPicGoSaveData({
[`picgo-plugin-${this.configName}`]: result
[`${this.configName}`]: result
})
break
case 'uploader':
@@ -407,7 +421,7 @@ export default class extends Vue {
})
}
handleSearchResult (item: INPMSearchResultObject) {
const name = item.package.name.replace(/picgo-plugin-/, '')
const name = handleStreamlinePluginName(item.package.name)
let gui = false
if (item.package.keywords && item.package.keywords.length > 0) {
if (item.package.keywords.includes('picgo-gui-plugin')) {
@@ -416,12 +430,13 @@ export default class extends Vue {
}
return {
name: name,
fullName: item.package.name,
author: item.package.author.name,
description: item.package.description,
logo: `https://cdn.jsdelivr.net/npm/${item.package.name}/logo.png`,
config: {},
homepage: item.package.links ? item.package.links.homepage : '',
hasInstall: this.pluginNameList.some(plugin => plugin === item.package.name.replace(/picgo-plugin-/, '')),
hasInstall: this.pluginNameList.some(plugin => plugin === item.package.name),
version: item.package.version,
gui,
ing: false // installing or uninstalling
@@ -433,7 +448,8 @@ export default class extends Vue {
const current = this.$db.get('picBed.current')
if (current === name) {
this.letPicGoSaveData({
'picBed.current': 'smms'
'picBed.current': 'smms',
'picBed.uploader': 'smms'
})
}
}
@@ -457,11 +473,16 @@ export default class extends Vue {
letPicGoSaveData (data: IObj) {
ipcRenderer.send('picgoSaveData', data)
}
handleImportLocalPlugin () {
ipcRenderer.send('importLocalPlugin')
this.loading = true
}
beforeDestroy () {
ipcRenderer.removeAllListeners('pluginList')
ipcRenderer.removeAllListeners('installSuccess')
ipcRenderer.removeAllListeners('uninstallSuccess')
ipcRenderer.removeAllListeners('updateSuccess')
ipcRenderer.removeAllListeners('hideLoading')
}
}
</script>
@@ -491,6 +512,7 @@ $darwinBg = #172426
font-size 20px
text-align center
margin 10px auto
position relative
i.el-icon-goods
font-size 20px
vertical-align middle
@@ -498,6 +520,16 @@ $darwinBg = #172426
transition color .2s ease-in-out
&:hover
color #49B1F5
i.el-icon-download
position absolute
right 0
top 8px
font-size 20px
vertical-align middle
cursor pointer
transition color .2s ease-in-out
&:hover
color #49B1F5
.handle-bar
margin-bottom 20px
&.cut-width
+3 -2
View File
@@ -98,6 +98,7 @@
import { Component, Vue, Watch } from 'vue-property-decorator'
import keyDetect from '@/utils/key-binding'
import { ipcRenderer, IpcRendererEvent } from 'electron'
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
@Component({
name: 'shortkey-page'
@@ -119,7 +120,7 @@ export default class extends Vue {
}
@Watch('keyBindingVisible')
onKeyBindingVisibleChange (val: boolean) {
ipcRenderer.send('toggleShortKeyModifiedMode', val)
ipcRenderer.send(TOGGLE_SHORTKEY_MODIFIED_MODE, val)
}
calcOrigin (item: string) {
const [origin] = item.split(':')
@@ -162,7 +163,7 @@ export default class extends Vue {
})
}
beforeDestroy () {
ipcRenderer.send('toggleShortKeyModifiedMode', false)
ipcRenderer.send(TOGGLE_SHORTKEY_MODIFIED_MODE, false)
}
}
</script>
+59 -5
View File
@@ -48,7 +48,8 @@
<div class="paste-style__text">
快捷上传
</div>
<el-button type="primary" round size="mini" @click="uploadClipboardFiles" class="paste-upload">剪贴板图片上传</el-button>
<el-button type="primary" round size="mini" @click="uploadClipboardFiles" class="quick-upload" style="width: 50%">剪贴板图片</el-button>
<el-button type="primary" round size="mini" @click="uploadURLFiles" class="quick-upload" style="width: 46%; margin-left: 6px">URL</el-button>
</div>
</div>
</el-col>
@@ -62,6 +63,13 @@ import {
IpcRendererEvent,
remote
} from 'electron'
import {
SHOW_INPUT_BOX,
SHOW_INPUT_BOX_RESPONSE
} from '~/universal/events/constants'
import {
isUrl
} from '~/universal/utils/common'
const { Menu } = remote
@Component({
name: 'upload'
@@ -92,6 +100,7 @@ export default class extends Vue {
})
ipcRenderer.send('getPicBeds')
ipcRenderer.on('getPicBeds', this.getPicBeds)
this.$bus.$on(SHOW_INPUT_BOX_RESPONSE, this.handleInputBoxValue)
}
@Watch('progress')
onProgressChange (val: number) {
@@ -106,13 +115,41 @@ export default class extends Vue {
}
}
beforeDestroy () {
this.$bus.$off(SHOW_INPUT_BOX_RESPONSE)
ipcRenderer.removeAllListeners('uploadProgress')
ipcRenderer.removeAllListeners('syncPicBed')
ipcRenderer.removeListener('getPicBeds', this.getPicBeds)
}
onDrop (e: DragEvent) {
this.dragover = false
this.ipcSendFiles(e.dataTransfer!.files)
const items = e.dataTransfer!.items
if (items.length === 2 && items[0].type === 'text/uri-list') {
this.handleURLDrag(items, e.dataTransfer!)
} else if (items[0].type === 'text/plain') {
const str = e.dataTransfer!.getData(items[0].type)
if (isUrl(str)) {
ipcRenderer.send('uploadChoosedFiles', [{ path: str }])
} else {
this.$message.error('请拖入合法的图片文件或者图片URL地址')
}
} else {
this.ipcSendFiles(e.dataTransfer!.files)
}
}
handleURLDrag (items: DataTransferItemList, dataTransfer: DataTransfer) {
// text/html
// Use this data to get a more precise URL
const urlString = dataTransfer.getData(items[1].type)
const urlMatch = urlString.match(/<img.*src="(.*?)"/)
if (urlMatch) {
ipcRenderer.send('uploadChoosedFiles', [
{
path: urlMatch[1]
}
])
} else {
this.$message.error('请拖入合法的图片文件或者图片URL地址')
}
}
openUplodWindow () {
document.getElementById('file-uploader')!.click()
@@ -141,6 +178,24 @@ export default class extends Vue {
uploadClipboardFiles () {
ipcRenderer.send('uploadClipboardFilesFromUploadPage')
}
async uploadURLFiles () {
const str = await navigator.clipboard.readText()
this.$bus.$emit(SHOW_INPUT_BOX, {
value: isUrl(str) ? str : '',
title: '请输入URL',
placeholder: 'http://或者https://开头'
})
}
handleInputBoxValue (val: string) {
if (val === '') return false
if (isUrl(val)) {
ipcRenderer.send('uploadChoosedFiles', [{
path: val
}])
} else {
this.$message.error('请输入合法的URL')
}
}
getDefaultPicBed () {
const current: string = this.$db.get('picBed.current')
this.picBed.forEach(item => {
@@ -167,7 +222,8 @@ export default class extends Vue {
checked: this.$db.get('picBed.current') === item.type,
click () {
_this.letPicGoSaveData({
'picBed.current': item.type
'picBed.current': item.type,
'picBed.uploader': item.type
})
ipcRenderer.send('syncPicBed')
}
@@ -239,8 +295,6 @@ export default class extends Vue {
.el-radio-button__inner
border-left none
border-radius 0 14px 14px 0
.paste-upload
width 100%
.el-icon-caret-bottom
cursor pointer
</style>
+1 -1
View File
@@ -25,7 +25,7 @@
:rules="{
required: true, message: '分支名不能为空', trigger: 'blur'
}">
<el-input v-model="form.branch" @keyup.native.enter="confirm" placeholder="例如:master"></el-input>
<el-input v-model="form.branch" @keyup.native.enter="confirm" placeholder="例如:main"></el-input>
</el-form-item>
<el-form-item
label="设定Token"
+7 -2
View File
@@ -1,6 +1,6 @@
<template>
<div id="others-view">
<el-row :gutter="16">
<el-row :gutter="16" class="setting-list">
<el-col :span="16" :offset="4">
<div class="view-title">
{{ picBedName }}设置
@@ -69,7 +69,8 @@ export default class extends Vue {
}
setDefaultPicBed (type: string) {
this.letPicGoSaveData({
'picBed.current': type
'picBed.current': type,
'picBed.uploader': type
})
// @ts-ignore 来自mixin的数据
this.defaultPicBed = type
@@ -91,6 +92,10 @@ export default class extends Vue {
</script>
<style lang='stylus'>
#others-view
.setting-list
height 425px
overflow-y auto
overflow-x hidden
.el-form
label
line-height 22px
+69 -21
View File
@@ -5,12 +5,27 @@
<div class="view-title">
SM.MS设置
</div>
<div class="content">
感谢SM.MS提供的优质服务
</div>
<div style="text-align: center; margin-top: 20px;">
<el-button type="success" @click="confirm" round :disabled="defaultPicBed === 'smms'" size="mini">设为默认图床</el-button>
</div>
<el-form
ref="smms"
label-position="right"
label-width="120px"
:model="form"
size="mini">
<el-form-item
label="设定Token"
prop="token"
:rules="{
required: true, message: 'Token不能为空', trigger: 'blur'
}">
<el-input v-model="form.token" type="password" placeholder="token" @keyup.native.enter="confirm('smmsForm')"></el-input>
</el-form-item>
<el-form-item>
<el-button-group>
<el-button type="primary" @click="confirm" round>确定</el-button>
<el-button type="success" @click="setDefaultPicBed('smms')" round :disabled="defaultPicBed === 'smms'">设为默认图床</el-button>
</el-button-group>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
@@ -23,25 +38,58 @@ import mixin from '@/utils/ConfirmButtonMixin'
mixins: [mixin]
})
export default class extends Vue {
confirm () {
this.letPicGoSaveData({
'picBed.smms': true
})
// @ts-ignore 来自mixin
this.setDefaultPicBed('smms')
const successNotification = new window.Notification('设置结果', {
body: '设置成功'
})
successNotification.onclick = () => {
return true
form: ISMMSConfig = {
token: ''
}
created () {
const config = this.$db.get('picBed.smms.token') as (string | boolean)
if (typeof config !== 'boolean') {
this.form.token = config
}
}
confirm () {
// @ts-ignore
this.$refs.smms.validate((valid) => {
if (valid) {
this.letPicGoSaveData({
'picBed.smms': this.form
})
const successNotification = new window.Notification('设置结果', {
body: '设置成功'
})
successNotification.onclick = () => {
return true
}
} else {
return false
}
})
}
}
</script>
<style lang='stylus'>
#smms-view
.content
text-align center
font-size 14px
color #eee
.el-form
label
line-height 22px
padding-bottom 0
color #eee
.el-input__inner
border-radius 19px
&-item
margin-bottom 10.5px
.el-radio-group
width 100%
label
width 25%
.el-radio-button__inner
width 100%
.el-radio-button:first-child
.el-radio-button__inner
border-left none
border-radius 14px 0 0 14px
.el-radio-button:last-child
.el-radio-button__inner
border-left none
border-radius 0 14px 14px 0
</style>
+3 -3
View File
@@ -22,9 +22,9 @@ export default new Router({
component: () => import(/* webpackChunkName: "MiniPage" */ '@/pages/MiniPage.vue')
},
{
path: '/setting',
name: 'setting-page',
component: () => import(/* webpackChunkName: "SettingPage" */ '@/layouts/SettingPage.vue'),
path: '/main-page',
name: 'main-page',
component: () => import(/* webpackChunkName: "SettingPage" */ '@/layouts/Main.vue'),
children: [
{
path: 'upload',
+2 -1
View File
@@ -5,7 +5,8 @@ export default class extends Vue {
defaultPicBed = this.$db.get('picBed.current')
setDefaultPicBed (type: string) {
this.letPicGoSaveData({
'picBed.current': type
'picBed.current': type,
'picBed.uploader': type
})
this.defaultPicBed = type
const successNotification = new Notification('设置默认图床', {
+2
View File
@@ -0,0 +1,2 @@
import Vue from 'vue'
export default new Vue()
+55
View File
@@ -0,0 +1,55 @@
import fs from 'fs-extra'
import path from 'path'
import { app } from 'electron'
import dayjs from 'dayjs'
const errorMsg = {
broken: 'PicGo 配置文件损坏,已经恢复为默认配置',
brokenButBackup: 'PicGo 配置文件损坏,已经恢复为备份配置'
}
function dbChecker () {
if (process.type !== 'renderer') {
if (!global.notificationList) global.notificationList = []
const STORE_PATH = app.getPath('userData')
const configFilePath = path.join(STORE_PATH, 'data.json')
const configFileBackupPath = path.join(STORE_PATH, 'data.bak.json')
if (!fs.existsSync(configFilePath)) {
return
}
let configFile: string = '{}'
let optionsTpl = {
title: '注意',
body: ''
}
try {
configFile = fs.readFileSync(configFilePath, { encoding: 'utf-8' })
JSON.parse(configFile)
} catch (e) {
fs.unlinkSync(configFilePath)
if (fs.existsSync(configFileBackupPath)) {
try {
configFile = fs.readFileSync(configFileBackupPath, { encoding: 'utf-8' })
JSON.parse(configFile)
fs.writeFileSync(configFilePath, configFile, { encoding: 'utf-8' })
const stats = fs.statSync(configFileBackupPath)
optionsTpl.body = `${errorMsg.brokenButBackup}\n备份文件版本:${dayjs(stats.mtime).format('YYYY-MM-DD HH:mm:ss')}`
global.notificationList.push(optionsTpl)
return
} catch (e) {
optionsTpl.body = errorMsg.broken
global.notificationList.push(optionsTpl)
return
}
}
optionsTpl.body = errorMsg.broken
global.notificationList.push(optionsTpl)
return
}
fs.writeFileSync(configFileBackupPath, configFile, { encoding: 'utf-8' })
}
}
export {
dbChecker
}
+7 -1
View File
@@ -5,6 +5,7 @@ import FileSync from 'lowdb/adapters/FileSync'
import path from 'path'
import fs from 'fs-extra'
import { remote, app } from 'electron'
import { dbChecker } from './dbChecker'
const APP = process.type === 'renderer' ? remote.app : app
const STORE_PATH = APP.getPath('userData')
@@ -13,6 +14,7 @@ if (process.type !== 'renderer') {
if (!fs.pathExistsSync(STORE_PATH)) {
fs.mkdirpSync(STORE_PATH)
}
dbChecker()
}
class DB {
@@ -29,7 +31,11 @@ class DB {
if (!this.db.has('picBed').value()) {
this.db.set('picBed', {
current: 'weibo'
current: 'smms', // deprecated
uploader: 'smms',
smms: {
token: ''
}
}).write()
}
+3
View File
@@ -0,0 +1,3 @@
export const SHOW_INPUT_BOX = 'SHOW_INPUT_BOX'
export const SHOW_INPUT_BOX_RESPONSE = 'SHOW_INPUT_BOX_RESPONSE'
export const TOGGLE_SHORTKEY_MODIFIED_MODE = 'TOGGLE_SHORTKEY_MODIFIED_MODE'
+16 -3
View File
@@ -3,18 +3,31 @@ import {
} from 'electron'
import {
IWindowList
} from '~/main/apis/window/constants'
} from 'apis/app/window/constants'
declare interface IWindowListItem {
isValid: boolean
multiple: boolean
options: () => IBrowserWindowOptions,
callback: (window: BrowserWindow | null) => void
callback: (window: BrowserWindow, windowManager: IWindowManager) => void
}
declare interface IWindowManager {
create: (name: IWindowList) => BrowserWindow | null
get: (name: IWindowList) => BrowserWindow | null
delete: (name: IWindowList) => void
has: (name: IWindowList) => boolean
// delete: (name: IWindowList) => void
deleteById: (id: number) => void
getAvailableWindow: () => BrowserWindow
}
// https://stackoverflow.com/questions/35074713/extending-typescript-global-object-in-node-js/44387594#44387594
declare global {
namespace NodeJS {
interface Global {
PICGO_GUI_VERSION: string
PICGO_CORE_VERSION: string
notificationList: IAppNotification[]
}
}
}
+1
View File
@@ -8,6 +8,7 @@ declare module 'vue/types/vue' {
$db: typeof db
$http: typeof axios
$builtInPicBed: string[]
$bus: Vue
letPicGoSaveData(data: IObj): void
}
}
+26 -2
View File
@@ -30,8 +30,8 @@ interface IServerCTX {
}
interface IServerConfig {
port: number
host: string,
port: number | string
host: string
enable: boolean
}
@@ -44,6 +44,7 @@ interface ImgInfo {
height?: number
extname?: string
imgUrl?: string
id?: string
[propName: string]: any
}
@@ -122,6 +123,7 @@ interface IBounds {
type ICtx = import('picgo')
interface IPicGoPlugin {
name: string
fullName: string
author: string
description: string
logo: string
@@ -144,6 +146,7 @@ interface IPicGoPlugin {
interface IPluginMenuConfig {
name: string
fullName?: string
config: any[]
}
@@ -170,6 +173,8 @@ interface INPMSearchResultObject {
}
}
type IDispose = () => void
// GuiApi
interface IGuiApi {
showInputBox: (options: IShowInputBoxOption) => Promise<string>
@@ -179,6 +184,7 @@ interface IGuiApi {
showMessageBox: (options?: IShowMessageBoxOption) => Promise<IShowMessageBoxResult>
}
interface IShowInputBoxOption {
value?: string
title: string
placeholder: string
}
@@ -190,6 +196,14 @@ type IUploadOption = string[]
interface IShowNotificationOption {
title: string
body: string
icon?: string | import('electron').NativeImage
}
interface IPrivateShowNotificationOption extends IShowNotificationOption{
/**
* click notification to copy the body
*/
clickToCopy?: boolean
}
interface IShowMessageBoxOption {
@@ -251,6 +265,10 @@ interface IQiniuConfig {
path: string
}
interface ISMMSConfig {
token: string
}
interface ITcYunConfig {
secretId: string,
secretKey: string,
@@ -271,3 +289,9 @@ interface IUpYunConfig {
}
type ILoggerType = string | Error | boolean | number | undefined
interface IAppNotification {
title: string
body: string
icon?: string
}
+2
View File
@@ -7,6 +7,8 @@ interface ISettingForm {
uploadNotification: boolean
miniWindowOntop: boolean
logLevel: string[]
autoCopyUrl: boolean
checkBetaUpdate: boolean
}
interface IShortKeyMap {
+32
View File
@@ -0,0 +1,32 @@
export const isUrl = (url: string): boolean => (url.startsWith('http://') || url.startsWith('https://'))
export const isUrlEncode = (url: string): boolean => {
url = url || ''
try {
return url !== decodeURI(url)
} catch (e) {
// if some error caught, try to let it go
return true
}
}
export const handleUrlEncode = (url: string): string => {
if (!isUrlEncode(url)) {
url = encodeURI(url)
}
return url
}
/**
* streamline the full plugin name to a simple one
* for example:
* 1. picgo-plugin-xxx -> xxx
* 2. @xxx/picgo-plugin-yyy -> yyy
* @param name pluginFullName
*/
export const handleStreamlinePluginName = (name: string) => {
if (/^@[^/]+\/picgo-plugin-/.test(name)) {
return name.replace(/^@[^/]+\/picgo-plugin-/, '')
} else {
return name.replace(/picgo-plugin-/, '')
}
}
+3 -2
View File
@@ -1,9 +1,10 @@
import db from '#/datastore'
import { IPasteStyle } from '#/types/enum'
import { handleUrlEncode } from './common'
const formatCustomLink = (customLink: string, item: ImgInfo) => {
let fileName = item.fileName!.replace(new RegExp(`\\${item.extname}$`), '')
let url = item.url || item.imgUrl
const url = handleUrlEncode(item.url || item.imgUrl)
const formatObj = {
url,
fileName
@@ -19,7 +20,7 @@ const formatCustomLink = (customLink: string, item: ImgInfo) => {
}
export default (style: IPasteStyle, item: ImgInfo) => {
let url = item.url || item.imgUrl
const url = handleUrlEncode(item.url || item.imgUrl)
const customLink = db.get('settings.customLink') || '$url'
const tpl = {
'markdown': `![](${url})`,
+6
View File
@@ -30,6 +30,12 @@
],
"#/*": [
"src/universal/*"
],
"apis/*": [
"src/main/apis/*"
],
"@core/*": [
"src/main/apis/core/*"
]
},
"lib": [
+17 -1
View File
@@ -2,7 +2,11 @@ const path = require('path')
function resolve (dir) {
return path.join(__dirname, dir)
}
module.exports = {
const config = {
configureWebpack: {
devtool: 'nosources-source-map'
},
chainWebpack: config => {
config.resolve.alias
.set('@', resolve('src/renderer'))
@@ -20,6 +24,8 @@ module.exports = {
.set('~', resolve('src'))
.set('root', resolve('./'))
.set('#', resolve('src/universal'))
.set('apis', resolve('src/main/apis'))
.set('@core', resolve('src/main/apis/core'))
},
builderOptions: {
productName: 'PicGo',
@@ -72,3 +78,13 @@ module.exports = {
}
}
}
if (process.env.NODE_ENV === 'development') {
config.configureWebpack = {
devtool: 'eval-source-map'
}
}
module.exports = {
...config
}
+25 -35
View File
@@ -3659,7 +3659,7 @@ de-indent@^1.0.2:
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=
debug@2, debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@@ -3797,11 +3797,6 @@ deepmerge@^1.2.0, deepmerge@^1.5.2:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753"
integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==
deepmerge@^4.0.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
deepmerge@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.0.1.tgz#25c1c24f110fb914f80001b925264dd77f3f4312"
@@ -5999,6 +5994,13 @@ ignore@^5.0.2:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf"
integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
image-size@^0.8.3:
version "0.8.3"
resolved "https://registry.npmjs.org/image-size/-/image-size-0.8.3.tgz#f0b568857e034f29baffd37013587f2c0cad8b46"
integrity sha512-SMtq1AJ+aqHB45c3FsB4ERK0UCiA2d3H1uq8s+8T0Pf8A3W4teyBQyaFaktH6xvZqh+npwlKU7i4fJo0r7TYTg==
dependencies:
queue "6.0.1"
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
@@ -7581,11 +7583,6 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
next-tick@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -8292,10 +8289,10 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
picgo@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/picgo/-/picgo-1.4.4.tgz#4fd2d51793a7f90a144a816c60035ed1f2f76bf2"
integrity sha512-0rieY+qlyfLVhPdwXZSqwitNCifgWJohDnnfHE92tPmP738r35Ab0wfa7eh4LRuI8Gx376FTNdlTIUQ4mHEsxQ==
picgo@^1.4.19:
version "1.4.19"
resolved "https://registry.yarnpkg.com/picgo/-/picgo-1.4.19.tgz#cbd4b39f1d1a2a5f231e62a7fdbf87f53298f2a8"
integrity sha512-Y1BUrwq9rzEDQ06ZV5ZR8WRZqQe5hAHNVs4F/nEtwWBfr2YFwPxdce0b/rEPMZDSM7dfCWrts2M1qZC8VxB2+g==
dependencies:
chalk "^2.4.1"
commander "^2.17.0"
@@ -8306,12 +8303,12 @@ picgo@^1.4.4:
ejs "^2.6.1"
fs-extra "^6.0.1"
globby "^8.0.2"
image-size "^0.8.3"
inquirer "^6.0.0"
lodash-id "^0.14.0"
lowdb "^1.0.0"
md5 "^2.2.1"
minimatch "^3.0.4"
probe-image-size "^4.0.0"
qiniu "^7.2.1"
request "^2.87.0"
request-promise-native "^1.0.5"
@@ -8785,18 +8782,6 @@ private@^0.1.6:
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
probe-image-size@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-4.1.1.tgz#c836c53154b6dd04dbcf66af2bbd50087b15e1dc"
integrity sha512-42LqKZqTLxH/UvAZ2/cKhAsR4G/Y6B7i7fI2qtQu9hRBK4YjS6gqO+QRtwTjvojUx4+/+JuOMzLoFyRecT9qRw==
dependencies:
any-promise "^1.3.0"
deepmerge "^4.0.0"
inherits "^2.0.3"
next-tick "^1.0.0"
request "^2.83.0"
stream-parser "~0.3.1"
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -8923,6 +8908,11 @@ qiniu@^7.2.1:
tunnel-agent "0.6.0"
urllib "2.22.0"
qrcode.vue@^1.7.0:
version "1.7.0"
resolved "https://registry.npmjs.org/qrcode.vue/-/qrcode.vue-1.7.0.tgz#c54b2934f0650c10d92785d08aaad36c55e0fc56"
integrity sha512-R7t6Y3fDDtcU7L4rtqwGUDP9xD64gJhIwpfjhRCTKmBoYF6SS49PIJHRJ048cse6OI7iwTwgyy2C46N9Ygoc6g==
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@@ -8961,6 +8951,13 @@ querystringify@^2.1.1:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
queue@6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/queue/-/queue-6.0.1.tgz#abd5a5b0376912f070a25729e0b6a7d565683791"
integrity sha512-AJBQabRCCNr9ANq8v77RJEv73DPbn55cdTb+Giq4X0AVnNVZvMHlYp7XlQiN+1npCZj1DuSmaA2hYVUUDgxFDg==
dependencies:
inherits "~2.0.3"
quick-lru@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
@@ -10109,13 +10106,6 @@ stream-http@^2.7.2:
to-arraybuffer "^1.0.0"
xtend "^4.0.0"
stream-parser@~0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773"
integrity sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=
dependencies:
debug "2"
stream-shift@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"