Compare commits

..
4 Commits
Author SHA1 Message Date
PiEgg 9274b3f8be 📦 Chore(fuck): electron-builder publish option 2020-01-01 19:38:31 +08:00
PiEgg 7809b626d3 🐛 Fix: cI build error in linux 2020-01-01 19:29:37 +08:00
PiEgg a7995c037c 📦 Chore: fix CI script 2020-01-01 19:01:47 +08:00
PiEgg 85f4c67e0b 🎉 Release: v2.2.0 2020-01-01 18:18:51 +08:00
90 changed files with 2105 additions and 4001 deletions
+1 -3
View File
@@ -15,9 +15,7 @@ module.exports = {
'plugins': ['@typescript-eslint'],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'off' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
"indent": "off",
"@typescript-eslint/indent": ["error", 2]
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
},
parserOptions: {
parser: '@typescript-eslint/parser'
-1
View File
@@ -1 +0,0 @@
custom: ["https://paypal.me/Molunerfinn"]
+7 -11
View File
@@ -1,34 +1,30 @@
<!--
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的相关信息
<!-- PicGo 的版本、所在平台(Mac 或者 Windows 或者 Linux -->
<!-- 版本、所在平台(Mac或者Windows -->
## 问题重现
<!-- 如果是 Bug Report 请填写本项 -->
<!-- 如果是Bug Report请填写本项 -->
<!-- 请附上相关截图 -->
<!-- 请附上 PicGo 的相关报错日志(用文本的形式)否则无法判断原因-->
<!-- 报错日志可以在 PicGo 设置 -> 设置日志文件 -> 点击打开 后找到 -->
## 功能请求
<!-- 如果是 Feature Request 请填写本项 -->
<!-- 如果是Feature Request请填写本项 -->
<!-- 详细描述你所预想的功能或者是现有功能的改进。 -->
---
<!--
最后,喜欢 PicGo 的话不妨给它点个 star~
最后,喜欢PicGo的话不妨给它点个star~
如果可以的话,请我喝杯咖啡?首页有赞助二维码,谢谢你的支持!
-->
-55
View File
@@ -1,55 +0,0 @@
# 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,4 +16,3 @@ docs/dist/
.env.local
.env.*.local
dist_electron/
test.js
+3 -3
View File
@@ -22,7 +22,9 @@ addons:
- icnsutils
#- xvfb
before_install:
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install git-lfs; fi
- 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" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi
install:
#- export DISPLAY=':99.0'
@@ -37,8 +39,6 @@ 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
-233
View File
@@ -1,236 +1,3 @@
# :tada: 2.3.0-beta.7 (2021-08-01)
### :sparkles: Features
* add gallery db ([6ddd660](https://github.com/Molunerfinn/PicGo/commit/6ddd660))
* add win32 support ([1657542](https://github.com/Molunerfinn/PicGo/commit/1657542)), closes [#632](https://github.com/Molunerfinn/PicGo/issues/632)
* finish custom config path ([7030f7a](https://github.com/Molunerfinn/PicGo/commit/7030f7a)), closes [#255](https://github.com/Molunerfinn/PicGo/issues/255)
### :bug: Bug Fixes
* bug of gallery db for plugin ([96a63ea](https://github.com/Molunerfinn/PicGo/commit/96a63ea))
* enable plugin should reload ([49e5f34](https://github.com/Molunerfinn/PicGo/commit/49e5f34)), closes [#659](https://github.com/Molunerfinn/PicGo/issues/659)
* gallery db bug ([f1eb7f4](https://github.com/Molunerfinn/PicGo/commit/f1eb7f4))
* multiple uploading in the same time will cause output conflict ([06b67e5](https://github.com/Molunerfinn/PicGo/commit/06b67e5)), closes [#666](https://github.com/Molunerfinn/PicGo/issues/666)
* multiple uploading in the same time will cause rename failed ([12cecc2](https://github.com/Molunerfinn/PicGo/commit/12cecc2))
* uploader error in linux ([ab762ef](https://github.com/Molunerfinn/PicGo/commit/ab762ef)), closes [#627](https://github.com/Molunerfinn/PicGo/issues/627)
* use uploader first ([92022a6](https://github.com/Molunerfinn/PicGo/commit/92022a6))
* windows ia32 && x64 build options ([bdf523a](https://github.com/Molunerfinn/PicGo/commit/bdf523a))
# :tada: 2.3.0-beta.6 (2021-04-24)
### :sparkles: Features
* add baidu tongji for analytics ([f536391](https://github.com/Molunerfinn/PicGo/commit/f536391))
* add logs for picgo-server ([2d9e9c0](https://github.com/Molunerfinn/PicGo/commit/2d9e9c0)), closes [#627](https://github.com/Molunerfinn/PicGo/issues/627)
* add privacy policy ([992ff35](https://github.com/Molunerfinn/PicGo/commit/992ff35))
### :bug: Bug Fixes
* add plugin install failed notice ([b05139f](https://github.com/Molunerfinn/PicGo/commit/b05139f))
* default picBed using picBed.uploader instead of picBed.current ([0a986c8](https://github.com/Molunerfinn/PicGo/commit/0a986c8))
* dev error with install vue-devtools ([a657c51](https://github.com/Molunerfinn/PicGo/commit/a657c51)), closes [#653](https://github.com/Molunerfinn/PicGo/issues/653) [#658](https://github.com/Molunerfinn/PicGo/issues/658)
* disable plugin need reload app ([a1b70b4](https://github.com/Molunerfinn/PicGo/commit/a1b70b4))
* fix analytics value ([06d40ef](https://github.com/Molunerfinn/PicGo/commit/06d40ef))
* windows cli uploading bug ([321e339](https://github.com/Molunerfinn/PicGo/commit/321e339)), closes [#657](https://github.com/Molunerfinn/PicGo/issues/657)
### :package: Chore
* add main process hot reload ([3fd6e4e](https://github.com/Molunerfinn/PicGo/commit/3fd6e4e))
* fix eslint error notice in indent ([69e1dc8](https://github.com/Molunerfinn/PicGo/commit/69e1dc8))
# :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)
### :bug: Bug Fixes
* picgo-server upload clipboard file's result bug ([4ebd76f](https://github.com/Molunerfinn/PicGo/commit/4ebd76f))
* releaseUrl may can't get latest version ([ee46ab1](https://github.com/Molunerfinn/PicGo/commit/ee46ab1))
## :tada: 2.2.1 (2020-01-09)
### :sparkles: Features
* add alias for plugin config name ([5a06483](https://github.com/Molunerfinn/PicGo/commit/5a06483))
* add aliyun oss options ([a33f1ad](https://github.com/Molunerfinn/PicGo/commit/a33f1ad)), closes [#347](https://github.com/Molunerfinn/PicGo/issues/347)
* **server:** add http server for uploading images by a http request ([c56d4ef](https://github.com/Molunerfinn/PicGo/commit/c56d4ef))
* add server config settings ([6b57cf7](https://github.com/Molunerfinn/PicGo/commit/6b57cf7))
* only shows visible pic-beds ([9d4d605](https://github.com/Molunerfinn/PicGo/commit/9d4d605)), closes [#310](https://github.com/Molunerfinn/PicGo/issues/310)
### :bug: Bug Fixes
* decrease title-bar z-index when config-form dialog shows ([f2750e1](https://github.com/Molunerfinn/PicGo/commit/f2750e1))
* **website:** website pictures error ([a5b6526](https://github.com/Molunerfinn/PicGo/commit/a5b6526))
* add new tray icon for macOS dark-mode ([c5adf3b](https://github.com/Molunerfinn/PicGo/commit/c5adf3b)), closes [#267](https://github.com/Molunerfinn/PicGo/issues/267)
* beforeOpen handler in windows ([cd30a6c](https://github.com/Molunerfinn/PicGo/commit/cd30a6c))
* busApi event register first && emit later ([e1a0cbb](https://github.com/Molunerfinn/PicGo/commit/e1a0cbb))
* enum type error ([4e3fa28](https://github.com/Molunerfinn/PicGo/commit/4e3fa28))
* handle empty request-body ([81e6acb](https://github.com/Molunerfinn/PicGo/commit/81e6acb))
* launch error in new structrue ([bc8e641](https://github.com/Molunerfinn/PicGo/commit/bc8e641))
* miniWindow minimize bug ([5f2b7c7](https://github.com/Molunerfinn/PicGo/commit/5f2b7c7))
* plugin config-form && default plugin logo ([514fc40](https://github.com/Molunerfinn/PicGo/commit/514fc40))
* release script ([b4f10c6](https://github.com/Molunerfinn/PicGo/commit/b4f10c6))
* removeById handler error ([c4f0a30](https://github.com/Molunerfinn/PicGo/commit/c4f0a30)), closes [#382](https://github.com/Molunerfinn/PicGo/issues/382)
* rename page not work ([29a55ed](https://github.com/Molunerfinn/PicGo/commit/29a55ed))
* save debug mode && PICGO_ENV into config file ([c6ead5b](https://github.com/Molunerfinn/PicGo/commit/c6ead5b))
* server may never start ([73870a5](https://github.com/Molunerfinn/PicGo/commit/73870a5))
* settingPage && miniPage style in windows ([3fd9572](https://github.com/Molunerfinn/PicGo/commit/3fd9572))
### :pencil: Documentation
* add note for windows electron mirror ([46a49ed](https://github.com/Molunerfinn/PicGo/commit/46a49ed))
* remove weibo picbed ([e81b8f4](https://github.com/Molunerfinn/PicGo/commit/e81b8f4))
* update installation by scoop ([91b397d](https://github.com/Molunerfinn/PicGo/commit/91b397d)), closes [#295](https://github.com/Molunerfinn/PicGo/issues/295)
* update readme ([1b3522e](https://github.com/Molunerfinn/PicGo/commit/1b3522e))
* update README ([f491209](https://github.com/Molunerfinn/PicGo/commit/f491209))
* update site ([fe9e19a](https://github.com/Molunerfinn/PicGo/commit/fe9e19a))
# :tada: 2.2.0 (2020-01-01)
+23 -24
View File
@@ -1,59 +1,58 @@
## 常见问题
> 在使用 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),应该能找到答案。
> 在使用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),应该能找到答案。
## 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.6PicGo 支持了如下图床:
截止v1.6,PicGo支持了如下图床:
- `微博图床` 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
所以本体内将不会再支持其他图床。需要其他图床支持可以参考目前已有的三方 [插件](https://github.com/PicGo/Awesome-PicGo),如果还是没有你所需要的图床欢迎开发一个插件供大家使用
所以本体内将不会再支持其他图床。待2.0版本出来支持插件系统后可以由其他开发者开发相应的图床插件用以支持其他图床
## 6. 一个图床设置多个信息
## 6.一个图床设置多个信息
不能。因为目前的架构只支持一个图床一份信息。
## 7. GitHub 图床有时能上传,有时上传失败
## 7.v2.0版本有什么新功能
1. GitHub 图床不支持上传同名文件,如果有同名文件上传,会报错。建议开启 `时间戳重命名` 避免同名文件
2. GitHub 服务器和国内 GFW 的问题会导致有时上传成功,有时上传失败,无解。想要稳定请使用付费云存储,如阿里云、腾讯云等,价格也不会贵。
在不改变PicGo现有使用界面和使用习惯的基础上,将会重构整个底层`传输->转换->上传`的架构,使其能够支持插件系统,方便其他开发者基于PicGo的底层进行二次开发
## 8. Mac 上无法打开 PicGo 的主窗口界面
理论上有了插件系统之后,其他开发者能够支持如下功能:
PicGo 在 Mac 上是一个顶部栏应用,在 dock 栏是不会有图标的。要打开主窗口,请右键或者双指点按顶部栏 PicGo 图标,选择「打开详细窗口」即可打开主窗口。
1. 能够实现上传前图片压缩
2. 能够通过图片url实现上传图片 -> 更深入地可以批量地实现图床的迁移
3. 能够支持其他图床甚至是自己的图床
## 9. 上传失败,或者是服务器出错
等等。
1. PicGo 自带的图床都经过测试,上传出错一般都不是 PicGo 自身的原因。如果你用的是 GitHub 图床请参考上面的第 7 点
2. 检查 PicGo 的日志(报错日志可以在 PicGo 设置 -> 设置日志文件 -> 点击打开 后找到),看看 `[PicGo Error]` 的报错信息里有什么关键信息
1. 先自行搜索 error 里的报错信息,往往你能百度或者谷歌出问题原因,不必开 issue
2. 如果有带有 `401``403``40X` 状态码字样的,不用怀疑,就是你配置写错了,仔细检查配置,看看是否多了空格之类的。
3. 如果带有 `HttpError``RequestError``socket hang up` 等字样的说明这是网络问题,我无法帮你解决网络问题,请检查你自己的网络,是否有代理,DNS 设置是否正常等。
届时底层架构将会独立出来成为一个独立的npm包,能够直接在命令行里调用
目前开发进度可以参考[PicGo-Core](https://github.com/Molunerfinn/PicGo-Core)
+30 -53
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/Molunerfinn/PicGo/actions">
<a href="https://github.com/feross/standard">
<img src="https://img.shields.io/badge/code%20style-standard-green.svg?style=flat-square" alt="">
</a>
<a href="https://github.com/Molunerfinn/PicGo/actions">
<img src="https://github.com/Molunerfinn/PicGo/workflows/Build/badge.svg" alt="">
<a href="https://travis-ci.org/Molunerfinn/PicGo/builds">
<img src="https://img.shields.io/travis/Molunerfinn/PicGo.svg?style=flat-square" 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,67 +17,47 @@
<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: 一个用于快速上传图片并获取图片 URL 链接的工具**
**PicGo 在上传图片之后自动会将图片链接复制到你的剪贴板里,可选 5 种复制的链接格式。**
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 V2` v2.3.0-beta.0
- `SM.MS` v1.5.1
- `阿里云 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) 找到。欢迎贡献!
- 支持拖拽图片上传
- 支持快捷键上传剪贴板里第一张图片
- 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) 与我交流
PicGo 支持 macOS、Windows 64位(>= v1.3.1),Linux>= v1.6.0)。
**如果第一次使用,请参考应用 [使用文档](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)。**
支持快捷键`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://github.com/Molunerfinn/PicGo/releases)。
点击此处下载[应用](https://github.com/Molunerfinn/PicGo/releases)。
### Windows
macOS 用户请下载最新版本的 `dmg` 文件,Windows 用户请下载最新版本的 `exe` 文件,Linux用户请下载 `AppImage` 文件。
Windows 用户请下载最新版本的 `exe` 文件。
**如果你是 Arch 类的 Linux 用户,可以直接通过 `aurman -S picgo-appimage` 来安装 PicGo。感谢 @houbaron 的贡献!**
**还可以使用 [Scoop](https://scoop.sh/) 来安装 PicGo: `scoop bucket add helbing https://github.com/helbing/scoop-bucket` & `scoop install picgo`。 感谢 @helbing 的贡献!**
**如果你是 macOS 用户,可以使用 `brew cask` 来安装 PicGo: `brew cask install picgo`。感谢 @womeimingzi11 的贡献!**
**还可以使用 [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 的贡献!**
**如果你是 Windows 用户,还可以使用 [Scoop](https://scoop.sh/) 来安装 PicGo: `scoop bucket add helbing https://github.com/helbing/scoop-bucket` & `scoop install picgo`。 感谢 @helbing 的贡献!**
## 应用截图
@@ -93,9 +73,9 @@ Linux 用户请下载 `AppImage` 文件。
> 如果想学习 Electron-vue 的开发,可以查看我写的系列教程——[Electron-vue 开发实战](https://molunerfinn.com/tags/Electron-vue/)
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` 将导致未知错误!**
1. 你需要有 Node、Git环境,了解 npm 的相关知识。
2. `git clone https://github.com/Molunerfinn/PicGo.git` 并进入项目
3. `npm install` 下载依赖
4. Mac 需要有 Xcode 环境,Windows 需要有 VS 环境。
### 开发模式
@@ -104,29 +84,26 @@ Linux 用户请下载 `AppImage` 文件。
```bash
ctrl+c # 退出开发模式
npm run electron:serve # 重新进入开发模式
npm run dev # 重新进入开发模式
```
**注:Windows 开发模式运行之后会在底部任务栏的右下角应用区出现 PicGo 的应用图标。**
### 生产模式
如果你需要自行构建,可以 `npm run electron:build` 开始进行构建。构建成功后,会在 `dist_electron` 目录里出现构建成功的相应安装文件。
**注意**:如果你的网络环境不太好,可能会出现 `electron-builder` 下载 `electron` 二进制文件失败的情况。这个时候需要在 `npm run electron:build` 之前指定一下 `electron` 的源为国内源:
**注意**:如果你的网络环境不太好,可能会出现 `electron-builder` 下载 `electron` 二进制文件失败的情况。这个时候需要在 `npm run build` 之前指定一下 `electron` 的源为国内源:
```bash
export ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/"
# 在 Windows 上,则可以使用 set ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/ (无需引号)
npm run electron:build
npm run build
```
只需第一次构建的时候指定一下国内源即可。后续构建不需要特地指定。二进制文件下载在 `~/.electron/` 目录下。如果想要更新 `electron` 构建版本,可以删除 `~/.electron/` 目录,然后重新运行上一步,让 `electron-builder `去下载最新的 `electron` 二进制文件。
## 其他相关
- [vs-picgo](https://github.com/PicGo/vs-picgo)PicGo 的 VS Code 版。
- [flutter-picgo](https://github.com/PicGo/flutter-picgo)PicGo 的手机版(支持 Android 和 iOS )。
- [vs-picgo](https://github.com/Spades-S/vs-picgo)PicGo 的 VS Code 版。
## 赞助
@@ -144,4 +121,4 @@ npm run electron:build
[MIT](http://opensource.org/licenses/MIT)
Copyright (c) 2017 - 2019 Molunerfinn
Copyright (c) 2017 - 2019 Molunerfinn
+1 -2
View File
@@ -26,7 +26,6 @@ install:
build_script:
#- yarn test
- yarn build --win --ia32
- yarn release
- npm run release
test: off
+4 -8
View File
@@ -1,10 +1,10 @@
{
"name": "picgo",
"version": "2.3.0-beta.7",
"version": "2.2.0",
"private": true,
"scripts": {
"dev": "vue-cli-service electron:serve",
"build": "vue-cli-service electron:build",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
@@ -34,7 +34,6 @@
]
},
"dependencies": {
"@picgo/store": "^1.0.3",
"axios": "^0.19.0",
"core-js": "^3.3.2",
"element-ui": "^2.13.0",
@@ -43,9 +42,7 @@
"keycode": "^2.2.0",
"lodash-id": "^0.14.0",
"lowdb": "^1.0.0",
"picgo": "^1.4.24",
"qrcode.vue": "^1.7.0",
"uuidv4": "^6.2.11",
"picgo": "^1.4.4",
"vue": "^2.6.10",
"vue-gallery": "^2.0.1",
"vue-lazyload": "^1.2.6",
@@ -70,7 +67,6 @@
"conventional-changelog": "^3.1.18",
"cz-customizable": "^6.2.0",
"electron": "^6.0.0",
"electron-devtools-installer": "^3.2.0",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"husky": "^3.1.0",
-1
View File
@@ -27,7 +27,6 @@ function main {
$stream.Dispose() | out-null
$imagePath
Exit 1
}
try {
-18
View File
@@ -1,18 +0,0 @@
#!/bin/sh
# grab the paths
scriptPath=$(echo $0 | awk '{ print substr( $0, 1, length($0)-6 ) }')"windows10.ps1"
imagePath=$(echo $1 | awk '{ print substr( $0, 1, length($0)-18 ) }')
imageName=$(echo $1 | awk '{ print substr( $0, length($0)-17, length($0) ) }')
# run the powershell script
res=$(powershell.exe -noprofile -noninteractive -nologo -sta -executionpolicy unrestricted -file $(wslpath -w $scriptPath) $(wslpath -w $imagePath)"\\"$imageName)
# note that there is a return symbol in powershell result
noImage=$(echo "no image\r")
# check whether image exists
if [ "$res" = "$noImage" ] ;then
echo "no image"
else
echo $(wslpath -u $res)
fi
+722 -2
View File
@@ -1,6 +1,726 @@
import { bootstrap } from '~/main/lifeCycle'
'use strict'
bootstrap.launchApp()
import {
app,
BrowserWindow,
Tray,
Menu,
Notification,
clipboard,
ipcMain,
globalShortcut,
dialog,
systemPreferences,
WebContents,
IpcMainEvent,
protocol
} from 'electron'
import {
createProtocol,
installVueDevtools
} from 'vue-cli-plugin-electron-builder/lib'
import db from '#/datastore'
import picgo from '~/main/utils/picgo'
import uploader from '~/main/utils/uploader'
import beforeOpen from '~/main/utils/beforeOpen'
import pasteTemplate from '#/utils/pasteTemplate'
import updateChecker from '~/main/utils/updateChecker'
import { getPicBeds } from '~/main/utils/getPicBeds'
import pkg from 'root/package.json'
import picgoCoreIPC from '~/main/utils/picgoCoreIPC'
import fixPath from 'fix-path'
import { getUploadFiles } from '~/main/utils/handleArgv'
import bus from '~/main/utils/eventBus'
import {
updateShortKeyFromVersion212
} from '~/main/migrate/shortKeyUpdateHelper'
import shortKeyHandler from '~/main/utils/shortKeyHandler'
import logger from '~/main/utils/logger'
import {
UPLOAD_WITH_FILES,
UPLOAD_WITH_FILES_RESPONSE,
UPLOAD_WITH_CLIPBOARD_FILES,
UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE,
GET_WINDOW_ID,
GET_WINDOW_ID_REPONSE,
GET_SETTING_WINDOW_ID,
GET_SETTING_WINDOW_ID_RESPONSE
} from '~/main/utils/busApi/constants'
import server from '~/main/server/index'
const isDevelopment = process.env.NODE_ENV !== 'production'
protocol.registerSchemesAsPrivileged([{ scheme: 'picgo', privileges: { secure: true, standard: true } }])
beforeOpen()
let window: BrowserWindow | null
let settingWindow: BrowserWindow | null
let miniWindow: BrowserWindow | null
let tray: Tray | null
let menu: Menu | null
let contextMenu: Menu | null
const winURL = isDevelopment
? (process.env.WEBPACK_DEV_SERVER_URL as string)
: `picgo://./index.html`
const settingWinURL = isDevelopment
? `${(process.env.WEBPACK_DEV_SERVER_URL as string)}#setting/upload`
: `picgo://./index.html#setting/upload`
const miniWinURL = isDevelopment
? `${(process.env.WEBPACK_DEV_SERVER_URL as string)}#mini-page`
: `picgo://./index.html#mini-page`
// fix the $PATH in macOS
fixPath()
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 (settingWindow) {
settingWindow.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 () {
if (settingWindow === null) {
createSettingWindow()
settingWindow!.show()
} else {
settingWindow.show()
settingWindow.focus()
}
if (miniWindow) {
miniWindow.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: '退出'
}
])
}
function createTray () {
const menubarPic = process.platform === 'darwin' ? `${__static}/menubar.png` : `${__static}/menubar-nodarwin.png`
tray = new Tray(menubarPic)
tray.on('right-click', () => {
if (window) {
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
})
}
window!.webContents.send('clipboardFiles', obj)
}, 0)
} else {
if (window) {
window.hide()
}
if (settingWindow === null) {
createSettingWindow()
settingWindow!.show()
} else {
settingWindow.show()
settingWindow.focus()
}
if (miniWindow) {
miniWindow.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 imgs = await uploader.setWebContents(window!.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])
}
window!.webContents.send('dragFiles', imgs)
}
})
// toggleWindow()
}
const createWindow = () => {
if (process.platform !== 'darwin' && process.platform !== 'win32') {
return
}
window = new BrowserWindow({
height: 350,
width: 196, // 196
show: false,
frame: false,
fullscreenable: false,
resizable: false,
transparent: true,
vibrancy: 'ultra-dark',
webPreferences: {
nodeIntegration: true,
nodeIntegrationInWorker: true,
backgroundThrottling: false
}
})
window.loadURL(winURL)
window.on('closed', () => {
window = null
})
window.on('blur', () => {
window!.hide()
})
return window
}
const createMiniWindow = () => {
if (miniWindow || process.platform === 'darwin') {
return false
}
let obj: IBrowserWindowOptions = {
height: 64,
width: 64,
show: process.platform === 'linux',
frame: false,
fullscreenable: false,
skipTaskbar: true,
resizable: false,
transparent: process.platform !== 'linux',
icon: `${__static}/logo.png`,
webPreferences: {
backgroundThrottling: false,
nodeIntegration: true,
nodeIntegrationInWorker: true
}
}
if (db.get('settings.miniWindowOntop')) {
obj.alwaysOnTop = true
}
miniWindow = new BrowserWindow(obj)
miniWindow.loadURL(miniWinURL)
miniWindow.on('closed', () => {
miniWindow = null
})
return miniWindow
}
const createSettingWindow = () => {
const options: IBrowserWindowOptions = {
height: 450,
width: 800,
show: false,
frame: true,
center: true,
fullscreenable: false,
resizable: false,
title: 'PicGo',
vibrancy: 'ultra-dark',
transparent: true,
titleBarStyle: 'hidden',
webPreferences: {
backgroundThrottling: false,
nodeIntegration: true,
nodeIntegrationInWorker: true,
webSecurity: false
}
}
if (process.platform !== 'darwin') {
options.show = false
options.frame = false
options.backgroundColor = '#3f3c37'
options.transparent = false
options.icon = `${__static}/logo.png`
}
settingWindow = new BrowserWindow(options)
settingWindow!.loadURL(settingWinURL)
settingWindow!.on('closed', () => {
bus.emit('toggleShortKeyModifiedMode', false)
settingWindow = null
if (process.platform === 'linux') {
process.nextTick(() => {
app.quit()
})
}
})
createMenu()
createMiniWindow()
return settingWindow
}
const 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) => {
if (window!.isVisible()) {
window!.hide()
} else {
showWindow(bounds)
}
}
const showWindow = (bounds: IBounds) => {
window!.setPosition(bounds.x - 98 + 11, bounds.y, false)
window!.webContents.send('updateFiles')
window!.show()
window!.focus()
}
const uploadClipboardFiles = async (): Promise<string> => {
const win = getAvailableWindow()
let img = await uploader.setWebContents(win!.webContents).upload()
if (img !== false) {
if (img.length > 0) {
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
clipboard.writeText(pasteTemplate(pasteStyle, img[0]))
const notification = new Notification({
title: '上传成功',
body: img[0].imgUrl!,
icon: img[0].imgUrl
})
notification.show()
db.insert('uploaded', img[0])
window!.webContents.send('clipboardFiles', [])
window!.webContents.send('uploadFiles', img)
if (settingWindow) {
settingWindow.webContents.send('updateGallery')
}
return img[0].imgUrl as string
} else {
const notification = new Notification({
title: '上传不成功',
body: '你剪贴板最新的一条记录不是图片哦'
})
notification.show()
return ''
}
} else {
return ''
}
}
const uploadChoosedFiles = async (webContents: WebContents, files: IFileWithPath[]): Promise<string[]> => {
const input = files.map(item => item.path)
const imgs = await uploader.setWebContents(webContents).upload(input)
const result = []
if (imgs !== false) {
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
let pasteText = ''
for (let i = 0; i < imgs.length; i++) {
pasteText += pasteTemplate(pasteStyle, imgs[i]) + '\r\n'
const notification = new Notification({
title: '上传成功',
body: imgs[i].imgUrl!,
icon: files[i].path
})
setTimeout(() => {
notification.show()
}, i * 100)
db.insert('uploaded', imgs[i])
result.push(imgs[i].imgUrl!)
}
clipboard.writeText(pasteText)
window!.webContents.send('uploadFiles', imgs)
if (settingWindow) {
settingWindow.webContents.send('updateGallery')
}
return result
} else {
return []
}
}
picgoCoreIPC()
// from macOS tray
ipcMain.on('uploadClipboardFiles', async () => {
const img = await uploader.setWebContents(window!.webContents).upload()
if (img !== false) {
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
clipboard.writeText(pasteTemplate(pasteStyle, img[0]))
const notification = new Notification({
title: '上传成功',
body: img[0].imgUrl!,
// icon: file[0]
icon: img[0].imgUrl
})
notification.show()
db.insert('uploaded', img[0])
window!.webContents.send('clipboardFiles', [])
if (settingWindow) {
settingWindow.webContents.send('updateGallery')
}
}
window!.webContents.send('uploadFiles')
})
ipcMain.on('uploadClipboardFilesFromUploadPage', () => {
uploadClipboardFiles()
})
ipcMain.on('uploadChoosedFiles', async (evt: IpcMainEvent, files: IFileWithPath[]) => {
return uploadChoosedFiles(evt.sender, files)
})
ipcMain.on('updateShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, oldKey: string, from: string) => {
const result = shortKeyHandler.updateShortKey(item, oldKey, from)
evt.sender.send('updateShortKeyResponse', result)
if (result) {
const notification = new Notification({
title: '操作成功',
body: '你的快捷键已经修改成功'
})
notification.show()
} else {
const notification = new Notification({
title: '操作失败',
body: '快捷键冲突,请重新设置'
})
notification.show()
}
})
ipcMain.on('bindOrUnbindShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, from: string) => {
const result = shortKeyHandler.bindOrUnbindShortKey(item, from)
if (result) {
const notification = new Notification({
title: '操作成功',
body: '你的快捷键已经修改成功'
})
notification.show()
} else {
const notification = new Notification({
title: '操作失败',
body: '快捷键冲突,请重新设置'
})
notification.show()
}
})
ipcMain.on('updateCustomLink', () => {
const notification = new Notification({
title: '操作成功',
body: '你的自定义链接格式已经修改成功'
})
notification.show()
})
ipcMain.on('autoStart', (evt: IpcMainEvent, val: boolean) => {
app.setLoginItemSettings({
openAtLogin: val
})
})
ipcMain.on('openSettingWindow', () => {
if (!settingWindow) {
createSettingWindow()
} else {
settingWindow.show()
}
if (miniWindow) {
miniWindow.hide()
}
})
ipcMain.on('openMiniWindow', () => {
if (!miniWindow) {
createMiniWindow()
}
miniWindow!.show()
miniWindow!.focus()
settingWindow!.hide()
})
// from mini window
ipcMain.on('syncPicBed', () => {
if (settingWindow) {
settingWindow.webContents.send('syncPicBed')
}
})
ipcMain.on('getPicBeds', (evt: IpcMainEvent) => {
const picBeds = getPicBeds()
evt.sender.send('getPicBeds', picBeds)
evt.returnValue = picBeds
})
ipcMain.on('toggleShortKeyModifiedMode', (evt: IpcMainEvent, val: boolean) => {
bus.emit('toggleShortKeyModifiedMode', val)
})
ipcMain.on('updateServer', () => {
server.restart()
})
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 = getAvailableWindow()
uploadChoosedFiles(win.webContents, files)
}
} else {
if (settingWindow) {
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())
}
}
createWindow()
createSettingWindow()
if (process.platform === 'darwin' || process.platform === 'win32') {
createTray()
}
db.set('needReload', false)
updateChecker()
initEventCenter()
// 不需要阻塞
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 = getAvailableWindow()
uploadChoosedFiles(win.webContents, files)
}
}
}
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
createProtocol('picgo')
if (window === null) {
createWindow()
}
if (settingWindow === null) {
createSettingWindow()
}
})
app.on('will-quit', () => {
globalShortcut.unregisterAll()
bus.removeAllListeners()
server.shutdown()
})
app.setLoginItemSettings({
openAtLogin: db.get('settings.autoStart') || false
})
function initEventCenter () {
const eventList: any = {
'picgo:upload': uploadClipboardFiles,
[UPLOAD_WITH_CLIPBOARD_FILES]: busCallUploadClipboardFiles,
[UPLOAD_WITH_FILES]: busCallUploadFiles,
[GET_WINDOW_ID]: busCallGetWindowId,
[GET_SETTING_WINDOW_ID]: busCallGetSettingWindowId
}
for (let i in eventList) {
bus.on(i, eventList[i])
}
}
function getAvailableWindow () {
let win
if (miniWindow && miniWindow.isVisible()) {
win = miniWindow
} else {
win = settingWindow || window || createSettingWindow()
}
return win
}
async function busCallUploadClipboardFiles () {
const imgUrl = await uploadClipboardFiles()
bus.emit(UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE, imgUrl)
}
async function busCallUploadFiles (pathList: IFileWithPath[]) {
const win = getAvailableWindow()
const urls = await uploadChoosedFiles(win.webContents, pathList)
bus.emit(UPLOAD_WITH_FILES_RESPONSE, urls)
}
function busCallGetWindowId () {
const win = getAvailableWindow()
bus.emit(GET_WINDOW_ID_REPONSE, win.id)
}
function busCallGetSettingWindowId () {
if (!settingWindow) createSettingWindow()
bus.emit(GET_SETTING_WINDOW_ID_RESPONSE, settingWindow!.id)
}
// 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()
})
}
}
/**
* Auto Updater
+3 -7
View File
@@ -1,15 +1,13 @@
import Vue from 'vue'
import App from './renderer/App.vue'
import router from './renderer/router'
import db from '#/datastore/index'
import ElementUI from 'element-ui'
import { webFrame } from 'electron'
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'
import { initTalkingData } from './renderer/utils/analytics'
import db from './renderer/utils/db'
webFrame.setVisualZoomLevelLimits(1, 1)
webFrame.setLayoutZoomLevelLimits(0, 0)
@@ -17,6 +15,7 @@ webFrame.setLayoutZoomLevelLimits(0, 0)
Vue.config.productionTip = false
Vue.prototype.$builtInPicBed = [
'smms',
'weibo',
'imgur',
'qiniu',
'tcyun',
@@ -24,9 +23,8 @@ Vue.prototype.$builtInPicBed = [
'aliyun',
'github'
]
Vue.prototype.$$db = db
Vue.prototype.$db = db
Vue.prototype.$http = axios
Vue.prototype.$bus = bus
Vue.use(ElementUI)
Vue.use(VueLazyLoad)
@@ -36,5 +34,3 @@ new Vue({
router,
render: h => h(App)
}).$mount('#app')
initTalkingData()
-13
View File
@@ -1,13 +0,0 @@
# 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.
-276
View File
@@ -1,276 +0,0 @@
import {
app,
Menu,
Tray,
dialog,
clipboard,
systemPreferences,
Notification
} from 'electron'
import uploader from 'apis/app/uploader'
import getPicBeds from '~/main/utils/getPicBeds'
import db, { GalleryDB } from '~/main/apis/core/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'
import { privacyManager } from '~/main/utils/privacyManager'
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 () {
privacyManager.show(false)
}
},
{
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`)
})
// drop-files only be supported in macOS
// so the tray window must be available
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], db.get('settings.customLink')))
const notification = new Notification({
title: '上传成功',
body: imgs[i].imgUrl!,
icon: files[i]
})
setTimeout(() => {
notification.show()
}, i * 100)
await GalleryDB.getInstance().insert(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()
}
}
-77
View File
@@ -1,77 +0,0 @@
import {
Notification,
WebContents
} from 'electron'
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from 'apis/app/window/constants'
import uploader from '.'
import pasteTemplate from '#/utils/pasteTemplate'
import db, { GalleryDB } from '~/main/apis/core/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()
if (img !== false) {
if (img.length > 0) {
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
handleCopyUrl(pasteTemplate(pasteStyle, img[0], db.get('settings.customLink')))
const notification = new Notification({
title: '上传成功',
body: img[0].imgUrl!,
icon: img[0].imgUrl
})
notification.show()
await GalleryDB.getInstance().insert(img[0])
// trayWindow just be created in mac/windows, not in linux
trayWindow?.webContents?.send('clipboardFiles', [])
trayWindow?.webContents?.send('uploadFiles', img)
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('updateGallery')
}
return handleUrlEncode(img[0].imgUrl as string)
} else {
const notification = new Notification({
title: '上传不成功',
body: '你剪贴板最新的一条记录不是图片哦'
})
notification.show()
return ''
}
} else {
return ''
}
}
export const uploadChoosedFiles = async (webContents: WebContents, files: IFileWithPath[]): Promise<string[]> => {
const input = files.map(item => item.path)
const imgs = await uploader.setWebContents(webContents).upload(input)
const result = []
if (imgs !== false) {
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
const pasteText: string[] = []
for (let i = 0; i < imgs.length; i++) {
pasteText.push(pasteTemplate(pasteStyle, imgs[i], db.get('settings.customLink')))
const notification = new Notification({
title: '上传成功',
body: imgs[i].imgUrl!,
icon: files[i].path
})
setTimeout(() => {
notification.show()
}, i * 100)
await GalleryDB.getInstance().insert(imgs[i])
result.push(handleUrlEncode(imgs[i].imgUrl!))
}
handleCopyUrl(pasteText.join('\n'))
// trayWindow just be created in mac/windows, not in linux
windowManager.get(IWindowList.TRAY_WINDOW)?.webContents?.send('uploadFiles', imgs)
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('updateGallery')
}
return result
} else {
return []
}
}
-142
View File
@@ -1,142 +0,0 @@
import {
Notification,
BrowserWindow,
ipcMain,
WebContents
} from 'electron'
import dayjs from 'dayjs'
import picgo from '@core/picgo'
import db from '~/main/apis/core/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, calcDurationRange } from '~/main/utils/common'
import { TALKING_DATA_EVENT } from '~/universal/events/constants'
import logger from '@core/picgo/logger'
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)
})
})
}
const handleTalkingData = (webContents: WebContents, options: IAnalyticsData) => {
const data: ITalkingDataOptions = {
EventId: 'upload',
Label: options.type,
MapKv: {
by: options.fromClipboard ? 'clipboard' : 'files', // 上传剪贴板图片还是选择的文文件
count: options.count, // 上传的数量
duration: calcDurationRange(options.duration || 0), // 上传耗时
type: options.type
}
}
webContents.send(TALKING_DATA_EVENT, data)
}
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, 'ms').format('YYYYMMDDHHmmSSS') + 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
}
async upload (img?: IUploadOption): Promise<ImgInfo[]|false> {
try {
const startTime = Date.now()
const output = await picgo.upload(img)
if (Array.isArray(output) && output.some((item: ImgInfo) => item.imgUrl)) {
if (this.webContents) {
handleTalkingData(this.webContents, {
fromClipboard: !img,
type: db.get('picBed.uploader') || db.get('picBed.current') || 'smms',
count: img ? img.length : 1,
duration: Date.now() - startTime
} as IAnalyticsData)
}
return output.filter(item => item.imgUrl)
} else {
return false
}
} catch (e) {
logger.error(e)
setTimeout(() => {
showNotification({
title: '上传失败',
body: util.format(e.stack),
clickToCopy: true
})
}, 500)
return false
}
}
}
export default new Uploader()
-24
View File
@@ -1,24 +0,0 @@
export enum IWindowList {
SETTING_WINDOW = 'SETTING_WINDOW',
TRAY_WINDOW = 'TRAY_WINDOW',
MINI_WINDOW = 'MINI_WINDOW',
RENAME_WINDOW = 'RENAME_WINDOW'
}
const isDevelopment = process.env.NODE_ENV !== 'production'
export const TRAY_WINDOW_URL = isDevelopment
? (process.env.WEBPACK_DEV_SERVER_URL as string)
: `picgo://./index.html`
export const SETTING_WINDOW_URL = isDevelopment
? `${(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`
: `picgo://./index.html#mini-page`
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`
-167
View File
@@ -1,167 +0,0 @@
import {
IWindowList,
SETTING_WINDOW_URL,
TRAY_WINDOW_URL,
MINI_WINDOW_URL,
RENAME_WINDOW_URL
} from './constants'
import { IWindowListItem } from '#/types/electron'
import bus from '@core/bus'
import { CREATE_APP_MENU } from '@core/bus/constants'
import db from '~/main/apis/core/datastore'
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
import { app } from 'electron'
const windowList = new Map<IWindowList, IWindowListItem>()
windowList.set(IWindowList.TRAY_WINDOW, {
isValid: process.platform !== 'linux',
multiple: false,
options () {
return {
height: 350,
width: 196, // 196
show: false,
frame: false,
fullscreenable: false,
resizable: false,
transparent: true,
vibrancy: 'ultra-dark',
webPreferences: {
nodeIntegration: true,
nodeIntegrationInWorker: true,
backgroundThrottling: false
}
}
},
callback (window) {
window.loadURL(TRAY_WINDOW_URL)
window.on('blur', () => {
window.hide()
})
}
})
windowList.set(IWindowList.SETTING_WINDOW, {
isValid: true,
multiple: false,
options () {
const options: IBrowserWindowOptions = {
height: 450,
width: 800,
show: false,
frame: true,
center: true,
fullscreenable: false,
resizable: false,
title: 'PicGo',
vibrancy: 'ultra-dark',
transparent: true,
titleBarStyle: 'hidden',
webPreferences: {
backgroundThrottling: false,
nodeIntegration: true,
nodeIntegrationInWorker: true,
webSecurity: false
}
}
if (process.platform !== 'darwin') {
options.show = false
options.frame = false
options.backgroundColor = '#3f3c37'
options.transparent = false
options.icon = `${__static}/logo.png`
}
return options
},
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()
})
}
})
bus.emit(CREATE_APP_MENU)
windowManager.create(IWindowList.MINI_WINDOW)
}
})
windowList.set(IWindowList.MINI_WINDOW, {
isValid: process.platform !== 'darwin',
multiple: false,
options () {
let obj: IBrowserWindowOptions = {
height: 64,
width: 64,
show: process.platform === 'linux',
frame: false,
fullscreenable: false,
skipTaskbar: true,
resizable: false,
transparent: process.platform !== 'linux',
icon: `${__static}/logo.png`,
webPreferences: {
backgroundThrottling: false,
nodeIntegration: true,
nodeIntegrationInWorker: true
}
}
if (db.get('settings.miniWindowOntop')) {
obj.alwaysOnTop = true
}
return obj
},
callback (window) {
window.loadURL(MINI_WINDOW_URL)
}
})
windowList.set(IWindowList.RENAME_WINDOW, {
isValid: true,
multiple: true,
options () {
let options: IBrowserWindowOptions = {
height: 175,
width: 300,
show: true,
fullscreenable: false,
resizable: false,
vibrancy: 'ultra-dark',
webPreferences: {
nodeIntegration: true,
nodeIntegrationInWorker: true,
backgroundThrottling: false
}
}
if (process.platform !== 'darwin') {
options.show = true
options.backgroundColor = '#3f3c37'
options.autoHideMenuBar = true
options.transparent = false
}
return options
},
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()
const positionX = bounds.x + bounds.width / 2 - 150
let positionY
// if is the settingWindow
if (bounds.height > 400) {
positionY = bounds.y + bounds.height / 2 - 88
} else { // if is the miniWindow
positionY = bounds.y + bounds.height / 2
}
window.setPosition(positionX, positionY, false)
}
}
})
export default windowList
-75
View File
@@ -1,75 +0,0 @@
import {
BrowserWindow
} from 'electron'
import { IWindowManager, IWindowListItem } from '#/types/electron'
import windowList from './windowList'
import {
IWindowList
} from './constants'
class WindowManager implements IWindowManager {
private windowMap: Map<IWindowList | string, BrowserWindow> = new Map()
private windowIdMap: Map<number, IWindowList | string> = new Map()
create (name: IWindowList) {
const windowConfig: IWindowListItem = windowList.get(name)!
if (windowConfig.isValid) {
if (!windowConfig.multiple) {
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}`)
} else {
this.windowMap.set(name, window)
this.windowIdMap.set(window.id, name)
}
windowConfig.callback(window, this)
window.on('close', () => {
this.deleteById(id)
})
return window
} else {
return null
}
}
get (name: IWindowList) {
if (this.has(name)) {
return this.windowMap.get(name)!
} else {
const window = this.create(name)
return window
}
}
has (name: IWindowList) {
return this.windowMap.has(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) {
this.windowMap.delete(name)
this.windowIdMap.delete(id)
}
}
getAvailableWindow () {
const miniWindow = this.windowMap.get(IWindowList.MINI_WINDOW)
if (miniWindow && miniWindow.isVisible()) {
return miniWindow
} else {
const settingWindow = this.windowMap.get(IWindowList.SETTING_WINDOW)
const trayWindow = this.windowMap.get(IWindowList.TRAY_WINDOW)
return settingWindow || trayWindow || this.create(IWindowList.SETTING_WINDOW)!
}
}
}
export default new WindowManager()
-132
View File
@@ -1,132 +0,0 @@
import fs from 'fs-extra'
import path from 'path'
import { remote, app } from 'electron'
import dayjs from 'dayjs'
const APP = process.type === 'renderer' ? remote.app : app
const STORE_PATH = APP.getPath('userData')
const configFilePath = path.join(STORE_PATH, 'data.json')
const configFileBackupPath = path.join(STORE_PATH, 'data.bak.json')
export const defaultConfigPath = configFilePath
let _configFilePath = ''
let hasCheckPath = false
const errorMsg = {
broken: 'PicGo 配置文件损坏,已经恢复为默认配置',
brokenButBackup: 'PicGo 配置文件损坏,已经恢复为备份配置'
}
function dbChecker () {
if (process.type !== 'renderer') {
if (!global.notificationList) global.notificationList = []
// db save bak
try {
const { dbPath, dbBackupPath } = getGalleryDBPath()
if (fs.existsSync(dbPath)) {
fs.copyFileSync(dbPath, dbBackupPath)
}
} catch (e) {
console.error(e)
}
const configFilePath = dbPathChecker()
if (!fs.existsSync(configFilePath)) {
return
}
let configFile: string = '{}'
let optionsTpl = {
title: '注意',
body: ''
}
// config save bak
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' })
}
}
/**
* Get config path
*/
function dbPathChecker (): string {
if (_configFilePath) {
return _configFilePath
}
// defaultConfigPath
_configFilePath = defaultConfigPath
// if defaultConfig path is not exit
// do not parse the content of config
if (!fs.existsSync(defaultConfigPath)) {
return _configFilePath
}
try {
const configString = fs.readFileSync(defaultConfigPath, { encoding: 'utf-8' })
const config = JSON.parse(configString)
const userConfigPath: string = config.configPath || ''
if (userConfigPath) {
if (fs.existsSync(userConfigPath) && userConfigPath.endsWith('.json')) {
_configFilePath = userConfigPath
return _configFilePath
}
}
return _configFilePath
} catch (e) {
// TODO: local logger is needed
if (!hasCheckPath) {
let optionsTpl = {
title: '注意',
body: '自定义文件解析出错,请检查路径内容是否正确'
}
global.notificationList.push(optionsTpl)
hasCheckPath = true
}
console.error(e)
_configFilePath = defaultConfigPath
return _configFilePath
}
}
function dbPathDir () {
return path.dirname(dbPathChecker())
}
function getGalleryDBPath (): {
dbPath: string
dbBackupPath: string
} {
const configPath = dbPathChecker()
const dbPath = path.join(path.dirname(configPath), 'picgo.db')
const dbBackupPath = path.join(path.dirname(dbPath), 'picgo.bak.db')
return {
dbPath,
dbBackupPath
}
}
export {
dbChecker,
dbPathChecker,
dbPathDir,
getGalleryDBPath
}
-95
View File
@@ -1,95 +0,0 @@
import Datastore from 'lowdb'
// @ts-ignore
import LodashId from 'lodash-id'
import FileSync from 'lowdb/adapters/FileSync'
import fs from 'fs-extra'
import { dbPathChecker, dbPathDir, getGalleryDBPath } from './dbChecker'
import { DBStore } from '@picgo/store'
const STORE_PATH = dbPathDir()
if (!fs.pathExistsSync(STORE_PATH)) {
fs.mkdirpSync(STORE_PATH)
}
const CONFIG_PATH: string = dbPathChecker()
const DB_PATH: string = getGalleryDBPath().dbPath
// TODO: use JSONStore with @picgo/store
class ConfigStore {
private db: Datastore.LowdbSync<Datastore.AdapterSync>
constructor () {
const adapter = new FileSync(CONFIG_PATH)
this.db = Datastore(adapter)
this.db._.mixin(LodashId)
if (!this.db.has('picBed').value()) {
this.db.set('picBed', {
current: 'smms', // deprecated
uploader: 'smms',
smms: {
token: ''
}
}).write()
}
if (!this.db.has('settings.shortKey').value()) {
this.db.set('settings.shortKey[picgo:upload]', {
enable: true,
key: 'CommandOrControl+Shift+P',
name: 'upload',
label: '快捷上传'
}).write()
}
}
read () {
return this.db.read()
}
get (key = '') {
return this.read().get(key).value()
}
set (key: string, value: any) {
return this.read().set(key, value).write()
}
has (key: string) {
return this.read().has(key).value()
}
insert (key: string, value: any): void {
// @ts-ignore
return this.read().get(key).insert(value).write()
}
unset (key: string, value: any): boolean {
return this.read().get(key).unset(value).value()
}
getById (key: string, id: string) {
// @ts-ignore
return this.read().get(key).getById(id).value()
}
removeById (key: string, id: string) {
// @ts-ignore
return this.read().get(key).removeById(id).write()
}
getConfigPath () {
return CONFIG_PATH
}
}
export default new ConfigStore()
// v2.3.0 add gallery db
class GalleryDB {
private static instance: DBStore
private constructor () {
console.log('init gallery db')
}
public static getInstance (): DBStore {
if (!GalleryDB.instance) {
GalleryDB.instance = new DBStore(DB_PATH, 'gallery')
}
return GalleryDB.instance
}
}
export {
GalleryDB
}
-5
View File
@@ -1,5 +0,0 @@
import picgo from '.'
const logger = picgo.log
export default logger
-61
View File
@@ -1,61 +0,0 @@
import bus from '@core/bus'
import {
uploadClipboardFiles,
uploadChoosedFiles
} from 'apis/app/uploader/apis'
import {
createMenu
} 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,
UPLOAD_WITH_CLIPBOARD_FILES,
UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE,
GET_WINDOW_ID,
GET_WINDOW_ID_REPONSE,
GET_SETTING_WINDOW_ID,
GET_SETTING_WINDOW_ID_RESPONSE,
CREATE_APP_MENU
} from '@core/bus/constants'
function initEventCenter () {
const eventList: any = {
'picgo:upload': uploadClipboardFiles,
[UPLOAD_WITH_CLIPBOARD_FILES]: busCallUploadClipboardFiles,
[UPLOAD_WITH_FILES]: busCallUploadFiles,
[GET_WINDOW_ID]: busCallGetWindowId,
[GET_SETTING_WINDOW_ID]: busCallGetSettingWindowId,
[CREATE_APP_MENU]: createMenu
}
for (let i in eventList) {
bus.on(i, eventList[i])
}
}
async function busCallUploadClipboardFiles () {
const imgUrl = await uploadClipboardFiles()
bus.emit(UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE, imgUrl)
}
async function busCallUploadFiles (pathList: IFileWithPath[]) {
const win = windowManager.getAvailableWindow()
const urls = await uploadChoosedFiles(win.webContents, pathList)
bus.emit(UPLOAD_WITH_FILES_RESPONSE, urls)
}
function busCallGetWindowId () {
const win = windowManager.getAvailableWindow()
bus.emit(GET_WINDOW_ID_REPONSE, win.id)
}
function busCallGetSettingWindowId () {
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
bus.emit(GET_SETTING_WINDOW_ID_RESPONSE, settingWindow.id)
}
export default {
listen () {
initEventCenter()
}
}
-146
View File
@@ -1,146 +0,0 @@
import {
app,
ipcMain,
Notification,
IpcMainEvent
} from 'electron'
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, { GalleryDB } from '~/main/apis/core/datastore'
import server from '~/main/server'
import getPicBeds from '~/main/utils/getPicBeds'
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/app/uploader/apis'
import picgoCoreIPC from './picgoCoreIPC'
import { handleCopyUrl } from '~/main/utils/common'
export default {
listen () {
picgoCoreIPC.listen()
// from macOS tray
ipcMain.on('uploadClipboardFiles', async () => {
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!
const img = await uploader.setWebContents(trayWindow.webContents).upload()
if (img !== false) {
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
handleCopyUrl(pasteTemplate(pasteStyle, img[0], db.get('settings.customLink')))
const notification = new Notification({
title: '上传成功',
body: img[0].imgUrl!,
// icon: file[0]
icon: img[0].imgUrl
})
notification.show()
await GalleryDB.getInstance().insert(img[0])
trayWindow.webContents.send('clipboardFiles', [])
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('updateGallery')
}
}
trayWindow.webContents.send('uploadFiles')
})
ipcMain.on('uploadClipboardFilesFromUploadPage', () => {
uploadClipboardFiles()
})
ipcMain.on('uploadChoosedFiles', async (evt: IpcMainEvent, files: IFileWithPath[]) => {
return uploadChoosedFiles(evt.sender, files)
})
ipcMain.on('updateShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, oldKey: string, from: string) => {
const result = shortKeyHandler.updateShortKey(item, oldKey, from)
evt.sender.send('updateShortKeyResponse', result)
if (result) {
const notification = new Notification({
title: '操作成功',
body: '你的快捷键已经修改成功'
})
notification.show()
} else {
const notification = new Notification({
title: '操作失败',
body: '快捷键冲突,请重新设置'
})
notification.show()
}
})
ipcMain.on('bindOrUnbindShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, from: string) => {
const result = shortKeyHandler.bindOrUnbindShortKey(item, from)
if (result) {
const notification = new Notification({
title: '操作成功',
body: '你的快捷键已经修改成功'
})
notification.show()
} else {
const notification = new Notification({
title: '操作失败',
body: '快捷键冲突,请重新设置'
})
notification.show()
}
})
ipcMain.on('updateCustomLink', () => {
const notification = new Notification({
title: '操作成功',
body: '你的自定义链接格式已经修改成功'
})
notification.show()
})
ipcMain.on('autoStart', (evt: IpcMainEvent, val: boolean) => {
app.setLoginItemSettings({
openAtLogin: val
})
})
ipcMain.on('openSettingWindow', () => {
windowManager.get(IWindowList.SETTING_WINDOW)!.show()
if (windowManager.has(IWindowList.MINI_WINDOW)) {
windowManager.get(IWindowList.MINI_WINDOW)!.hide()
}
})
ipcMain.on('openMiniWindow', () => {
const miniWindow = windowManager.get(IWindowList.MINI_WINDOW)!
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
miniWindow.show()
miniWindow.focus()
settingWindow.hide()
})
// from mini window
ipcMain.on('syncPicBed', () => {
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')
}
})
ipcMain.on('getPicBeds', (evt: IpcMainEvent) => {
const picBeds = getPicBeds()
evt.sender.send('getPicBeds', picBeds)
evt.returnValue = picBeds
})
ipcMain.on(TOGGLE_SHORTKEY_MODIFIED_MODE, (evt: IpcMainEvent, val: boolean) => {
bus.emit(TOGGLE_SHORTKEY_MODIFIED_MODE, val)
})
ipcMain.on('updateServer', () => {
server.restart()
})
},
dispose () {}
}
-346
View File
@@ -1,346 +0,0 @@
import path from 'path'
import GuiApi from 'apis/gui'
import {
dialog,
shell,
IpcMainEvent,
ipcMain
} 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'
import { dbPathChecker } from 'apis/core/datastore/dbChecker'
import {
PICGO_SAVE_CONFIG,
PICGO_GET_CONFIG,
PICGO_GET_DB,
PICGO_INSERT_DB,
PICGO_INSERT_MANY_DB,
PICGO_UPDATE_BY_ID_DB,
PICGO_GET_BY_ID_DB,
PICGO_REMOVE_BY_ID_DB,
PICGO_OPEN_FILE
} from '#/events/constants'
import { GalleryDB } from 'apis/core/datastore'
import { IObject, IFilter } from '@picgo/store/dist/types'
// 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 = path.dirname(dbPathChecker())
// 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, fullName: string) => {
const dispose = handleNPMError()
const res = await picgo.pluginHandler.install([fullName])
event.sender.send('installPlugin', {
success: res.success,
body: fullName,
errMsg: res.success ? '' : res.body
})
if (res.success) {
shortKeyHandler.registerPluginShortKey(res.body[0])
} else {
showNotification({
title: '插件安装失败',
body: res.body as string
})
}
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])
} else {
showNotification({
title: '插件卸载失败',
body: res.body as string
})
}
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])
} else {
showNotification({
title: '插件更新失败',
body: res.body as string
})
}
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)
if (plugin?.guiMenu?.(picgo)?.length) {
const menu: GuiMenuItem[] = plugin.guiMenu(picgo)
menu.forEach(item => {
if (item.label === label) {
item.handle(picgo, GuiApi.getInstance())
}
})
}
})
}
const handleRemoveFiles = () => {
ipcMain.on('removeFiles', (event: IpcMainEvent, files: ImgInfo[]) => {
setTimeout(() => {
picgo.emit('remove', files, GuiApi.getInstance())
}, 500)
})
}
const handlePicGoSaveConfig = () => {
ipcMain.on(PICGO_SAVE_CONFIG, (event: IpcMainEvent, data: IObj) => {
picgo.saveConfig(data)
})
}
const handlePicGoGetConfig = () => {
ipcMain.on(PICGO_GET_CONFIG, (event: IpcMainEvent, key: string | undefined, callbackId: string) => {
const result = picgo.getConfig(key)
event.sender.send(PICGO_GET_CONFIG, result, callbackId)
})
}
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')
})
})
}
const handlePicGoGalleryDB = () => {
ipcMain.on(PICGO_GET_DB, async (event: IpcMainEvent, filter: IFilter, callbackId: string) => {
const dbStore = GalleryDB.getInstance()
const res = await dbStore.get(filter)
event.sender.send(PICGO_GET_DB, res, callbackId)
})
ipcMain.on(PICGO_INSERT_DB, async (event: IpcMainEvent, value: IObject, callbackId: string) => {
const dbStore = GalleryDB.getInstance()
const res = await dbStore.insert(value)
event.sender.send(PICGO_INSERT_DB, res, callbackId)
})
ipcMain.on(PICGO_INSERT_MANY_DB, async (event: IpcMainEvent, value: IObject[], callbackId: string) => {
const dbStore = GalleryDB.getInstance()
const res = await dbStore.insertMany(value)
event.sender.send(PICGO_INSERT_MANY_DB, res, callbackId)
})
ipcMain.on(PICGO_UPDATE_BY_ID_DB, async (event: IpcMainEvent, id: string, value: IObject[], callbackId: string) => {
const dbStore = GalleryDB.getInstance()
const res = await dbStore.updateById(id, value)
event.sender.send(PICGO_UPDATE_BY_ID_DB, res, callbackId)
})
ipcMain.on(PICGO_GET_BY_ID_DB, async (event: IpcMainEvent, id: string, callbackId: string) => {
const dbStore = GalleryDB.getInstance()
const res = await dbStore.getById(id)
event.sender.send(PICGO_GET_BY_ID_DB, res, callbackId)
})
ipcMain.on(PICGO_REMOVE_BY_ID_DB, async (event: IpcMainEvent, id: string, callbackId: string) => {
const dbStore = GalleryDB.getInstance()
const res = await dbStore.removeById(id)
event.sender.send(PICGO_REMOVE_BY_ID_DB, res, callbackId)
})
}
const handleOpenFile = () => {
ipcMain.on(PICGO_OPEN_FILE, (event: IpcMainEvent, fileName: string) => {
const abFilePath = path.join(STORE_PATH, fileName)
shell.openItem(abFilePath)
})
}
export default {
listen () {
handleGetPluginList()
handlePluginInstall()
handlePluginUninstall()
handlePluginUpdate()
handleGetPicBedConfig()
handlePluginActions()
handleRemoveFiles()
handlePicGoSaveConfig()
handlePicGoGetConfig()
handlePicGoGalleryDB()
handleImportLocalPlugin()
handleOpenFile()
}
}
-33
View File
@@ -1,33 +0,0 @@
import fse from 'fs-extra'
import path from 'path'
import dayjs from 'dayjs'
import util from 'util'
import { dbPathDir } from 'apis/core/datastore/dbChecker'
const STORE_PATH = dbPathDir()
const LOG_PATH = path.join(STORE_PATH, '/picgo.log')
// since the error may occur in picgo-core
// so we can't use the log from picgo
export const loggerWriter = (error: Error) => {
let log = `${dayjs().format('YYYY-MM-DD HH:mm:ss')} [PicGo ERROR] startup error`
if (error?.stack) {
log += `\n------Error Stack Begin------\n${util.format(error.stack)}\n-------Error Stack End-------\n`
} else {
const msg = JSON.stringify(error)
log += `${msg}\n`
}
fse.appendFileSync(LOG_PATH, log)
}
const handleProcessError = (error: Error) => {
console.error(error)
loggerWriter(error)
}
process.on('uncaughtException', error => {
handleProcessError(error)
})
process.on('unhandledRejection', (error: any) => {
handleProcessError(error)
})
-191
View File
@@ -1,191 +0,0 @@
import './errorHandler'
import {
app,
globalShortcut,
protocol,
Notification
} from 'electron'
import {
createProtocol
} from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
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,
migrateGalleryFromVersion230
} 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, { GalleryDB } from '~/main/apis/core/datastore'
import bus from '@core/bus'
import { privacyManager } from '~/main/utils/privacyManager'
import logger from 'apis/core/picgo/logger'
import picgo from 'apis/core/picgo'
const isDevelopment = process.env.NODE_ENV !== 'production'
const handleStartUpFiles = (argv: string[], cwd: string) => {
const files = getUploadFiles(argv, cwd, logger)
if (files === null || files.length > 0) { // 如果有文件列表作为参数,说明是命令行启动
if (files === null) {
logger.info('cli -> uploading file from clipboard')
uploadClipboardFiles()
} else {
logger.info('cli -> uploading files from cli', ...files.map(item => item.path))
const win = windowManager.getAvailableWindow()
uploadChoosedFiles(win.webContents, files)
}
return true
} else {
return false
}
}
class LifeCycle {
private async 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'))
await migrateGalleryFromVersion230(db, GalleryDB.getInstance(), picgo)
}
private onReady () {
const readyFunction = async () => {
console.log('on ready')
createProtocol('picgo')
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
try {
await installExtension(VUEJS_DEVTOOLS)
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString())
}
}
const res = await privacyManager.init()
if (!res) {
return app.quit()
}
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') {
handleStartUpFiles(process.argv, process.cwd())
}
if (global.notificationList?.length > 0) {
while (global.notificationList.length) {
const option = global.notificationList.pop()
const notice = new Notification(option!)
notice.show()
}
}
}
if (!app.isReady()) {
app.on('ready', readyFunction)
} else {
readyFunction()
}
}
private onRunning () {
app.on('second-instance', (event, commandLine, workingDirectory) => {
logger.info('detect second instance')
const result = handleStartUpFiles(commandLine, workingDirectory)
if (!result) {
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()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
})
}
}
}
async launchApp () {
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
} else {
await this.beforeReady()
this.onReady()
this.onRunning()
this.onQuit()
}
}
}
const bootstrap = new LifeCycle()
export {
bootstrap
}
-54
View File
@@ -1,54 +0,0 @@
import { DBStore } from '@picgo/store'
import ConfigStore from '~/main/apis/core/datastore'
import path from 'path'
import fse from 'fs-extra'
import PicGoCore from '#/types/picgo'
// from v2.1.2
const updateShortKeyFromVersion212 = (db: typeof ConfigStore, shortKeyConfig: IShortKeyConfigs | IOldShortKeyConfigs) => {
// #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) {
// @ts-ignore
shortKeyConfig['picgo:upload'] = {
enable: true,
key: shortKeyConfig.upload,
name: 'upload',
label: '快捷上传'
}
// @ts-ignore
delete shortKeyConfig.upload
db.set('settings.shortKey', shortKeyConfig)
return true
}
return false
}
const migrateGalleryFromVersion230 = async (configDB: typeof ConfigStore, galleryDB: DBStore, picgo: PicGoCore) => {
const originGallery: ImgInfo[] = configDB.get('uploaded')
const configPath = configDB.getConfigPath()
const configBakPath = path.join(path.dirname(configPath), 'config.bak.json')
// migrate gallery from config to gallery db
if (originGallery && originGallery?.length > 0) {
if (fse.existsSync(configBakPath)) {
fse.copyFileSync(configPath, configBakPath)
}
await galleryDB.insertMany(originGallery)
picgo.saveConfig({
uploaded: []
})
}
}
export {
updateShortKeyFromVersion212,
migrateGalleryFromVersion230
}
+26
View File
@@ -0,0 +1,26 @@
import DB from '#/datastore'
// from v2.1.2
const updateShortKeyFromVersion212 = (db: typeof DB, shortKeyConfig: IShortKeyConfigs | IOldShortKeyConfigs) => {
let needUpgrade = false
if (shortKeyConfig.upload) {
needUpgrade = true
// @ts-ignore
shortKeyConfig['picgo:upload'] = {
enable: true,
key: shortKeyConfig.upload,
name: 'upload',
label: '快捷上传'
}
delete shortKeyConfig.upload
}
if (needUpgrade) {
db.set('settings.shortKey', shortKeyConfig)
return shortKeyConfig
} else {
return false
}
}
export {
updateShortKeyFromVersion212
}
+17 -51
View File
@@ -1,45 +1,25 @@
import http from 'http'
import routers from './routerManager'
import {
handleResponse,
ensureHTTPLink
handleResponse
} from './utils'
import picgo from '@core/picgo'
import logger from '@core/picgo/logger'
import axios from 'axios'
import picgo from '~/main/utils/picgo'
import logger from '~/main/utils/logger'
class Server {
private httpServer: http.Server
private config: IServerConfig
constructor () {
let config = picgo.getConfig<IServerConfig>('settings.server')
const result = this.checkIfConfigIsValid(config)
if (result) {
this.config = config
} else {
config = {
port: 36677,
host: '127.0.0.1',
enable: true
}
this.config = config
picgo.saveConfig({
'settings.server': config
})
this.config = picgo.getConfig('settings.server') || {
port: 36677,
host: '127.0.0.1',
enable: true
}
this.httpServer = http.createServer(this.handleRequest)
}
private checkIfConfigIsValid (config: IObj | undefined) {
if (config && config.port && config.host && (config.enable !== undefined)) {
return true
} else {
return false
}
}
private handleRequest = (request: http.IncomingMessage, response: http.ServerResponse) => {
if (request.method === 'POST') {
if (!routers.getHandler(request.url!)) {
logger.warn(`[PicGo Server] don't support [${request.url}] url`)
handleResponse({
response,
statusCode: 404,
@@ -58,7 +38,6 @@ class Server {
try {
postObj = (body === '') ? {} : JSON.parse(body)
} catch (err) {
logger.error(`[PicGo Server]`, err)
return handleResponse({
response,
body: {
@@ -67,7 +46,6 @@ class Server {
}
})
}
logger.info(`[PicGo Server] get the request`, body)
const handler = routers.getHandler(request.url!)
handler!({
...postObj,
@@ -76,43 +54,31 @@ class Server {
})
}
} else {
logger.warn(`[PicGo Server] don't support [${request.method}] method`)
response.statusCode = 404
response.end()
}
}
// port as string is a bug
private listen = (port: number | string) => {
private listen = (port: number) => {
logger.info(`[PicGo Server] is listening at ${port}`)
if (typeof port === 'string') {
port = parseInt(port, 10)
}
this.httpServer.listen(port, this.config.host).on('error', async (err: ErrnoException) => {
this.httpServer.listen(port, this.config.host).on('error', (err: ErrnoException) => {
if (err.errno === 'EADDRINUSE') {
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)
}
logger.warn(`[PicGo Server] ${port} is busy, trying with port ${port + 1}`)
this.config.port += 1
picgo.saveConfig({
'settings.server.port': this.config.port
})
this.listen(this.config.port)
}
})
}
startup () {
console.log('startup', this.config.enable)
if (this.config.enable) {
this.listen(this.config.port)
}
}
shutdown (hasStarted?: boolean) {
shutdown () {
this.httpServer.close()
if (!hasStarted) {
logger.info('[PicGo Server] shutdown')
}
logger.info('[PicGo Server] shutdown')
}
restart () {
this.config = picgo.getConfig('settings.server')
+15 -44
View File
@@ -1,18 +1,14 @@
import router from './router'
import {
uploadWithClipboardFiles,
uploadWithFiles
} from '~/main/utils/busApi/index'
import {
handleResponse
} from './utils'
import logger from '@core/picgo/logger'
import windowManager from 'apis/app/window/windowManager'
import { uploadChoosedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis'
import path from 'path'
import { dbPathDir } from 'apis/core/datastore/dbChecker'
const STORE_PATH = dbPathDir()
const LOG_PATH = path.join(STORE_PATH, 'picgo.log')
import logger from '../utils/logger'
const errorMessage = `upload error. see ${LOG_PATH} for more detail.`
router.post('/upload', async ({
router.get('/upload', async ({
response,
list = []
} : {
@@ -22,50 +18,39 @@ router.post('/upload', async ({
try {
if (list.length === 0) {
// upload with clipboard
logger.info('[PicGo Server] upload clipboard file')
const res = await uploadClipboardFiles()
if (res) {
const res = await uploadWithClipboardFiles()
if (res.success) {
handleResponse({
response,
body: {
success: true,
result: res
result: [res.result]
}
})
} else {
handleResponse({
response,
body: {
success: false,
message: errorMessage
}
response
})
}
} else {
logger.info('[PicGo Server] upload files in list')
// upload with files
const pathList = list.map(item => {
return {
path: item
}
})
const win = windowManager.getAvailableWindow()
const res = await uploadChoosedFiles(win.webContents, pathList)
if (res.length) {
const res = await uploadWithFiles(pathList)
if (res.success) {
handleResponse({
response,
body: {
success: true,
result: res
result: res.result
}
})
} else {
handleResponse({
response,
body: {
success: false,
message: errorMessage
}
response
})
}
}
@@ -75,24 +60,10 @@ router.post('/upload', async ({
response,
body: {
success: false,
message: errorMessage
message: err
}
})
}
})
router.post('/heartbeat', async ({
response
} : {
response: IHttpResponse,
}) => {
handleResponse({
response,
body: {
success: true,
result: 'alive'
}
})
})
export default router
-11
View File
@@ -1,5 +1,3 @@
import logger from '@core/picgo/logger'
export const handleResponse = ({
response,
statusCode = 200,
@@ -15,16 +13,7 @@ export const handleResponse = ({
header?: IObj,
body?: any
}) => {
if (body?.success === false) {
logger.warn('[PicGo Server] upload failed, see picgo.log for more detail ↑')
}
response.writeHead(statusCode, header)
response.write(JSON.stringify(body))
response.end()
}
export const ensureHTTPLink = (url: string): string => {
return url.startsWith('http')
? url
: `http://${url}`
}
+9 -22
View File
@@ -1,19 +1,12 @@
import fs from 'fs-extra'
import path from 'path'
import os from 'os'
import pkg from 'root/package.json'
import { dbPathChecker } from 'apis/core/datastore/dbChecker'
import { remote, app } from 'electron'
const configPath = dbPathChecker()
const CONFIG_DIR = path.dirname(configPath)
function injectPicGoVersion () {
global.PICGO_GUI_VERSION = pkg.version
global.PICGO_CORE_VERSION = pkg.dependencies.picgo.replace('^', '')
}
const APP = process.type === 'renderer' ? remote.app : app
const STORE_PATH = APP.getPath('userData')
function beforeOpen () {
injectPicGoVersion()
if (process.platform === 'darwin') {
resolveMacWorkFlow()
}
@@ -41,7 +34,7 @@ function resolveMacWorkFlow () {
*/
function resolveClipboardImageGenerator () {
let clipboardFiles = getClipboardFiles()
if (!fs.pathExistsSync(path.join(CONFIG_DIR, 'windows10.ps1'))) {
if (!fs.pathExistsSync(path.join(STORE_PATH, 'windows10.ps1'))) {
clipboardFiles.forEach(item => {
fs.copyFileSync(item.origin, item.dest)
})
@@ -52,15 +45,10 @@ function resolveClipboardImageGenerator () {
}
function diffFilesAndUpdate (filePath1: string, filePath2: string) {
try {
let file1 = fs.existsSync(filePath1) && fs.readFileSync(filePath1)
let file2 = fs.existsSync(filePath1) && fs.readFileSync(filePath2)
let file1 = fs.readFileSync(filePath1)
let file2 = fs.readFileSync(filePath2)
if (!file1 || !file2 || !file1.equals(file2)) {
fs.copyFileSync(filePath1, filePath2)
}
} catch (e) {
console.error(e)
if (!file1.equals(file2)) {
fs.copyFileSync(filePath1, filePath2)
}
}
@@ -70,14 +58,13 @@ function resolveClipboardImageGenerator () {
'/linux.sh',
'/mac.applescript',
'/windows.ps1',
'/windows10.ps1',
'/wsl.sh'
'/windows10.ps1'
]
return files.map(item => {
return {
origin: path.join(__static, item),
dest: path.join(CONFIG_DIR, item)
dest: path.join(STORE_PATH, item)
}
})
}
@@ -6,4 +6,3 @@ export const UPLOAD_WITH_FILES = 'UPLOAD_WITH_FILES'
export const UPLOAD_WITH_FILES_RESPONSE = 'UPLOAD_WITH_FILES_RESPONSE'
export const UPLOAD_WITH_CLIPBOARD_FILES = 'UPLOAD_WITH_CLIPBOARD_FILES'
export const UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE = 'UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE'
export const CREATE_APP_MENU = 'CREATE_APP_MENU'
@@ -1,4 +1,4 @@
import bus from '.'
import bus from '../eventBus'
import {
UPLOAD_WITH_FILES,
UPLOAD_WITH_FILES_RESPONSE,
-71
View File
@@ -1,71 +0,0 @@
import db from '~/main/apis/core/datastore'
import { clipboard, Notification, dialog } 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,
icon: options.icon || undefined
})
const handleClick = () => {
if (options.clickToCopy) {
clipboard.writeText(options.body)
}
}
notification.once('click', handleClick)
notification.once('close', () => {
notification.removeListener('click', handleClick)
})
notification.show()
}
export const showMessageBox = (options: any) => {
return new Promise<IShowMessageBoxResult>(async (resolve, reject) => {
dialog.showMessageBox(
options
).then((res) => {
resolve({
result: res.response,
checkboxChecked: res.checkboxChecked
})
})
})
}
export const calcDurationRange = (duration: number) => {
if (duration < 1000) {
return 500
} else if (duration < 1500) {
return 1000
} else if (duration < 3000) {
return 2000
} else if (duration < 5000) {
return 3000
} else if (duration < 7000) {
return 5000
} else if (duration < 10000) {
return 8000
} else if (duration < 12000) {
return 10000
} else if (duration < 20000) {
return 15000
} else if (duration < 30000) {
return 20000
}
// max range
return 100000
}
+6 -4
View File
@@ -1,17 +1,19 @@
import picgo from '@core/picgo'
import picgo from './picgo'
const getPicBeds = () => {
const picBedTypes = picgo.helper.uploader.getIdList()
const picBedFromDB = picgo.getConfig<IPicBedType[]>('picBed.list') || []
const picBedFromDB = picgo.getConfig('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[]
return picBeds
}
export default getPicBeds
export {
getPicBeds
}
@@ -1,39 +1,23 @@
import {
dialog,
BrowserWindow,
clipboard,
Notification,
ipcMain
WebContents,
ipcMain,
webContents
} from 'electron'
import path from 'path'
import db, { GalleryDB } from 'apis/core/datastore'
import { dbPathChecker, defaultConfigPath, getGalleryDBPath } from 'apis/core/datastore/dbChecker'
import uploader from 'apis/app/uploader'
import db from '#/datastore'
import uploader from './uploader'
import pasteTemplate from '#/utils/pasteTemplate'
import { handleCopyUrl } from '~/main/utils/common'
import {
getWindowId,
getSettingWindowId
} from '@core/bus/apis'
import {
SHOW_INPUT_BOX
} from '~/universal/events/constants'
import { DBStore } from '@picgo/store'
type PromiseResType<T> = T extends Promise<infer R> ? R : T
} from '~/main/utils/busApi'
// Cross-process support may be required in the future
class GuiApi implements IGuiApi {
private static instance: GuiApi
private windowId: number = -1
private settingWindowId: number = -1
private constructor () {
console.log('init guiapi')
}
public static getInstance (): GuiApi {
if (!GuiApi.instance) {
GuiApi.instance = new GuiApi()
}
return GuiApi.instance
}
private async showSettingWindow () {
this.settingWindowId = await getSettingWindowId()
const settingWindow = BrowserWindow.fromId(this.settingWindowId)
@@ -41,7 +25,7 @@ class GuiApi implements IGuiApi {
return true
}
settingWindow.show()
return new Promise<void>((resolve, reject) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 1000) // TODO: a better way to wait page loaded.
@@ -58,9 +42,9 @@ class GuiApi implements IGuiApi {
}) {
await this.showSettingWindow()
this.getWebcontentsByWindowId(this.settingWindowId)
.send(SHOW_INPUT_BOX, options)
.send('showInputBox', options)
return new Promise<string>((resolve, reject) => {
ipcMain.once(SHOW_INPUT_BOX, (event: Event, value: string) => {
ipcMain.once('showInputBox', (event: Event, value: string) => {
resolve(value)
})
})
@@ -81,9 +65,9 @@ class GuiApi implements IGuiApi {
const imgs = await uploader.setWebContents(webContents).upload(input)
if (imgs !== false) {
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
const pasteText: string[] = []
let pasteText = ''
for (let i = 0; i < imgs.length; i++) {
pasteText.push(pasteTemplate(pasteStyle, imgs[i], db.get('settings.customLink')))
pasteText += pasteTemplate(pasteStyle, imgs[i]) + '\r\n'
const notification = new Notification({
title: '上传成功',
body: imgs[i].imgUrl as string,
@@ -92,9 +76,9 @@ class GuiApi implements IGuiApi {
setTimeout(() => {
notification.show()
}, i * 100)
await GalleryDB.getInstance().insert(imgs[i])
db.insert('uploaded', imgs[i])
}
handleCopyUrl(pasteText.join('\n'))
clipboard.writeText(pasteText)
webContents.send('uploadFiles', imgs)
webContents.send('updateGallery')
return imgs
@@ -132,48 +116,6 @@ class GuiApi implements IGuiApi {
})
})
}
/**
* get picgo config/data path
*/
async getConfigPath () {
const currentConfigPath = dbPathChecker()
const galleryDBPath = getGalleryDBPath().dbPath
return {
defaultConfigPath,
currentConfigPath,
galleryDBPath
}
}
get galleryDB (): DBStore {
return new Proxy<DBStore>(GalleryDB.getInstance(), {
get (target, prop: keyof DBStore) {
if (prop === 'removeById') {
return new Proxy(GalleryDB.getInstance().removeById, {
apply (target, ctx, args) {
return new Promise((resolve) => {
const guiApi = GuiApi.getInstance()
guiApi.showMessageBox({
title: '警告',
message: '有插件正在试图删除一些相册图片,是否继续',
type: 'info',
buttons: ['Yes', 'No']
}).then(res => {
if (res.result === 0) {
resolve(Reflect.apply(target, ctx, args))
} else {
resolve(undefined)
}
})
})
}
})
}
return Reflect.get(target, prop)
}
})
}
}
export default GuiApi
+23 -43
View File
@@ -1,55 +1,35 @@
import path from 'path'
import fs from 'fs-extra'
import Logger from 'picgo/dist/src/lib/Logger'
interface IResultFileObject {
type ClipboardFileObject = {
path: string
}
type Result = IResultFileObject[]
interface IHandleArgvResult {
success: boolean
fileList?: string[]
}
const handleArgv = (argv: string[]): IHandleArgvResult => {
const uploadIndex = argv.indexOf('upload')
if (uploadIndex !== -1) {
const fileList = argv.slice(1 + uploadIndex)
return {
success: true,
fileList
}
}
return {
success: false
}
}
const getUploadFiles = (argv = process.argv, cwd = process.cwd(), logger: Logger) => {
const { success, fileList } = handleArgv(argv)
if (!success) {
return []
} else {
if (fileList?.length === 0) {
type Result = ClipboardFileObject[]
const getUploadFiles = (argv = process.argv, cwd = process.cwd()) => {
let files = argv.slice(1)
if (files.length > 0 && files[0] === 'upload') {
if (files.length === 1) {
return null // for uploading images in clipboard
} else if ((fileList?.length || 0) > 0) {
const result = fileList!.map(item => {
if (path.isAbsolute(item)) {
return {
path: item
}
} else {
let tempPath = path.join(cwd, item)
if (fs.existsSync(tempPath)) {
} else if (files.length > 1) {
files = argv.slice(1)
let result: Result = []
if (files.length > 0) {
result = files.map(item => {
if (path.isAbsolute(item)) {
return {
path: tempPath
path: item
}
} else {
logger.warn(`cli -> can't get file: ${tempPath}, invalid path`)
return null
let tempPath = path.join(cwd, item)
if (fs.existsSync(tempPath)) {
return {
path: tempPath
}
} else {
return null
}
}
}
}).filter(item => item !== null) as Result
}).filter(item => item !== null) as Result
}
return result
}
}
+76
View File
@@ -0,0 +1,76 @@
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()
@@ -1,13 +1,14 @@
import PicGoCore from '~/universal/types/picgo'
import { dbChecker, dbPathChecker } from 'apis/core/datastore/dbChecker'
import fs from 'fs-extra'
import {
app
} from 'electron'
import path from 'path'
// eslint-disable-next-line
const requireFunc = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require
const PicGo = requireFunc('picgo') as typeof PicGoCore
const STORE_PATH = app.getPath('userData')
const CONFIG_PATH = dbPathChecker()
dbChecker()
const CONFIG_PATH = path.join(STORE_PATH, '/data.json')
const picgo = new PicGo(CONFIG_PATH)
picgo.saveConfig({
@@ -15,6 +16,4 @@ picgo.saveConfig({
PICGO_ENV: 'GUI'
})
picgo.GUI_VERSION = global.PICGO_GUI_VERSION
export default picgo! as PicGoCore
export default picgo as PicGoCore
+225
View File
@@ -0,0 +1,225 @@
import path from 'path'
import GuiApi from './guiApi'
import {
dialog,
shell,
IpcMain,
IpcMainEvent,
App,
ipcMain,
app
} from 'electron'
import PicGoCore from '~/universal/types/picgo'
import { IPicGoHelperType } from '#/types/enum'
import shortKeyHandler from './shortKeyHandler'
import picgo from '~/main/utils/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 () => {
handleGetPluginList()
handlePluginInstall()
handlePluginUninstall()
handlePluginUpdate()
handleGetPicBedConfig()
handlePluginActions()
handleRemoveFiles()
handlePicGoSaveData()
}
-61
View File
@@ -1,61 +0,0 @@
import db from '~/main/apis/core/datastore'
import { ipcMain } from 'electron'
import { showMessageBox } from '~/main/utils/common'
import { SHOW_PRIVACY_MESSAGE } from '~/universal/events/constants'
class PrivacyManager {
async init () {
ipcMain.on(SHOW_PRIVACY_MESSAGE, () => {
this.show(false)
})
if (db.get('settings.privacyEnsure') !== true) {
const res = await this.show(true)
// cancel
if (res.result === 1) {
return false
} else {
db.set('settings.privacyEnsure', true)
return true
}
}
return true
}
async show (showCancel = true) {
const res = await showMessageBox({
type: 'info',
buttons: showCancel ? ['Yes', 'No'] : ['Yes'],
title: '隐私协议',
message: `
本软件尊重并保护所有使用服务用户的个人隐私权。为了给您提供更准确、更优质的服务,本软件会按照本隐私权政策的规定使用和收集您的一些行为信息。您在同意本软件服务使用协议之时,即视为您已经同意本隐私权政策全部内容。本隐私权政策属于本软件服务使用协议不可分割的一部分,如果不同意将无法使用。本协议会定期更新。
1.适用范围
a)在您使用本软件时,本软件会记录的您对本软件的一些操作行为信息,包括但不限于您使用本软件进行文件上传的耗时、类型、数量等信息。
2.信息的使用
a)在获得您的使用数据之后,本软件会将其上传至数据分析服务器,以便分析数据后,提供给您更好的服务。
3.信息披露
a)本软件不会将您的信息披露给不受信任的第三方。
b)根据法律的有关规定,或者行政或司法机构的要求,向第三方或者行政、司法机构披露;
c)如您出现违反中国有关法律、法规或者相关规则的情况,需要向第三方披露;
4.信息安全
a)本软件不会收集您的个人信息、密钥信息等隐私信息,所收集的信息仅仅作为改善软件、优化体验、了解软件日活等用途。
`
})
return res
}
}
const privacyManager = new PrivacyManager()
export {
privacyManager
}
@@ -1,18 +1,21 @@
import bus from '@core/bus'
import bus from './eventBus'
import PicGoCore from '~/universal/types/picgo'
import path from 'path'
import {
globalShortcut
app,
globalShortcut,
BrowserWindow
} from 'electron'
import logger from '@core/picgo/logger'
import GuiApi from '../../gui'
import db from '~/main/apis/core/datastore'
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
import shortKeyService from './shortKeyService'
import picgo from '@core/picgo'
import logger from './logger'
import GuiApi from './guiApi'
import db from '#/datastore'
import shortKeyService from './shortkeyService'
import picgo from './picgo'
class ShortKeyHandler {
private isInModifiedMode: boolean = false
constructor () {
bus.on(TOGGLE_SHORTKEY_MODIFIED_MODE, flag => {
bus.on('toggleShortKeyModifiedMode', flag => {
this.isInModifiedMode = flag
})
}
@@ -24,18 +27,14 @@ class ShortKeyHandler {
const commands = db.get('settings.shortKey') as IShortKeyConfigs
Object.keys(commands)
.filter(item => item.includes('picgo:'))
.forEach(command => {
.map(command => {
const config = commands[command]
// if disabled, don't register #534
if (config.enable) {
globalShortcut.register(config.key, () => {
this.handler(command)
})
}
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)
@@ -50,10 +49,7 @@ class ShortKeyHandler {
const command = `${item}:${cmd.name}`
if (db.has(`settings.shortKey[${command}]`)) {
const commandConfig = db.get(`settings.shortKey.${command}`) as IShortKeyConfig
// if disabled, don't register #534
if (commandConfig.enable) {
this.registerShortKey(commandConfig, command, cmd.handle, false)
}
this.registerShortKey(commandConfig, command, cmd.handle, false)
} else {
this.registerShortKey(cmd, command, cmd.handle, true)
}
@@ -72,8 +68,6 @@ 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}`]: {
@@ -130,7 +124,8 @@ class ShortKeyHandler {
} else if (command.includes('picgo-plugin-')) {
const handler = shortKeyService.getShortKeyHandler(command)
if (handler) {
return handler(picgo, GuiApi.getInstance())
const guiApi = new GuiApi()
return handler(picgo, guiApi)
}
} else {
logger.warn(`can not find command: ${command}`)
@@ -1,4 +1,4 @@
import logger from '@core/picgo/logger'
import logger from './logger'
class ShortKeyService {
private commandList: Map<string, IShortKeyHandler> = new Map()
registerCommand (command: string, handler: IShortKeyHandler) {
+14 -18
View File
@@ -1,11 +1,9 @@
import { dialog, shell } from 'electron'
import db from '~/main/apis/core/datastore'
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'
let release = 'https://cdn.jsdelivr.net/gh/Molunerfinn/PicGo/package.json'
const downloadUrl = 'https://github.com/Molunerfinn/PicGo/releases/latest'
const checkVersion = async () => {
@@ -17,15 +15,12 @@ const checkVersion = async () => {
if (showTip) {
let res: any
try {
res = await axios.get(releaseUrl).catch(async () => {
const result = await axios.get(releaseUrlBackup)
return result
})
res = await axios.get(release)
} catch (err) {
console.log(err)
}
if (res.status === 200) {
const latest = res.data.version || res.data.name
const latest = res.data.version
const result = compareVersion2Update(version, latest)
if (result) {
dialog.showMessageBox({
@@ -52,17 +47,18 @@ const checkVersion = async () => {
// if true -> update else return false
const compareVersion2Update = (current: string, latest: string) => {
try {
if (latest.includes('beta')) {
const isCheckBetaUpdate = db.get('settings.checkBetaUpdate') !== false
if (!isCheckBetaUpdate) {
return false
}
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
}
return lt(current, latest)
} catch (e) {
return false
}
return false
}
export default checkVersion
+161
View File
@@ -0,0 +1,161 @@
import {
app,
Notification,
BrowserWindow,
ipcMain,
WebContents
} from 'electron'
import dayjs from 'dayjs'
import picgo from '~/main/utils/picgo'
import db from '#/datastore'
const renameURL = process.env.NODE_ENV === 'development'
? `${(process.env.WEBPACK_DEV_SERVER_URL as string)}#rename-page`
: `picgo://./index.html#rename-page`
const createRenameWindow = (currentWindow: BrowserWindow) => {
let options: IBrowserWindowOptions = {
height: 175,
width: 300,
show: true,
fullscreenable: false,
resizable: false,
vibrancy: 'ultra-dark',
webPreferences: {
nodeIntegration: true,
nodeIntegrationInWorker: true,
backgroundThrottling: false
}
}
if (process.platform !== 'darwin') {
options.show = true
options.backgroundColor = '#3f3c37'
options.autoHideMenuBar = true
options.transparent = false
}
const window = new BrowserWindow(options)
window.loadURL(renameURL)
// check if this window is visible
if (currentWindow && currentWindow.isVisible()) {
// bounds: { x: 821, y: 75, width: 800, height: 450 }
const bounds = currentWindow.getBounds()
const positionX = bounds.x + bounds.width / 2 - 150
let positionY
// if is the settingWindow
if (bounds.height > 400) {
positionY = bounds.y + bounds.height / 2 - 88
} else { // if is the miniWindow
positionY = bounds.y + bounds.height / 2
}
window.setPosition(positionX, positionY, false)
}
return window
}
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) => {
ipcMain.once(`rename${id}`, (evt: Event, newName: string) => {
resolve(newName)
window.close()
})
window.on('close', () => {
resolve(null)
ipcMain.removeAllListeners(`rename${id}`)
})
})
}
class Uploader {
private webContents: WebContents | null = null
private currentWindow: BrowserWindow | 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 = createRenameWindow(this.currentWindow!)
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> {
this.currentWindow = BrowserWindow.fromWebContents(this.webContents!)
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()
+22 -19
View File
@@ -8,27 +8,30 @@
</el-switch>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'
@Component({
name: 'choose-pic-bed'
})
export default class extends Vue {
value = false
@Prop() type!: string
@Prop() label!: string
async created () {
const current = await this.getConfig<string>('picBed.current')
if (this.type === current) {
<script>
export default {
name: 'choose-pic-bed',
props: {
type: String,
label: String
},
data () {
return {
value: false
}
},
created () {
if (this.type === this.$db.get('picBed.current')) {
this.value = true
}
}
choosePicBed (val: string) {
this.saveConfig({
'picBed.current': this.type,
'picBed.uploader': this.type
})
this.$emit('update:choosed', this.type)
},
methods: {
choosePicBed (val) {
this.letPicGoSaveData({
'picBed.current': this.type
})
this.$emit('update:choosed', this.type)
}
}
}
</script>
+3 -3
View File
@@ -80,10 +80,10 @@ export default class extends Vue {
deep: true,
immediate: true
})
async handleConfigChange (val: any) {
handleConfigChange (val: any) {
this.ruleForm = Object.assign({}, {})
const config = await this.getConfig<IPicGoPluginConfig>(`picBed.${this.id}`)
if (val.length > 0 && config) {
const config = this.$db.get(`picBed.${this.id}`)
if (val.length > 0) {
this.configList = cloneDeep(val).map((item: any) => {
let defaultValue = item.default !== undefined
? item.default : item.type === 'checkbox'
@@ -1,64 +0,0 @@
<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="main-page">
<div id="setting-page">
<div class="fake-title-bar" :class="{ 'darwin': os === 'darwin' }">
<div class="fake-title-bar__title">
PicGo - {{ version }}
@@ -92,75 +92,57 @@
</el-row>
</el-dialog>
<el-dialog
class="qrcode-dialog"
top="3vh"
width="60%"
title="图床配置二维码"
:visible.sync="qrcodeVisible"
:modal-append-to-body="false"
lock-scroll
title="自定义链接格式"
:visible.sync="customLinkVisible"
>
<el-form
label-position="left"
label-width="70px"
size="mini"
label-position="top"
:model="customLink"
ref="customLink"
:rules="rules"
>
<el-form-item
label="选择图床"
label="用占位符$url来表示url的位置"
prop="value"
>
<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-input
class="align-center"
v-model="customLink.value"
:autofocus="true"
></el-input>
</el-form-item>
</el-form>
<div class="qrcode-container">
<qrcode-vue
v-show="choosedPicBedForQRCode.length > 0"
:size="280"
:value="picBedConfigString"
/>
<div>
[]($url)
</div>
<span slot="footer">
<el-button @click="cancelCustomLink">取消</el-button>
<el-button type="primary" @click="confirmCustomLink">确定</el-button>
</span>
</el-dialog>
<el-dialog
:title="inputBoxOptions.title || '输入框'"
:visible.sync="showInputBoxVisible"
:modal-append-to-body="false"
@close="handleInputBoxClose"
>
<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-dialog>
<input-box-dialog />
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator'
import QrcodeVue from 'qrcode.vue'
import pick from 'lodash/pick'
import { Component, Vue } from 'vue-property-decorator'
import pkg from 'root/package.json'
import keyDetect from '@/utils/key-binding'
import {
remote,
ipcRenderer,
IpcRendererEvent,
clipboard
} from 'electron'
// import db from '#/datastore'
import { remote, ipcRenderer, IpcRendererEvent } from 'electron'
import db from '#/datastore'
import mixin from '@/utils/mixin'
import InputBoxDialog from '@/components/InputBoxDialog.vue'
import {
SHOW_PRIVACY_MESSAGE
} from '~/universal/events/constants'
import { IConfig } from 'picgo/dist/src/types/index'
const { Menu, dialog, BrowserWindow } = remote
const customLinkRule = (rule: string, value: string, callback: (arg0?: Error) => void) => {
if (!/\$url/.test(value)) {
@@ -170,12 +152,8 @@ const customLinkRule = (rule: string, value: string, callback: (arg0?: Error) =>
}
}
@Component({
name: 'main-page',
mixins: [mixin],
components: {
InputBoxDialog,
QrcodeVue
}
name: 'setting-page',
mixins: [mixin]
})
export default class extends Vue {
version = process.env.NODE_ENV === 'production' ? pkg.version : 'Dev'
@@ -184,29 +162,38 @@ export default class extends Vue {
visible = false
keyBindingVisible = false
customLinkVisible = false
customLink = {
value: db.get('customLink') || '$url'
}
rules = {
value: [
{ validator: customLinkRule, trigger: 'blur' }
]
}
os = ''
shortKey: IShortKeyMap = {
upload: db.get('shortKey.upload')
}
picBed: IPicBedType[] = []
qrcodeVisible = false
picBedConfigString = ''
choosedPicBedForQRCode: string[] = []
// for showInputBox
showInputBoxVisible = false
inputBoxValue = ''
inputBoxOptions = {
title: '',
placeholder: ''
}
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(async () => {
const picBedConfig = await this.getConfig('picBed')
const config = pick(picBedConfig, ...this.choosedPicBedForQRCode)
this.picBedConfigString = JSON.stringify(config)
})
}
}
handleSelect (index: string) {
const type = index.match(/picbeds-/)
if (type === null) {
@@ -235,11 +222,7 @@ export default class extends Vue {
}
closeWindow () {
const window = BrowserWindow.getFocusedWindow()
if (process.platform === 'linux') {
window!.hide()
} else {
window!.close()
}
window!.close()
}
buildMenu () {
const _this = this
@@ -259,18 +242,6 @@ export default class extends Vue {
click () {
_this.visible = true
}
},
{
label: '生成图床配置二维码',
click () {
_this.qrcodeVisible = true
}
},
{
label: '隐私协议',
click () {
ipcRenderer.send(SHOW_PRIVACY_MESSAGE)
}
}
]
this.menu = Menu.buildFromTemplate(template)
@@ -279,16 +250,38 @@ export default class extends Vue {
// this.menu!.popup(remote.getCurrentWindow())
this.menu!.popup()
}
keyDetect (type: string, event: KeyboardEvent) {
this.shortKey[type] = keyDetect(event).join('+')
}
cancelKeyBinding () {
this.keyBindingVisible = false
this.shortKey = db.get('shortKey')
}
cancelCustomLink () {
this.customLinkVisible = false
this.customLink.value = db.get('customLink') || '$url'
}
confirmCustomLink () {
// @ts-ignore
this.$refs.customLink.validate((valid: boolean) => {
if (valid) {
db.set('customLink', this.customLink.value)
this.customLinkVisible = false
ipcRenderer.send('updateCustomLink')
} else {
return false
}
})
}
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
@@ -296,6 +289,7 @@ export default class extends Vue {
}
beforeDestroy () {
ipcRenderer.removeListener('getPicBeds', this.getPicBeds)
ipcRenderer.removeAllListeners('showInputBox')
}
}
</script>
@@ -314,17 +308,7 @@ $darwinBg = transparentify(#172426, #000, 0.7)
font-size 20px
text-align center
margin 10px auto
#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
#setting-page
.fake-title-bar
-webkit-app-region drag
height h = 22px
+93 -158
View File
@@ -48,19 +48,14 @@
<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="4">
<el-col :span="6">
<div class="item-base copy round" :class="{ active: isMultiple(choosedList)}" @click="multiCopy">
复制
<i class="el-icon-document"></i> 批量复制
</div>
</el-col>
<el-col :span="4">
<el-col :span="6">
<div class="item-base delete round" :class="{ active: isMultiple(choosedList)}" @click="multiRemove">
删除
</div>
</el-col>
<el-col :span="4">
<div class="item-base all-pick round" :class="{ active: filterList.length > 0}" @click="toggleSelectAll">
{{ isAllSelected ? '取消' : '全选' }}
<i class="el-icon-delete"></i> 批量删除
</div>
</el-col>
</el-row>
@@ -71,12 +66,12 @@
<el-col :span="20" :offset="2">
<el-row :gutter="16">
<gallerys
:images="filterList"
:images="images"
:index="idx"
@close="handleClose"
:options="options"
></gallerys>
<el-col :span="6" v-for="(item, index) in filterList" :key="item.id" class="gallery-list__img">
<el-col :span="6" v-for="(item, index) in images" :key="item.id" class="gallery-list__img">
<div
class="gallery-list__item"
@click="zoomImage(index)"
@@ -87,7 +82,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="(val) => handleChooseImage(val, index)"></el-checkbox>
<el-checkbox v-model="choosedList[item.id]" class="pull-right" @change=" handleBarActive = true"></el-checkbox>
</div>
</el-col>
</el-row>
@@ -111,9 +106,7 @@
// @ts-ignore
import gallerys from 'vue-gallery'
import pasteStyle from '#/utils/pasteTemplate'
import { IPasteStyle } from '#/types/enum'
import { Component, Vue, Watch } from 'vue-property-decorator'
import { IResult } from '@picgo/store/dist/types'
import { Component, Vue } from 'vue-property-decorator'
import {
ipcRenderer,
clipboard,
@@ -135,13 +128,11 @@ export default class extends Vue {
}
dialogVisible = false
imgInfo = {
id: '',
id: null,
imgUrl: ''
}
choosedList: IObjT<boolean> = {}
choosedPicBed: string[] = []
lastChoosed: number = -1
isShiftKeyPress: boolean = false
searchText = ''
handleBarActive = false
pasteStyle = ''
@@ -153,97 +144,61 @@ export default class extends Vue {
Custom: 'Custom'
}
picBed: IPicBedType[] = []
@Watch('$route')
handleRouteUpdate (to: any, from: any) {
console.log(to, from)
if (from.name === 'gallery') {
this.clearChoosedList()
}
if (to.name === 'gallery') {
this.updateGallery()
}
beforeRouteEnter (to: any, from: any, next: any) {
next((vm: any) => {
vm.getGallery()
vm.getPasteStyle()
vm.getPicBeds()
})
}
async created () {
created () {
ipcRenderer.on('updateGallery', (event: IpcRendererEvent) => {
this.$nextTick(async () => {
this.updateGallery()
this.$nextTick(() => {
this.filterList = this.getGallery()
})
})
ipcRenderer.send('getPicBeds')
ipcRenderer.on('getPicBeds', this.getPicBeds)
this.updateGallery()
}
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()
}
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!]
})
}
set filterList (val) {
this.images = val
}
getPicBeds (event: IpcRendererEvent, picBeds: IPicBedType[]) {
this.picBed = picBeds
}
getGallery (): ImgInfo[] {
if (this.searchText || this.choosedPicBed.length > 0) {
return this.images
.filter(item => {
let isInChoosedPicBed = true
let isIncludesSearchText = true
if (this.choosedPicBed.length > 0) {
isInChoosedPicBed = this.choosedPicBed.some(type => type === item.type)
}
if (this.searchText) {
isIncludesSearchText = item.fileName?.includes(this.searchText) || false
}
return isIncludesSearchText && isInChoosedPicBed
})
} else {
return this.images
}
}
async updateGallery () {
this.images = (await this.$$db.get({ orderBy: 'desc' })).data
}
@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)
getGallery () {
if (this.choosedPicBed.length > 0) {
let arr: ImgInfo[] = []
this.choosedPicBed.forEach(item => {
let obj: IObj = {
type: item
}
if (this.searchText) {
obj.fileName = this.searchText
}
// @ts-ignore
arr = arr.concat(this.$db.read().get('uploaded').filter(obj => {
return obj.fileName.indexOf(this.searchText) !== -1 && obj.type === item
}).reverse().value())
})
this.images = arr
} else {
if (this.searchText) {
let data = this.$db.read().get('uploaded')
// @ts-ignore
.filter(item => {
return item.fileName.indexOf(this.searchText) !== -1
}).reverse().value()
this.images = data
} else {
// @ts-ignore
this.images = this.$db.read().get('uploaded').slice().reverse().value()
}
this.lastChoosed = index
}
}
clearChoosedList () {
this.isShiftKeyPress = false
Object.keys(this.choosedList).forEach(key => {
this.choosedList[key] = false
})
this.lastChoosed = -1
return this.images
}
zoomImage (index: number) {
this.idx = index
@@ -262,10 +217,9 @@ export default class extends Vue {
this.idx = null
this.changeZIndexForGallery(false)
}
async copy (item: ImgInfo) {
const style = await this.getConfig<IPasteStyle>('settings.pasteStyle') || IPasteStyle.MARKDOWN
const customLink = await this.getConfig<string>('settings.customLink')
const copyLink = pasteStyle(style, item, customLink)
copy (item: ImgInfo) {
const style = this.$db.get('settings.pasteStyle') || 'markdown'
const copyLink = pasteStyle(style, item)
const obj = {
title: '复制链接成功',
body: copyLink,
@@ -282,9 +236,10 @@ export default class extends Vue {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const file = await this.$$db.getById(id)
await this.$$db.removeById(id)
}).then(() => {
const file = this.$db.get('uploaded').getById(id)
// @ts-ignore
this.$db.read().get('uploaded').removeById(id).write()
ipcRenderer.send('removeFiles', [file])
const obj = {
title: '操作结果',
@@ -294,21 +249,22 @@ export default class extends Vue {
myNotification.onclick = () => {
return true
}
this.updateGallery()
}).catch((e) => {
console.log(e)
this.getGallery()
}).catch(() => {
return true
})
}
openDialog (item: ImgInfo) {
this.imgInfo.id = item.id!
this.imgInfo.id = item.id
this.imgInfo.imgUrl = item.imgUrl as string
this.dialogVisible = true
}
async confirmModify () {
await this.$$db.updateById(this.imgInfo.id, {
imgUrl: this.imgInfo.imgUrl
})
confirmModify () {
this.$db.read().get('uploaded')
// @ts-ignore
.getById(this.imgInfo.id)
.assign({ imgUrl: this.imgInfo.imgUrl })
.write()
const obj = {
title: '修改图片URL成功',
body: this.imgInfo.imgUrl,
@@ -319,7 +275,7 @@ export default class extends Vue {
return true
}
this.dialogVisible = false
this.updateGallery()
this.getGallery()
}
choosePicBed (type: string) {
let idx = this.choosedPicBed.indexOf(type)
@@ -335,35 +291,26 @@ 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.
const multiRemoveNumber = Object.values(this.choosedList).filter(item => item).length
if (multiRemoveNumber) {
this.$confirm(`将在相册中移除刚才选中的 ${multiRemoveNumber} 张图片,是否继续?`, '提示', {
if (Object.values(this.choosedList).some(item => item)) {
this.$confirm('将删除刚才选中的图片,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
let files: IResult<ImgInfo>[] = []
const imageIDList = Object.keys(this.choosedList)
for (let i = 0; i < imageIDList.length; i++) {
const key = imageIDList[i]
}).then(() => {
let files: ImgInfo[] = []
Object.keys(this.choosedList).forEach(key => {
if (this.choosedList[key]) {
const file = await this.$$db.getById<ImgInfo>(key)
if (file) {
files.push(file)
await this.$$db.removeById(key)
}
// @ts-ignore
const file = this.$db.read().get('uploaded').getById(key).value()
files.push(file)
// @ts-ignore
this.$db.read().get('uploaded').removeById(key).write()
}
}
this.clearChoosedList()
this.choosedList = {} // 只有删除才能将这个置空
})
this.choosedList = {}
this.getGallery()
const obj = {
title: '操作结果',
body: '删除成功'
@@ -373,35 +320,30 @@ export default class extends Vue {
myNotification.onclick = () => {
return true
}
this.updateGallery()
}).catch(() => {
return true
})
}
}
async multiCopy () {
multiCopy () {
if (Object.values(this.choosedList).some(item => item)) {
const copyString: string[] = []
const style = await this.getConfig<IPasteStyle>('settings.pasteStyle') || IPasteStyle.MARKDOWN
const customLink = await this.getConfig<string>('settings.customLink')
let copyString = ''
const style = this.$db.get('settings.pasteStyle') || 'markdown'
// choosedList -> { [id]: true or false }; true means choosed. false means not choosed.
const imageIDList = Object.keys(this.choosedList)
for (let i = 0; i < imageIDList.length; i++) {
const key = imageIDList[i]
Object.keys(this.choosedList).forEach(key => {
if (this.choosedList[key]) {
const item = await this.$$db.getById<ImgInfo>(key)
if (item) {
copyString.push(pasteStyle(style, item, customLink))
this.choosedList[key] = false
}
// @ts-ignore
const item = this.$db.read().get('uploaded').getById(key).value()
copyString += pasteStyle(style, item) + '\n'
this.choosedList[key] = false
}
}
})
const obj = {
title: '批量复制链接成功',
body: copyString.join('\n')
body: copyString
}
const myNotification = new Notification(obj.title, obj)
clipboard.writeText(copyString.join('\n'))
clipboard.writeText(copyString)
myNotification.onclick = () => {
return true
}
@@ -410,11 +352,11 @@ export default class extends Vue {
toggleHandleBar () {
this.handleBarActive = !this.handleBarActive
}
// getPasteStyle () {
// this.pasteStyle = this.$db.get('settings.pasteStyle') || 'markdown'
// }
async handlePasteStyleChange (val: string) {
this.saveConfig('settings.pasteStyle', val)
getPasteStyle () {
this.pasteStyle = this.$db.get('settings.pasteStyle') || 'markdown'
}
handlePasteStyleChange (val: string) {
this.$db.set('settings.pasteStyle', val)
this.pasteStyle = val
}
beforeDestroy () {
@@ -461,13 +403,6 @@ 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
+7 -17
View File
@@ -23,7 +23,7 @@ import {
IpcRendererEvent,
remote
} from 'electron'
import { SHOW_PRIVACY_MESSAGE } from '~/universal/events/constants'
import path from 'path'
@Component({
name: 'mini-page',
mixins: [mixin]
@@ -136,18 +136,16 @@ export default class extends Vue {
openContextMenu () {
this.menu!.popup()
}
async buildMenu () {
buildMenu () {
const _this = this
const current = await this.getConfig('picBed.current')
const submenu = this.picBed.filter(item => item.visible).map(item => {
return {
label: item.name,
type: 'radio',
checked: current === item.type,
checked: this.$db.get('picBed.current') === item.type,
click () {
_this.saveConfig({
'picBed.current': item.type,
'picBed.uploader': item.type
_this.letPicGoSaveData({
'picBed.current': item.type
})
ipcRenderer.send('syncPicBed')
}
@@ -172,16 +170,8 @@ export default class extends Vue {
}
},
{
label: '隐藏窗口',
click () {
remote.BrowserWindow.getFocusedWindow()!.hide()
}
},
{
label: '隐私协议',
click () {
ipcRenderer.send(SHOW_PRIVACY_MESSAGE)
}
label: '最小化窗口',
role: 'minimize'
},
{
label: '重启应用',
+55 -152
View File
@@ -4,10 +4,10 @@
PicGo设置 - <i class="el-icon-document" @click="goConfigPage"></i>
</div>
<el-row class="setting-list">
<el-col :span="16" :offset="4">
<el-col :span="15" :offset="4">
<el-row>
<el-form
label-width="160px"
label-width="120px"
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,17 +56,6 @@
@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="开机自启"
>
@@ -118,16 +107,6 @@
@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="选择显示的图床"
>
@@ -183,20 +162,19 @@
</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="120px"
label-width="80px"
>
<el-form-item
label="上传代理"
label="代理地址"
>
<el-input
v-model="proxy"
@@ -204,24 +182,6 @@
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>
@@ -338,16 +298,14 @@
<script lang="ts">
import keyDetect from '@/utils/key-binding'
import pkg from 'root/package.json'
import { IConfig } from 'picgo/dist/src/types/index'
import { PICGO_OPEN_FILE } from '#/events/constants'
import path from 'path'
import {
ipcRenderer,
remote
} from 'electron'
import { Component, Vue } from 'vue-property-decorator'
// import db from '#/datastore'
const releaseUrl = 'https://api.github.com/repos/Molunerfinn/PicGo/releases/latest'
const releaseUrlBackup = 'https://cdn.jsdelivr.net/gh/Molunerfinn/PicGo@latest/package.json'
import db from '#/datastore'
const release = 'https://api.github.com/repos/Molunerfinn/PicGo/releases/latest'
const downloadUrl = 'https://github.com/Molunerfinn/PicGo/releases/latest'
const customLinkRule = (rule: string, value: string, callback: (arg0?: Error) => void) => {
if (!/\$url/.test(value)) {
@@ -356,22 +314,28 @@ const customLinkRule = (rule: string, value: string, callback: (arg0?: Error) =>
return callback()
}
}
let logLevel = db.get('settings.logLevel')
if (!Array.isArray(logLevel)) {
if (logLevel && logLevel.length > 0) {
logLevel = [logLevel]
} else {
logLevel = ['all']
}
}
@Component({
name: 'picgo-setting'
})
export default class extends Vue {
form: ISettingForm = {
updateHelper: false,
updateHelper: db.get('settings.showUpdateTip'),
showPicBedList: [],
autoStart: false,
rename: false,
autoRename: false,
uploadNotification: false,
miniWindowOntop: false,
logLevel: ['all'],
autoCopyUrl: true,
checkBetaUpdate: true
autoStart: db.get('settings.autoStart') || false,
rename: db.get('settings.rename') || false,
autoRename: db.get('settings.autoRename') || false,
uploadNotification: db.get('settings.uploadNotification') || false,
miniWindowOntop: db.get('settings.miniWindowOntop') || false,
logLevel
}
picBed: IPicBedType[] = []
logFileVisible = false
@@ -381,14 +345,12 @@ export default class extends Vue {
serverVisible = false
proxyVisible = false
customLink = {
value: '$url'
value: db.get('settings.customLink') || '$url'
}
shortKey: IShortKeyMap = {
upload: ''
upload: db.get('settings.shortKey.upload')
}
proxy = ''
npmRegistry = ''
npmProxy = ''
proxy = db.get('picBed.proxy') || ''
rules = {
value: [
{ validator: customLinkRule, trigger: 'blur' }
@@ -402,7 +364,7 @@ export default class extends Vue {
warn: '提醒-Warn',
none: '不记录日志-None'
}
server = {
server = db.get('settings.server') || {
port: 36677,
host: '127.0.0.1',
enable: true
@@ -423,45 +385,6 @@ export default class extends Vue {
ipcRenderer.send('getPicBeds')
ipcRenderer.on('getPicBeds', this.getPicBeds)
}
async initData () {
const config = (await this.getConfig<IConfig>())!
if (config !== undefined) {
const settings = config.settings || {}
const picBed = config.picBed
this.form.updateHelper = settings.showUpdateTip || false
this.form.autoStart = settings.autoStart || false
this.form.rename = settings.rename || false
this.form.autoRename = settings.autoRename || false
this.form.uploadNotification = settings.uploadNotification || false
this.form.miniWindowOntop = settings.miniWindowOntop || false
this.form.logLevel = this.initLogLevel(settings.logLevel || [])
this.form.autoCopyUrl = settings.autoCopy === undefined ? true : settings.autoCopy
this.form.checkBetaUpdate = settings.checkBetaUpdate === undefined ? true : settings.checkBetaUpdate
this.customLink.value = settings.customLink || '$url'
this.shortKey.upload = settings.shortKey.upload
this.proxy = picBed.proxy || ''
this.npmRegistry = settings.registry || ''
this.npmProxy = settings.proxy || ''
this.server = settings.server || {
port: 36677,
host: '127.0.0.1',
enable: true
}
}
}
initLogLevel (logLevel: string | string[]) {
if (!Array.isArray(logLevel)) {
if (logLevel && logLevel.length > 0) {
logLevel = [logLevel]
} else {
logLevel = ['all']
}
}
return logLevel
}
getPicBeds (event: Event, picBeds: IPicBedType[]) {
this.picBed = picBeds
this.form.showPicBedList = this.picBed.map(item => {
@@ -471,7 +394,10 @@ export default class extends Vue {
}) as string[]
}
openFile (file: string) {
ipcRenderer.send(PICGO_OPEN_FILE, file)
const { app, shell } = remote
const STORE_PATH = app.getPath('userData')
const FILE = path.join(STORE_PATH, `/${file}`)
shell.openItem(FILE)
}
openLogSetting () {
this.logFileVisible = true
@@ -479,15 +405,15 @@ export default class extends Vue {
keyDetect (type: string, event: KeyboardEvent) {
this.shortKey[type] = keyDetect(event).join('+')
}
async cancelCustomLink () {
cancelCustomLink () {
this.customLinkVisible = false
this.customLink.value = await this.getConfig<string>('settings.customLink') || '$url'
this.customLink.value = db.get('settings.customLink') || '$url'
}
confirmCustomLink () {
// @ts-ignore
this.$refs.customLink.validate((valid: boolean) => {
if (valid) {
this.saveConfig('settings.customLink', this.customLink.value)
db.set('settings.customLink', this.customLink.value)
this.customLinkVisible = false
ipcRenderer.send('updateCustomLink')
} else {
@@ -495,16 +421,14 @@ export default class extends Vue {
}
})
}
async cancelProxy () {
cancelProxy () {
this.proxyVisible = false
this.proxy = await this.getConfig<string>('picBed.proxy') || ''
this.proxy = db.get('picBed.proxy') || undefined
}
confirmProxy () {
this.proxyVisible = false
this.saveConfig({
'picBed.proxy': this.proxy,
'settings.proxy': this.npmProxy,
'settings.registry': this.npmRegistry
this.letPicGoSaveData({
'picBed.proxy': this.proxy
})
const successNotification = new Notification('设置代理', {
body: '设置成功'
@@ -514,10 +438,7 @@ export default class extends Vue {
}
}
updateHelperChange (val: boolean) {
this.saveConfig('settings.showUpdateTip', val)
}
checkBetaUpdateChange (val: boolean) {
this.saveConfig('settings.checkBetaUpdate', val)
db.set('settings.showUpdateTip', val)
}
handleShowPicBedListChange (val: string[]) {
const list = this.picBed.map(item => {
@@ -528,22 +449,22 @@ export default class extends Vue {
}
return item
})
this.saveConfig({
this.letPicGoSaveData({
'picBed.list': list
})
ipcRenderer.send('getPicBeds')
}
handleAutoStartChange (val: boolean) {
this.saveConfig('settings.autoStart', val)
db.set('settings.autoStart', val)
ipcRenderer.send('autoStart', val)
}
handleRename (val: boolean) {
this.saveConfig({
this.letPicGoSaveData({
'settings.rename': val
})
}
handleAutoRename (val: boolean) {
this.saveConfig({
this.letPicGoSaveData({
'settings.autoRename': val
})
}
@@ -563,16 +484,11 @@ export default class extends Vue {
}
checkUpdate () {
this.checkUpdateVisible = true
this.$http.get(releaseUrl)
this.$http.get(release)
.then(res => {
this.latestVersion = res.data.name
}).catch(async () => {
this.$http.get(releaseUrlBackup)
.then(res => {
this.latestVersion = res.data.version
}).catch(() => {
this.latestVersion = '网络错误暂时无法获取'
})
}).catch(err => {
console.log(err)
})
}
confirmCheckVersion () {
@@ -585,28 +501,17 @@ export default class extends Vue {
this.checkUpdateVisible = false
}
handleUploadNotification (val: boolean) {
this.saveConfig({
'settings.uploadNotification': val
})
db.set('settings.uploadNotification', val)
}
handleMiniWindowOntop (val: boolean) {
this.saveConfig('settings.miniWindowOntop', val)
db.set('settings.miniWindowOntop', val)
this.$message.info('需要重启生效')
}
handleAutoCopyUrl (val: boolean) {
this.saveConfig('settings.autoCopy', val)
const successNotification = new Notification('设置自动复制链接', {
body: '设置成功'
})
successNotification.onclick = () => {
return true
}
}
confirmLogLevelSetting () {
if (this.form.logLevel.length === 0) {
return this.$message.error('请选择日志记录等级')
}
this.saveConfig({
this.letPicGoSaveData({
'settings.logLevel': this.form.logLevel
})
const successNotification = new Notification('设置日志', {
@@ -617,9 +522,9 @@ export default class extends Vue {
}
this.logFileVisible = false
}
async cancelLogLevelSetting () {
cancelLogLevelSetting () {
this.logFileVisible = false
let logLevel = await this.getConfig<string | string[]>('settings.logLevel')
let logLevel = db.get('settings.logLevel')
if (!Array.isArray(logLevel)) {
if (logLevel && logLevel.length > 0) {
logLevel = [logLevel]
@@ -630,9 +535,7 @@ export default class extends Vue {
this.form.logLevel = logLevel
}
confirmServerSetting () {
// @ts-ignore
this.server.port = parseInt(this.server.port, 10)
this.saveConfig({
this.letPicGoSaveData({
'settings.server': this.server
})
const successNotification = new Notification('设置PicGo-Server', {
@@ -644,9 +547,9 @@ export default class extends Vue {
this.serverVisible = false
ipcRenderer.send('updateServer')
}
async cancelServerSetting () {
cancelServerSetting () {
this.serverVisible = false
this.server = await this.getConfig('settings.server') || {
this.server = db.get('settings.server') || {
port: 36677,
host: '127.0.0.1',
enable: true
+47 -84
View File
@@ -1,13 +1,7 @@
<template>
<div id="plugin-view">
<div class="view-title">
插件设置 -
<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>
插件设置 - <i class="el-icon-goods" @click="goAwesomeList"></i>
</div>
<el-row class="handle-bar" :class="{ 'cut-width': pluginList.length > 6 }">
<el-input
@@ -19,7 +13,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.fullName">
<el-col :span="12" v-for="item in pluginList" :key="item.name">
<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"
@@ -112,7 +106,6 @@ import {
remote,
IpcRendererEvent
} from 'electron'
import { handleStreamlinePluginName } from '~/universal/utils/common'
const { Menu } = remote
@Component({
@@ -132,8 +125,6 @@ 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"'
@@ -164,32 +155,26 @@ export default class extends Vue {
document.querySelector('.main-content.el-row').style.zIndex = 10
}
}
async created () {
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.fullName)
this.pluginNameList = list.map(item => item.name)
this.loading = false
})
ipcRenderer.on('installPlugin', (evt: IpcRendererEvent, { success, body }: {
success: boolean,
body: string
}) => {
ipcRenderer.on('installSuccess', (evt: IpcRendererEvent, plugin: string) => {
this.loading = false
this.pluginList.forEach(item => {
if (item.fullName === body) {
if (item.name === plugin) {
item.ing = false
item.hasInstall = success
item.hasInstall = true
}
})
})
ipcRenderer.on('updateSuccess', (evt: IpcRendererEvent, plugin: string) => {
this.loading = false
this.pluginList.forEach(item => {
if (item.fullName === plugin) {
if (item.name === plugin) {
item.ing = false
item.hasInstall = true
}
@@ -201,7 +186,7 @@ export default class extends Vue {
ipcRenderer.on('uninstallSuccess', (evt: IpcRendererEvent, plugin: string) => {
this.loading = false
this.pluginList = this.pluginList.filter(item => {
if (item.fullName === plugin) { // restore Uploader & Transformer after uninstalling
if (item.name === plugin) { // restore Uploader & Transformer after uninstalling
if (item.config.transformer.name) {
this.handleRestoreState('transformer', item.config.transformer.name)
}
@@ -210,33 +195,32 @@ export default class extends Vue {
}
this.getPicBeds()
}
return item.fullName !== plugin
return item.name !== plugin
})
this.pluginNameList = this.pluginNameList.filter(item => item !== plugin)
})
this.getPluginList()
this.getSearchResult = debounce(this.getSearchResult, 50)
this.needReload = await this.getConfig<boolean>('needReload') || false
this.needReload = this.$db.get('needReload')
}
async buildContextMenu (plugin: IPicGoPlugin) {
buildContextMenu (plugin: IPicGoPlugin) {
const _this = this
let menu = [{
label: '启用插件',
enabled: !plugin.enabled,
click () {
_this.saveConfig({
[`picgoPlugins.${plugin.fullName}`]: true
_this.letPicGoSaveData({
[`picgoPlugins.picgo-plugin-${plugin.name}`]: true
})
plugin.enabled = true
_this.getPicBeds()
_this.needReload = true
}
}, {
label: '禁用插件',
enabled: plugin.enabled,
click () {
_this.saveConfig({
[`picgoPlugins.${plugin.fullName}`]: false
_this.letPicGoSaveData({
[`picgoPlugins.picgo-plugin-${plugin.name}`]: false
})
plugin.enabled = false
_this.getPicBeds()
@@ -246,30 +230,28 @@ export default class extends Vue {
if (plugin.config.uploader.name) {
_this.handleRestoreState('uploader', plugin.config.uploader.name)
}
_this.needReload = true
}
}, {
label: '卸载插件',
click () {
_this.uninstallPlugin(plugin.fullName)
_this.uninstallPlugin(plugin.name)
}
}, {
label: '更新插件',
click () {
_this.updatePlugin(plugin.fullName)
_this.updatePlugin(plugin.name)
}
}]
for (let i in plugin.config) {
if (plugin.config[i].config.length > 0) {
const obj = {
label: `配置${i} - ${plugin.config[i].fullName || plugin.config[i].name}`,
label: `配置${i} - ${plugin.config[i].name}`,
click () {
_this.currentType = i
_this.configName = plugin.config[i].fullName || plugin.config[i].name
_this.configName = plugin.config[i].name
_this.dialogVisible = true
_this.config = plugin.config[i].config
},
enabled: plugin.enabled
}
}
menu.push(obj)
}
@@ -277,7 +259,7 @@ export default class extends Vue {
// handle transformer
if (plugin.config.transformer.name) {
let currentTransformer = await this.getConfig<string>('picBed.transformer') || 'path'
let currentTransformer = this.$db.get('picBed.transformer') || 'path'
let pluginTransformer = plugin.config.transformer.name
const obj = {
label: `${currentTransformer === pluginTransformer ? '禁用' : '启用'}transformer - ${plugin.config.transformer.name}`,
@@ -298,7 +280,7 @@ export default class extends Vue {
menu.push({
label: i.label,
click () {
ipcRenderer.send('pluginActions', plugin.fullName, i.label)
ipcRenderer.send('pluginActions', plugin.name, i.label)
}
})
}
@@ -321,13 +303,13 @@ export default class extends Vue {
type: 'warning'
}).then(() => {
item.ing = true
ipcRenderer.send('installPlugin', item.fullName)
ipcRenderer.send('installPlugin', item.name)
}).catch(() => {
console.log('Install canceled')
})
} else {
item.ing = true
ipcRenderer.send('installPlugin', item.fullName)
ipcRenderer.send('installPlugin', item.name)
}
}
uninstallPlugin (val: string) {
@@ -336,26 +318,22 @@ export default class extends Vue {
item.ing = true
}
})
this.loading = true
ipcRenderer.send('uninstallPlugin', val)
}
updatePlugin (val: string) {
this.pluginList.forEach(item => {
if (item.fullName === val) {
if (item.name === val) {
item.ing = true
}
})
this.loading = true
ipcRenderer.send('updatePlugin', val)
}
reloadApp () {
remote.app.relaunch()
remote.app.exit(0)
}
async handleReload () {
this.saveConfig({
needReload: true
})
handleReload () {
this.$db.set('needReload', true)
this.needReload = true
const successNotification = new Notification('更新成功', {
body: '请点击此通知重启应用以生效'
@@ -367,14 +345,14 @@ export default class extends Vue {
cleanSearch () {
this.searchText = ''
}
async toggleTransformer (transformer: string) {
let currentTransformer = await this.getConfig<string>('picBed.transformer') || 'path'
toggleTransformer (transformer: string) {
let currentTransformer = this.$db.get('picBed.transformer') || 'path'
if (currentTransformer === transformer) {
this.saveConfig({
this.letPicGoSaveData({
'picBed.transformer': 'path'
})
} else {
this.saveConfig({
this.letPicGoSaveData({
'picBed.transformer': transformer
})
}
@@ -385,17 +363,17 @@ export default class extends Vue {
if (result !== false) {
switch (this.currentType) {
case 'plugin':
this.saveConfig({
[`${this.configName}`]: result
this.letPicGoSaveData({
[`picgo-plugin-${this.configName}`]: result
})
break
case 'uploader':
this.saveConfig({
this.letPicGoSaveData({
[`picBed.${this.configName}`]: result
})
break
case 'transformer':
this.saveConfig({
this.letPicGoSaveData({
[`transformer.${this.configName}`]: result
})
break
@@ -429,7 +407,7 @@ export default class extends Vue {
})
}
handleSearchResult (item: INPMSearchResultObject) {
const name = handleStreamlinePluginName(item.package.name)
const name = item.package.name.replace(/picgo-plugin-/, '')
let gui = false
if (item.package.keywords && item.package.keywords.length > 0) {
if (item.package.keywords.includes('picgo-gui-plugin')) {
@@ -438,33 +416,31 @@ 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),
hasInstall: this.pluginNameList.some(plugin => plugin === item.package.name.replace(/picgo-plugin-/, '')),
version: item.package.version,
gui,
ing: false // installing or uninstalling
}
}
// restore Uploader & Transformer
async handleRestoreState (item: string, name: string) {
handleRestoreState (item: string, name: string) {
if (item === 'uploader') {
const current = await this.getConfig('picBed.current')
const current = this.$db.get('picBed.current')
if (current === name) {
this.saveConfig({
'picBed.current': 'smms',
'picBed.uploader': 'smms'
this.letPicGoSaveData({
'picBed.current': 'smms'
})
}
}
if (item === 'transformer') {
const current = await this.getConfig('picBed.transformer')
const current = this.$db.get('picBed.transformer')
if (current === name) {
this.saveConfig({
this.letPicGoSaveData({
'picBed.transformer': 'path'
})
}
@@ -478,16 +454,14 @@ export default class extends Vue {
goAwesomeList () {
remote.shell.openExternal('https://github.com/PicGo/Awesome-PicGo')
}
handleImportLocalPlugin () {
ipcRenderer.send('importLocalPlugin')
this.loading = true
letPicGoSaveData (data: IObj) {
ipcRenderer.send('picgoSaveData', data)
}
beforeDestroy () {
ipcRenderer.removeAllListeners('pluginList')
ipcRenderer.removeAllListeners('installPlugin')
ipcRenderer.removeAllListeners('installSuccess')
ipcRenderer.removeAllListeners('uninstallSuccess')
ipcRenderer.removeAllListeners('updateSuccess')
ipcRenderer.removeAllListeners('hideLoading')
}
}
</script>
@@ -517,7 +491,6 @@ $darwinBg = #172426
font-size 20px
text-align center
margin 10px auto
position relative
i.el-icon-goods
font-size 20px
vertical-align middle
@@ -525,16 +498,6 @@ $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
+13 -11
View File
@@ -98,7 +98,6 @@
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'
@@ -109,8 +108,8 @@ export default class extends Vue {
command = ''
shortKey = ''
currentIndex = 0
async created () {
const shortKeyConfig = (await this.getConfig<IShortKeyConfigs>('settings.shortKey'))!
created () {
const shortKeyConfig = this.$db.get('settings.shortKey') as IShortKeyConfigs
this.list = Object.keys(shortKeyConfig).map(item => {
return {
...shortKeyConfig[item],
@@ -120,7 +119,7 @@ export default class extends Vue {
}
@Watch('keyBindingVisible')
onKeyBindingVisibleChange (val: boolean) {
ipcRenderer.send(TOGGLE_SHORTKEY_MODIFIED_MODE, val)
ipcRenderer.send('toggleShortKeyModifiedMode', val)
}
calcOrigin (item: string) {
const [origin] = item.split(':')
@@ -132,23 +131,26 @@ export default class extends Vue {
toggleEnable (item: IShortKeyConfig) {
const status = !item.enable
item.enable = status
// this.$db.set(`settings.shortKey.${item.name}.enable`, status)
ipcRenderer.send('bindOrUnbindShortKey', item, item.from)
}
keyDetect (event: KeyboardEvent) {
this.shortKey = keyDetect(event).join('+')
}
async openKeyBindingDialog (config: IShortKeyConfig, index: number) {
openKeyBindingDialog (config: IShortKeyConfig, index: number) {
this.command = `${config.from}:${config.name}`
this.shortKey = await this.getConfig(`settings.shortKey.${this.command}.key`) || ''
this.shortKey = this.$db.get(`settings.shortKey.${this.command}.key`)
this.currentIndex = index
this.keyBindingVisible = true
}
async cancelKeyBinding () {
cancelKeyBinding () {
this.keyBindingVisible = false
this.shortKey = await this.getConfig<string>(`settings.shortKey.${this.command}.key`) || ''
this.shortKey = this.$db.get(`settings.shortKey.${this.command}.key`)
}
async confirmKeyBinding () {
const oldKey = await this.getConfig<string>(`settings.shortKey.${this.command}.key`)
confirmKeyBinding () {
const oldKey = this.$db.get(`settings.shortKey.${this.command}.key`)
// this.$db.set(`settings.shortKey.${this.command}.key`, this.shortKey)
// const newKey = this.$db.get(`settings.shortKey.${this.command}`)
const config = Object.assign({}, this.list[this.currentIndex])
config.key = this.shortKey
ipcRenderer.send('updateShortKey', config, oldKey, config.from)
@@ -160,7 +162,7 @@ export default class extends Vue {
})
}
beforeDestroy () {
ipcRenderer.send(TOGGLE_SHORTKEY_MODIFIED_MODE, false)
ipcRenderer.send('toggleShortKeyModifiedMode', false)
}
}
</script>
+16 -18
View File
@@ -30,15 +30,12 @@ import { Component, Vue } from 'vue-property-decorator'
import mixin from '@/utils/mixin'
import pasteTemplate from '#/utils/pasteTemplate'
import { ipcRenderer, clipboard } from 'electron'
import { IPasteStyle } from '#/types/enum'
import { IResult } from '@picgo/store/dist/types'
@Component({
name: 'tray-page',
mixins: [mixin]
})
export default class extends Vue {
files: IResult<ImgInfo>[] = []
files = []
notification = {
title: '复制链接成功',
body: '',
@@ -49,16 +46,16 @@ export default class extends Vue {
get reverseList () {
return this.files.slice().reverse()
}
async getData () {
this.files = (await this.$$db.get<ImgInfo>({ orderBy: 'desc', limit: 5 })).data
getData () {
// @ts-ignore
this.files = this.$db.read().get('uploaded').slice().reverse().slice(0, 5).value()
}
async copyTheLink (item: ImgInfo) {
copyTheLink (item: ImgInfo) {
this.notification.body = item.imgUrl!
this.notification.icon = item.imgUrl!
const myNotification = new Notification(this.notification.title, this.notification)
const pasteStyle = await this.getConfig<IPasteStyle>('settings.pasteStyle') || IPasteStyle.MARKDOWN
const customLink = await this.getConfig<string>('settings.customLink')
clipboard.writeText(pasteTemplate(pasteStyle, item, customLink))
const pasteStyle = this.$db.get('settings.pasteStyle') || 'markdown'
clipboard.writeText(pasteTemplate(pasteStyle, item))
myNotification.onclick = () => {
return true
}
@@ -86,18 +83,19 @@ export default class extends Vue {
mounted () {
this.disableDragFile()
this.getData()
ipcRenderer.on('dragFiles', async (event: Event, files: string[]) => {
for (let i = 0; i < files.length; i++) {
const item = files[i]
await this.$$db.insert(item)
}
this.files = (await this.$$db.get<ImgInfo>({ orderBy: 'desc', limit: 5 })).data
ipcRenderer.on('dragFiles', (event: Event, files: string[]) => {
files.forEach(item => {
this.$db.insert('uploaded', item)
})
// @ts-ignore
this.files = this.$db.read().get('uploaded').slice().reverse().slice(0, 5).value()
})
ipcRenderer.on('clipboardFiles', (event: Event, files: ImgInfo[]) => {
this.clipboardFiles = files
})
ipcRenderer.on('uploadFiles', async (event: Event) => {
this.files = (await this.$$db.get<ImgInfo>({ orderBy: 'desc', limit: 5 })).data
ipcRenderer.on('uploadFiles', (event: Event) => {
// @ts-ignore
this.files = this.$db.read().get('uploaded').slice().reverse().slice(0, 5).value()
console.log(this.files)
this.uploadFlag = false
})
+16 -73
View File
@@ -48,8 +48,7 @@
<div class="paste-style__text">
快捷上传
</div>
<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>
<el-button type="primary" round size="mini" @click="uploadClipboardFiles" class="paste-upload">剪贴板图片上传</el-button>
</div>
</div>
</el-col>
@@ -63,13 +62,6 @@ 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'
@@ -100,7 +92,6 @@ 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) {
@@ -115,41 +106,13 @@ 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
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地址')
}
this.ipcSendFiles(e.dataTransfer!.files)
}
openUplodWindow () {
document.getElementById('file-uploader')!.click()
@@ -169,39 +132,19 @@ export default class extends Vue {
})
ipcRenderer.send('uploadChoosedFiles', sendFiles)
}
async getPasteStyle () {
this.pasteStyle = await this.getConfig('settings.pasteStyle') || 'markdown'
getPasteStyle () {
this.pasteStyle = this.$db.get('settings.pasteStyle') || 'markdown'
}
handlePasteStyleChange (val: string) {
this.saveConfig({
'settings.pasteStyle': val
})
this.$db.set('settings.pasteStyle', val)
}
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')
}
}
async getDefaultPicBed () {
const currentPicBed = await this.getConfig<string>('picBed.current')
getDefaultPicBed () {
const current: string = this.$db.get('picBed.current')
this.picBed.forEach(item => {
if (item.type === currentPicBed) {
if (item.type === current) {
this.picBedName = item.name
}
})
@@ -210,23 +153,21 @@ export default class extends Vue {
this.picBed = picBeds
this.getDefaultPicBed()
}
async handleChangePicBed () {
await this.buildMenu()
handleChangePicBed () {
this.buildMenu()
// this.menu.popup(remote.getCurrentWindow())
this.menu!.popup()
}
async buildMenu () {
buildMenu () {
const _this = this
const currentPicBed = await this.getConfig<string>('picBed.current')
const submenu = this.picBed.filter(item => item.visible).map(item => {
return {
label: item.name,
type: 'radio',
checked: currentPicBed === item.type,
checked: this.$db.get('picBed.current') === item.type,
click () {
_this.saveConfig({
'picBed.current': item.type,
'picBed.uploader': item.type
_this.letPicGoSaveData({
'picBed.current': item.type
})
ipcRenderer.send('syncPicBed')
}
@@ -298,6 +239,8 @@ 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>
+3 -3
View File
@@ -86,8 +86,8 @@ export default class extends Vue {
customUrl: '',
options: ''
}
async created () {
const config = await this.getConfig<IAliYunConfig>('picBed.aliyun')
created () {
const config = this.$db.get('picBed.aliyun') as IAliYunConfig
if (config) {
this.form = Object.assign({}, config)
}
@@ -96,7 +96,7 @@ export default class extends Vue {
// @ts-ignore
this.$refs.aliyun.validate((valid) => {
if (valid) {
this.saveConfig({
this.letPicGoSaveData({
'picBed.aliyun': this.form
})
const successNotification = new window.Notification('设置结果', {
+4 -4
View File
@@ -25,7 +25,7 @@
:rules="{
required: true, message: '分支名不能为空', trigger: 'blur'
}">
<el-input v-model="form.branch" @keyup.native.enter="confirm" placeholder="例如:main"></el-input>
<el-input v-model="form.branch" @keyup.native.enter="confirm" placeholder="例如:master"></el-input>
</el-form-item>
<el-form-item
label="设定Token"
@@ -71,8 +71,8 @@ export default class extends Vue {
customUrl: '',
branch: ''
}
async created () {
const config = await this.getConfig<IGitHubConfig>('picBed.github')
created () {
const config = this.$db.get('picBed.github') as IGitHubConfig
if (config) {
this.form = Object.assign({}, config)
}
@@ -81,7 +81,7 @@ export default class extends Vue {
// @ts-ignore
this.$refs.github.validate((valid) => {
if (valid) {
this.saveConfig({
this.letPicGoSaveData({
'picBed.github': this.form
})
const successNotification = new Notification('设置结果', {
+3 -3
View File
@@ -48,8 +48,8 @@ export default class extends Vue {
clientId: '',
proxy: ''
}
async created () {
const config = await this.getConfig<IImgurConfig>('picBed.imgur')
created () {
const config = this.$db.get('picBed.imgur') as IImgurConfig
if (config) {
this.form = Object.assign({}, config)
}
@@ -58,7 +58,7 @@ export default class extends Vue {
// @ts-ignore
this.$refs.imgur.validate((valid) => {
if (valid) {
this.saveConfig({
this.letPicGoSaveData({
'picBed.imgur': this.form
})
const successNotification = new Notification('设置结果', {
+4 -9
View File
@@ -1,6 +1,6 @@
<template>
<div id="others-view">
<el-row :gutter="16" class="setting-list">
<el-row :gutter="16">
<el-col :span="16" :offset="4">
<div class="view-title">
{{ picBedName }}设置
@@ -56,7 +56,7 @@ export default class extends Vue {
// @ts-ignore
const result = await this.$refs.configForm.validate()
if (result !== false) {
this.saveConfig({
this.letPicGoSaveData({
[`picBed.${this.type}`]: result
})
const successNotification = new Notification('设置结果', {
@@ -68,9 +68,8 @@ export default class extends Vue {
}
}
setDefaultPicBed (type: string) {
this.saveConfig({
'picBed.current': type,
'picBed.uploader': type
this.letPicGoSaveData({
'picBed.current': type
})
// @ts-ignore 来自mixin的数据
this.defaultPicBed = type
@@ -92,10 +91,6 @@ 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
+4 -4
View File
@@ -17,7 +17,7 @@
:rules="{
required: true, message: 'AccessKey不能为空', trigger: 'blur'
}">
<el-input v-model="form.accessKey" placeholder="AccessKey" @keyup.native.enter="confirm"></el-input>
<el-input v-model="form.accessKey" placeholder="AccessKey" @keyup.native.enter="confirm('weiboForm')"></el-input>
</el-form-item>
<el-form-item
label="设定SecretKey"
@@ -88,8 +88,8 @@ export default class extends Vue {
options: '',
path: ''
}
async created () {
const config = await this.getConfig<IQiniuConfig>('picBed.qiniu')
created () {
const config = this.$db.get('picBed.qiniu') as IQiniuConfig
if (config) {
this.form = Object.assign({}, config)
}
@@ -98,7 +98,7 @@ export default class extends Vue {
// @ts-ignore
this.$refs.qiniu.validate((valid) => {
if (valid) {
this.saveConfig({
this.letPicGoSaveData({
'picBed.qiniu': this.form
})
const successNotification = new Notification('设置结果', {
+20 -68
View File
@@ -5,27 +5,12 @@
<div class="view-title">
SM.MS设置
</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>
<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-col>
</el-row>
</div>
@@ -38,58 +23,25 @@ import mixin from '@/utils/ConfirmButtonMixin'
mixins: [mixin]
})
export default class extends Vue {
form: ISMMSConfig = {
token: ''
}
async created () {
const config = await this.getConfig<string | boolean>('picBed.smms.token')
if (typeof config !== 'boolean') {
this.form.token = config || ''
}
}
confirm () {
// @ts-ignore
this.$refs.smms.validate((valid) => {
if (valid) {
this.saveConfig({
'picBed.smms': this.form
})
const successNotification = new window.Notification('设置结果', {
body: '设置成功'
})
successNotification.onclick = () => {
return true
}
} else {
return false
}
this.letPicGoSaveData({
'picBed.smms': true
})
// @ts-ignore 来自mixin
this.setDefaultPicBed('smms')
const successNotification = new window.Notification('设置结果', {
body: '设置成功'
})
successNotification.onclick = () => {
return true
}
}
}
</script>
<style lang='stylus'>
#smms-view
.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
.content
text-align center
font-size 14px
color #eee
</style>
+4 -4
View File
@@ -30,7 +30,7 @@
:rules="{
required: true, message: 'SecretId不能为空', trigger: 'blur'
}">
<el-input v-model="form.secretId" placeholder="SecretId" @keyup.native.enter="confirm"></el-input>
<el-input v-model="form.secretId" placeholder="SecretId" @keyup.native.enter="confirm('weiboForm')"></el-input>
</el-form-item>
<el-form-item
label="设定SecretKey"
@@ -104,8 +104,8 @@ export default class extends Vue {
customUrl: '',
version: 'v4'
}
async created () {
const config = await this.getConfig<ITcYunConfig>('picBed.tcyun')
created () {
const config = this.$db.get('picBed.tcyun') as ITcYunConfig
if (config) {
this.form = Object.assign({}, config)
}
@@ -114,7 +114,7 @@ export default class extends Vue {
// @ts-ignore
this.$refs.tcyun.validate((valid) => {
if (valid) {
this.saveConfig({
this.letPicGoSaveData({
'picBed.tcyun': this.form
})
const successNotification = new window.Notification('设置结果', {
+3 -3
View File
@@ -79,8 +79,8 @@ export default class extends Vue {
options: '',
path: ''
}
async created () {
const config = await this.getConfig<IUpYunConfig>('picBed.upyun')
created () {
const config = this.$db.get('picBed.upyun') as IUpYunConfig
if (config) {
this.form = Object.assign({}, config)
}
@@ -89,7 +89,7 @@ export default class extends Vue {
// @ts-ignore
this.$refs.tcyun.validate((valid) => {
if (valid) {
this.saveConfig({
this.letPicGoSaveData({
'picBed.upyun': this.form
})
const successNotification = new Notification('设置结果', {
+153
View File
@@ -0,0 +1,153 @@
<template>
<div id="weibo-view">
<el-row :gutter="16">
<el-col :span="16" :offset="4">
<div class="view-title">
微博图床设置[已停止支持]
</div>
<el-form
ref="weiboForm"
label-position="right"
label-width="120px"
size="small"
:model="form">
<el-form-item
label="设定用户名"
prop="username"
:rules="{
required: !chooseCookie, message: '用户名不能为空', trigger: 'blur'
}">
<el-input v-model="form.username" placeholder="用户名" @keyup.native.enter="confirm('weiboForm')" :disabled="chooseCookie"></el-input>
</el-form-item>
<el-form-item
label="设定密码"
prop="password"
:rules="{required: !chooseCookie,messsage: '密码不能为空',trigger: 'blur'}">
<el-input v-model="form.password" type="password" @keyup.native.enter="confirm('weiboForm')" placeholder="密码" :disabled="chooseCookie"></el-input>
</el-form-item>
<el-form-item
label="使用Cookie上传"
>
<el-switch
v-model="chooseCookie"
active-text="cookie模式"
@change="handleSwitchChange"
></el-switch>
<i class="el-icon-question" @click="openWiki"></i>
</el-form-item>
<el-form-item
label="设定Cookie"
prop="cookie"
:rules="{
required: chooseCookie, message: '密码不能为空', trigger: 'blur'
}">
<el-input v-model="form.cookie" @keyup.native.enter="confirm('weiboForm')" placeholder="Cookie" :disabled="!chooseCookie"></el-input>
</el-form-item>
<el-form-item label="* 图片质量">
<el-radio-group v-model="quality">
<el-radio label="thumbnail">缩略图</el-radio>
<el-radio label="mw690">中等尺寸</el-radio>
<el-radio label="large">原图</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button-group>
<el-button type="primary" @click="confirm('weiboForm')" round>确定</el-button>
<el-button type="success" @click="setDefaultPicBed('weibo')" round :disabled="defaultPicBed === 'weibo'">设为默认图床</el-button>
</el-button-group>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script>
import mixin from '@/utils/ConfirmButtonMixin'
export default {
name: 'weibo',
mixins: [mixin],
data () {
return {
form: {
username: '',
password: '',
cookie: ''
},
chooseCookie: false,
quality: 'large'
}
},
created () {
const config = this.$db.get('picBed.weibo')
if (config) {
this.form.username = config.username
this.form.password = config.password
this.quality = config.quality || 'large'
this.form.cookie = config.cookie
this.chooseCookie = config.chooseCookie
}
},
methods: {
confirm (formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.letPicGoSaveData({
'picBed.weibo': {
username: this.form.username,
password: this.form.password,
quality: this.quality,
cookie: this.form.cookie,
chooseCookie: this.chooseCookie
}
})
const successNotification = new window.Notification('设置结果', {
body: '设置成功'
})
successNotification.onclick = () => {
return true
}
} else {
return false
}
})
},
handleSwitchChange () {
this.$refs['weiboForm'].resetFields()
},
openWiki () {
this.$electron.remote.shell.openExternal('https://picgo.github.io/PicGo-Doc/zh/guide/config.html#微博图床')
}
}
}
</script>
<style lang='stylus'>
.el-message
left 60%
#weibo-view
.el-form
label
line-height 22px
padding-bottom 0
color #eee
.el-button-group
width 100%
.el-button
width 50%
.el-input__inner
border-radius 19px
.el-radio-group
margin-left 25px
.el-switch__label
color #eee
&.is-active
color #409EFF
.el-icon-question
font-size 20px
float right
margin-top 9px
color #eee
cursor pointer
transition .2s color ease-in-out
&:hover
color #409EFF
</style>
+8 -3
View File
@@ -22,15 +22,20 @@ export default new Router({
component: () => import(/* webpackChunkName: "MiniPage" */ '@/pages/MiniPage.vue')
},
{
path: '/main-page',
name: 'main-page',
component: () => import(/* webpackChunkName: "SettingPage" */ '@/layouts/Main.vue'),
path: '/setting',
name: 'setting-page',
component: () => import(/* webpackChunkName: "SettingPage" */ '@/layouts/SettingPage.vue'),
children: [
{
path: 'upload',
component: () => import(/* webpackChunkName: "Upload" */ '@/pages/Upload.vue'),
name: 'upload'
},
{
path: 'weibo',
component: () => import(/* webpackChunkName: "Weibo" */ '@/pages/picbeds/Weibo.vue'),
name: 'weibo'
},
{
path: 'qiniu',
component: () => import(/* webpackChunkName: "Qiniu" */ '@/pages/picbeds/Qiniu.vue'),
+3 -11
View File
@@ -1,19 +1,11 @@
import { Component, Vue } from 'vue-property-decorator'
import { ipcRenderer } from 'electron'
import { IConfig } from 'picgo/dist/src/types'
@Component
export default class extends Vue {
defaultPicBed = 'smms'
async created () {
const config = await this.getConfig<IConfig>()
if (config) {
this.defaultPicBed = config?.picBed?.uploader || config?.picBed?.current || 'smms'
}
}
defaultPicBed = this.$db.get('picBed.current')
setDefaultPicBed (type: string) {
this.saveConfig({
'picBed.current': type,
'picBed.uploader': type
this.letPicGoSaveData({
'picBed.current': type
})
this.defaultPicBed = type
const successNotification = new Notification('设置默认图床', {
-23
View File
@@ -1,23 +0,0 @@
/* eslint-disable camelcase */
import {
TALKING_DATA_APPID, TALKING_DATA_EVENT
} from '~/universal/events/constants'
import pkg from 'root/package.json'
import { ipcRenderer } from 'electron'
import { handleTalkingDataEvent } from './common'
const { version } = pkg
export const initTalkingData = () => {
setTimeout(() => {
const talkingDataScript = document.createElement('script')
talkingDataScript.src = `http://sdk.talkingdata.com/app/h5/v1?appid=${TALKING_DATA_APPID}&vn=${version}&vc=${version}`
const head = document.getElementsByTagName('head')[0]
head.appendChild(talkingDataScript)
}, 0)
}
ipcRenderer.on(TALKING_DATA_EVENT, (_, data: ITalkingDataOptions) => {
handleTalkingDataEvent(data)
})
-2
View File
@@ -1,2 +0,0 @@
import Vue from 'vue'
export default new Vue()
-10
View File
@@ -1,10 +0,0 @@
const isDevelopment = process.env.NODE_ENV !== 'production'
/* eslint-disable camelcase */
export const handleTalkingDataEvent = (data: ITalkingDataOptions) => {
const { EventId, Label = '', MapKv = {} } = data
MapKv.from = window.location.href
window.TDAPP.onEvent(EventId, Label, MapKv)
if (isDevelopment) {
console.log('talkingData', data)
}
}
-53
View File
@@ -1,53 +0,0 @@
import { IObject, IResult, IGetResult, IFilter } from '@picgo/store/dist/types'
import { ipcRenderer, IpcRendererEvent } from 'electron'
import { uuid } from 'uuidv4'
import {
PICGO_GET_DB,
PICGO_INSERT_DB,
PICGO_INSERT_MANY_DB,
PICGO_UPDATE_BY_ID_DB,
PICGO_GET_BY_ID_DB,
PICGO_REMOVE_BY_ID_DB
} from '#/events/constants'
import { IGalleryDB } from '#/types/extra-vue'
export class GalleryDB implements IGalleryDB {
async get<T> (filter?: IFilter): Promise<IGetResult<T>> {
const res = await this.msgHandler<IGetResult<T>>(PICGO_GET_DB, filter)
return res
}
async insert<T> (value: T): Promise<IResult<T>> {
const res = await this.msgHandler<IResult<T>>(PICGO_INSERT_DB, value)
return res
}
async insertMany<T> (value: T[]): Promise<IResult<T>[]> {
const res = await this.msgHandler<IResult<T>[]>(PICGO_INSERT_MANY_DB, value)
return res
}
async updateById (id: string, value: IObject): Promise<boolean> {
const res = await this.msgHandler<boolean>(PICGO_UPDATE_BY_ID_DB, id, value)
return res
}
async getById<T> (id: string): Promise<IResult<T> | undefined> {
const res = await this.msgHandler<IResult<T> | undefined>(PICGO_GET_BY_ID_DB, id)
return res
}
async removeById (id: string): Promise<void> {
const res = await this.msgHandler<void>(PICGO_REMOVE_BY_ID_DB, id)
return res
}
private msgHandler<T> (method: string, ...args: any[]): Promise<T> {
return new Promise((resolve) => {
const callbackId = uuid()
const callback = (event: IpcRendererEvent, data: T, returnCallbackId: string) => {
if (returnCallbackId === callbackId) {
resolve(data)
ipcRenderer.removeListener(method, callback)
}
}
ipcRenderer.on(method, callback)
ipcRenderer.send(method, ...args, callbackId)
})
}
}
export default new GalleryDB()
+3 -25
View File
@@ -1,30 +1,8 @@
import { Component, Vue } from 'vue-property-decorator'
import { ipcRenderer, IpcRendererEvent } from 'electron'
import { PICGO_SAVE_CONFIG, PICGO_GET_CONFIG } from '#/events/constants'
import { uuid } from 'uuidv4'
import { ipcRenderer } from 'electron'
@Component
export default class extends Vue {
// support string key + value or object config
saveConfig (config: IObj | string, value?: any) {
if (typeof config === 'string') {
config = {
[config]: value
}
}
console.log(PICGO_SAVE_CONFIG, config, value)
ipcRenderer.send(PICGO_SAVE_CONFIG, config)
}
getConfig<T> (key?: string): Promise<T | undefined> {
return new Promise((resolve) => {
const callbackId = uuid()
const callback = (event: IpcRendererEvent, config: T | undefined, returnCallbackId: string) => {
if (returnCallbackId === callbackId) {
resolve(config)
ipcRenderer.removeListener(PICGO_GET_CONFIG, callback)
}
}
ipcRenderer.on(PICGO_GET_CONFIG, callback)
ipcRenderer.send(PICGO_GET_CONFIG, key, callbackId)
})
letPicGoSaveData (data: IObj) {
ipcRenderer.send('picgoSaveData', data)
}
}
+66
View File
@@ -0,0 +1,66 @@
import Datastore from 'lowdb'
// @ts-ignore
import LodashId from 'lodash-id'
import FileSync from 'lowdb/adapters/FileSync'
import path from 'path'
import fs from 'fs-extra'
import { remote, app } from 'electron'
const APP = process.type === 'renderer' ? remote.app : app
const STORE_PATH = APP.getPath('userData')
if (process.type !== 'renderer') {
if (!fs.pathExistsSync(STORE_PATH)) {
fs.mkdirpSync(STORE_PATH)
}
}
class DB {
private db: Datastore.LowdbSync<Datastore.AdapterSync>
constructor () {
const adapter = new FileSync(path.join(STORE_PATH, '/data.json'))
this.db = Datastore(adapter)
this.db._.mixin(LodashId)
if (!this.db.has('uploaded').value()) {
this.db.set('uploaded', []).write()
}
if (!this.db.has('picBed').value()) {
this.db.set('picBed', {
current: 'weibo'
}).write()
}
if (!this.db.has('settings.shortKey').value()) {
this.db.set('settings.shortKey[picgo:upload]', {
enable: true,
key: 'CommandOrControl+Shift+P',
name: 'upload',
label: '快捷上传'
}).write()
}
}
read () {
return this.db.read()
}
get (key = '') {
return this.read().get(key).value()
}
set (key: string, value: any) {
return this.read().set(key, value).write()
}
has (key: string) {
return this.read().has(key).value()
}
insert (key: string, value: any): void {
// @ts-ignore
return this.read().get(key).insert(value).write()
}
unset (key: string, value: any): boolean {
return this.read().get(key).unset(value).value()
}
}
export default new DB()
-15
View File
@@ -1,15 +0,0 @@
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'
export const TALKING_DATA_APPID = '7E6832BCE3F1438696579E541DFEBFDA'
export const TALKING_DATA_EVENT = 'TALKING_DATA_EVENT'
export const SHOW_PRIVACY_MESSAGE = 'SHOW_PRIVACY_MESSAGE'
export const PICGO_SAVE_CONFIG = 'PICGO_SAVE_CONFIG'
export const PICGO_GET_CONFIG = 'PICGO_GET_CONFIG'
export const PICGO_GET_DB = 'PICGO_GET_DB'
export const PICGO_INSERT_DB = 'PICGO_INSERT_DB'
export const PICGO_INSERT_MANY_DB = 'PICGO_INSERT_MANY_DB'
export const PICGO_UPDATE_BY_ID_DB = 'PICGO_UPDATE_BY_ID_DB'
export const PICGO_GET_BY_ID_DB = 'PICGO_GET_BY_ID_DB'
export const PICGO_REMOVE_BY_ID_DB = 'PICGO_REMOVE_BY_ID_DB'
export const PICGO_OPEN_FILE = 'PICGO_OPEN_FILE'
-33
View File
@@ -1,33 +0,0 @@
import {
BrowserWindow
} from 'electron'
import {
IWindowList
} from 'apis/app/window/constants'
declare interface IWindowListItem {
isValid: boolean
multiple: boolean
options: () => IBrowserWindowOptions,
callback: (window: BrowserWindow, windowManager: IWindowManager) => void
}
declare interface IWindowManager {
create: (name: IWindowList) => BrowserWindow | null
get: (name: IWindowList) => BrowserWindow | null
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[]
}
}
}
+3 -15
View File
@@ -1,25 +1,13 @@
import VueRouter, { Route } from 'vue-router'
import db from '#/datastore'
import axios from 'axios'
import { IObject, IResult, IGetResult, IFilter } from '@picgo/store/dist/types'
interface IGalleryDB {
get<T>(filter?: IFilter): Promise<IGetResult<T>>
insert<T> (value: T): Promise<IResult<T>>
insertMany<T> (value: T[]): Promise<IResult<T>[]>
updateById (id: string, value: IObject): Promise<boolean>
getById<T> (id: string): Promise<IResult<T> | undefined>
removeById (id: string): Promise<void>
}
declare module 'vue/types/vue' {
interface Vue {
$router: VueRouter,
$route: Route,
$db: typeof db
$http: typeof axios
$builtInPicBed: string[]
$bus: Vue
$$db: IGalleryDB
saveConfig(data: IObj | string, value?: any): void
getConfig<T>(key?: string): Promise<T | undefined>
letPicGoSaveData(data: IObj): void
}
}
-6
View File
@@ -10,10 +10,4 @@ declare global {
[elem: string]: any
}
}
interface Window {
TDAPP: {
onEvent: (EventId: string, Label?: string, MapKv?: IStringKeyMap) => void
}
}
}
+8 -57
View File
@@ -7,7 +7,7 @@ interface IObjT<T> {
[propName: string]: T
}
declare interface ErrnoException extends Error {
interface ErrnoException extends Error {
errno?: number | string;
code?: string;
path?: string;
@@ -15,10 +15,6 @@ declare interface ErrnoException extends Error {
stack?: string;
}
declare var __static: string
declare type ILogType = 'success' | 'info' | 'warn' | 'error'
// Server
type routeHandler = (ctx: IServerCTX) => Promise<void>
@@ -30,8 +26,8 @@ interface IServerCTX {
}
interface IServerConfig {
port: number | string
host: string
port: number
host: string,
enable: boolean
}
@@ -44,8 +40,6 @@ interface ImgInfo {
height?: number
extname?: string
imgUrl?: string
id?: string
type?: string
[propName: string]: any
}
@@ -120,11 +114,15 @@ interface IBounds {
y: number
}
declare type ILogType = 'success' | 'info' | 'warn' | 'error'
// global value
declare var __static: string
// PicGo Types
type ICtx = import('picgo')
interface IPicGoPlugin {
name: string
fullName: string
author: string
description: string
logo: string
@@ -145,17 +143,8 @@ interface IPicGoPlugin {
hasInstall?: boolean
}
interface IPicGoPluginConfig {
name: string
type: string
required: boolean
default?: any
[propName: string]: any
}
interface IPluginMenuConfig {
name: string
fullName?: string
config: any[]
}
@@ -182,8 +171,6 @@ interface INPMSearchResultObject {
}
}
type IDispose = () => void
// GuiApi
interface IGuiApi {
showInputBox: (options: IShowInputBoxOption) => Promise<string>
@@ -193,7 +180,6 @@ interface IGuiApi {
showMessageBox: (options?: IShowMessageBoxOption) => Promise<IShowMessageBoxResult>
}
interface IShowInputBoxOption {
value?: string
title: string
placeholder: string
}
@@ -205,14 +191,6 @@ 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 {
@@ -274,10 +252,6 @@ interface IQiniuConfig {
path: string
}
interface ISMMSConfig {
token: string
}
interface ITcYunConfig {
secretId: string,
secretKey: string,
@@ -298,26 +272,3 @@ interface IUpYunConfig {
}
type ILoggerType = string | Error | boolean | number | undefined
interface IAppNotification {
title: string
body: string
icon?: string
}
interface ITalkingDataOptions {
EventId: string
Label?: string
MapKv?: IStringKeyMap
}
interface IAnalyticsData {
fromClipboard: boolean
type: string
count: number
duration?: number // 耗时
}
interface IStringKeyMap {
[propName: string]: any
}
-2
View File
@@ -7,8 +7,6 @@ interface ISettingForm {
uploadNotification: boolean
miniWindowOntop: boolean
logLevel: string[]
autoCopyUrl: boolean
checkBetaUpdate: boolean
}
interface IShortKeyMap {
-32
View File
@@ -1,32 +0,0 @@
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-/, '')
}
}
+6 -6
View File
@@ -1,9 +1,9 @@
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}$`), '')
const url = handleUrlEncode(item.url || item.imgUrl)
let url = item.url || item.imgUrl
const formatObj = {
url,
fileName
@@ -18,15 +18,15 @@ const formatCustomLink = (customLink: string, item: ImgInfo) => {
return customLink
}
export default (style: IPasteStyle, item: ImgInfo, customLink: string | undefined) => {
const url = handleUrlEncode(item.url || item.imgUrl)
const _customLink = customLink || '$url'
export default (style: IPasteStyle, item: ImgInfo) => {
let url = item.url || item.imgUrl
const customLink = db.get('settings.customLink') || '$url'
const tpl = {
'markdown': `![](${url})`,
'HTML': `<img src="${url}"/>`,
'URL': url,
'UBB': `[IMG]${url}[/IMG]`,
'Custom': formatCustomLink(_customLink, item)
'Custom': formatCustomLink(customLink, item)
}
return tpl[style]
}
+1 -7
View File
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2020",
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
@@ -30,12 +30,6 @@
],
"#/*": [
"src/universal/*"
],
"apis/*": [
"src/main/apis/*"
],
"@core/*": [
"src/main/apis/core/*"
]
},
"lib": [
+2 -37
View File
@@ -2,24 +2,13 @@ const path = require('path')
function resolve (dir) {
return path.join(__dirname, dir)
}
const arch = process.argv.includes('--ia32') ? 'ia32' : 'x64'
const config = {
configureWebpack: {
devtool: 'nosources-source-map'
},
module.exports = {
chainWebpack: config => {
config.resolve.alias
.set('@', resolve('src/renderer'))
.set('~', resolve('src'))
.set('root', resolve('./'))
.set('#', resolve('src/universal'))
// define
// config.plugin('define')
// .tap(args => {
// return args
// })
},
pluginOptions: {
electronBuilder: {
@@ -31,8 +20,6 @@ const config = {
.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',
@@ -68,14 +55,7 @@ const config = {
},
win: {
icon: 'build/icons/icon.ico',
// eslint-disable-next-line no-template-curly-in-string
artifactName: `PicGo Setup \${version}-${arch}.exe`,
target: [{
target: 'nsis',
arch: [
arch
]
}]
target: 'nsis'
},
nsis: {
shortcutName: 'PicGo',
@@ -84,23 +64,8 @@ const config = {
},
linux: {
icon: 'build/icons/'
},
snap: {
publish: ['github']
}
}
}
}
}
if (process.env.NODE_ENV === 'development') {
config.configureWebpack = {
devtool: 'source-map'
}
// for dev main process hot reload
config.pluginOptions.electronBuilder.mainProcessWatch = ['src/main/**/*']
}
module.exports = {
...config
}
+35 -168
View File
@@ -918,18 +918,6 @@
dependencies:
ora "^3.4.0"
"@picgo/store@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@picgo/store/-/store-1.0.3.tgz#cfe346ad30c80f378e0bc81311e7468df436bc1f"
integrity sha512-quyu56yuUsFFJGQ88suTwUSEzKosZPzR1M52eyqzyQ2HZWZVn0pa/eBBsh7OYefS0V13YLR5llnIkQaL8y11Ww==
dependencies:
"@types/bson" "^4.0.1"
"@types/graceful-fs" "^4.1.3"
bson "^4.0.3"
comment-json "^4.1.0"
lodash-id "^0.14.0"
lowdb "^1.0.0"
"@sindresorhus/is@^0.14.0":
version "0.14.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
@@ -951,13 +939,6 @@
dependencies:
defer-to-connect "^1.0.1"
"@types/bson@^4.0.1":
version "4.0.4"
resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.4.tgz#79d2d26e81070044db2a1a8b2cc2f673c840e1e5"
integrity sha512-awqorHvQS0DqxkHQ/FxcPX9E+H7Du51Qw/2F+5TBMSaE3G0hm+8D3eXJ6MAzFw75nE8V7xF0QvzUSdxIjJb/GA==
dependencies:
"@types/node" "*"
"@types/caseless@*":
version "0.12.2"
resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
@@ -999,13 +980,6 @@
"@types/minimatch" "*"
"@types/node" "*"
"@types/graceful-fs@^4.1.3":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15"
integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==
dependencies:
"@types/node" "*"
"@types/inquirer@^6.5.0":
version "6.5.0"
resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be"
@@ -1090,11 +1064,6 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.6.tgz#c880579e087d7a0db13777ff8af689f4ffc7b0d5"
integrity sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ==
"@types/uuid@8.3.1":
version "8.3.1"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f"
integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==
"@types/webpack-env@^1.13.9":
version "1.14.1"
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.14.1.tgz#0d8a53f308f017c53a5ddc3d07f4d6fa76b790d7"
@@ -1843,11 +1812,6 @@ array-includes@^3.0.3:
define-properties "^1.1.3"
es-abstract "^1.17.0-next.0"
array-timsort@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/array-timsort/-/array-timsort-1.0.3.tgz#3c9e4199e54fb2b9c3fe5976396a21614ef0d926"
integrity sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==
array-union@^1.0.1, array-union@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
@@ -2045,11 +2009,6 @@ base64-js@^1.0.2:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
base@^0.11.1:
version "0.11.2"
resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
@@ -2293,13 +2252,6 @@ browserslist@^4.0.0, browserslist@^4.6.0, browserslist@^4.7.3, browserslist@^4.8
electron-to-chromium "^1.3.322"
node-releases "^1.1.42"
bson@^4.0.3:
version "4.4.1"
resolved "https://registry.yarnpkg.com/bson/-/bson-4.4.1.tgz#682c3cb8b90b222414ce14ef8398154ba2cc21bc"
integrity sha512-Uu4OCZa0jouQJCKOk1EmmyqtdWAP5HVLru4lQxTwzJzxT+sJ13lVpEZU/MATDxtHiekWMAL84oQY3Xn1LpJVSg==
dependencies:
buffer "^5.6.0"
buffer-alloc-unsafe@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
@@ -2360,14 +2312,6 @@ buffer@^5.1.0, buffer@^5.2.1:
base64-js "^1.0.2"
ieee754 "^1.1.4"
buffer@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.1.13"
builder-util-runtime@8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.3.0.tgz#f5fac9139af6facf42a21fbe4d3aebed88fda33e"
@@ -2935,17 +2879,6 @@ comment-json@^2.3.1:
has-own-prop "^2.0.0"
repeat-string "^1.6.1"
comment-json@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.1.0.tgz#09d08f0fbc4ad5eeccbac20f469adbb967dcbd2c"
integrity sha512-WEghmVYaNq9NlWbrkzQTSsya9ycLyxJxpTQfZEan6a5Jomnjw18zS3Podf8q1Zf9BvonvQd/+Z7Z39L7KKzzdQ==
dependencies:
array-timsort "^1.0.3"
core-util-is "^1.0.2"
esprima "^4.0.1"
has-own-prop "^2.0.0"
repeat-string "^1.6.1"
commitizen@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/commitizen/-/commitizen-4.0.3.tgz#c19a4213257d0525b85139e2f36db7cc3b4f6dae"
@@ -3726,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.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
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:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@@ -3864,6 +3797,11 @@ 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"
@@ -4256,16 +4194,6 @@ electron-chromedriver@^5.0.1:
electron-download "^4.1.1"
extract-zip "^1.6.7"
electron-devtools-installer@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/electron-devtools-installer/-/electron-devtools-installer-3.2.0.tgz#acc48d24eb7033fe5af284a19667e73b78d406d0"
integrity sha512-t3UczsYugm4OAbqvdImMCImIMVdFzJAHgbwHpkl5jmfu1izVgUcP/mnrPqJIpEeCK1uZGpt+yHgWEN+9EwoYhQ==
dependencies:
rimraf "^3.0.2"
semver "^7.2.1"
tslib "^2.1.0"
unzip-crx-3 "^0.2.0"
electron-download@^4.1.0, electron-download@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8"
@@ -6046,11 +5974,6 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
dependencies:
postcss "^7.0.14"
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
ieee754@^1.1.4:
version "1.1.13"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
@@ -6076,13 +5999,6 @@ 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"
@@ -6430,11 +6346,6 @@ is-directory@^0.3.1:
resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
is-docker@^2.0.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
is-extendable@^0.1.0, is-extendable@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
@@ -6646,13 +6557,6 @@ is-wsl@^1.1.0:
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
is-wsl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
dependencies:
is-docker "^2.0.0"
is-yarn-global@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
@@ -7203,13 +7107,6 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
make-dir@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
@@ -7684,6 +7581,11 @@ 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"
@@ -8390,10 +8292,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.24:
version "1.4.24"
resolved "https://registry.yarnpkg.com/picgo/-/picgo-1.4.24.tgz#0d5a492e1f4d29823912535122fdc14928a6ddad"
integrity sha512-4QVTRIMLJes9c5vDuUl/c0TOEtUJycQhVdsXYtwx0fkKk/vhvfh0pRYUc33DEW88eGRi2CzAE2JJ93KHy5A8cg==
picgo@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/picgo/-/picgo-1.4.4.tgz#4fd2d51793a7f90a144a816c60035ed1f2f76bf2"
integrity sha512-0rieY+qlyfLVhPdwXZSqwitNCifgWJohDnnfHE92tPmP738r35Ab0wfa7eh4LRuI8Gx376FTNdlTIUQ4mHEsxQ==
dependencies:
chalk "^2.4.1"
commander "^2.17.0"
@@ -8404,13 +8306,12 @@ picgo@^1.4.24:
ejs "^2.6.1"
fs-extra "^6.0.1"
globby "^8.0.2"
image-size "^0.8.3"
inquirer "^6.0.0"
is-wsl "^2.2.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"
@@ -8884,6 +8785,18 @@ 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"
@@ -9010,11 +8923,6 @@ 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"
@@ -9053,13 +8961,6 @@ 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"
@@ -9556,13 +9457,6 @@ rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3, rimraf@^2.7.1:
dependencies:
glob "^7.1.3"
rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
dependencies:
glob "^7.1.3"
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
@@ -9716,13 +9610,6 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.2.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.2.1:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
dependencies:
lru-cache "^6.0.0"
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@@ -10222,6 +10109,13 @@ 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"
@@ -10829,11 +10723,6 @@ tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
tslib@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
tslint@^5.20.1:
version "5.20.1"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d"
@@ -11030,15 +10919,6 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
unzip-crx-3@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/unzip-crx-3/-/unzip-crx-3-0.2.0.tgz#d5324147b104a8aed9ae8639c95521f6f7cda292"
integrity sha512-0+JiUq/z7faJ6oifVB5nSwt589v1KCduqIJupNVDoWSXZtWDmjDGO3RAEOvwJ07w90aoXoP4enKsR7ecMrJtWQ==
dependencies:
jszip "^3.1.0"
mkdirp "^0.5.1"
yaku "^0.16.6"
unzip-crx@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/unzip-crx/-/unzip-crx-0.2.0.tgz#4c0baa8bdac756256754beca7843c13d7b858c18"
@@ -11214,24 +11094,11 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^3.0.1, uuid@^3.3.2:
version "3.3.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==
uuidv4@^6.2.11:
version "6.2.11"
resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.2.11.tgz#34d5a03324eb38296b87ae523a64233b5286cc27"
integrity sha512-OTS4waH9KplrXNADKo+Q1kT9AHWr8DaC0S5F54RQzEwcUaEzBEWQQlJyDUw/u1bkRhJyqkqhLD4M4lbFbV+89g==
dependencies:
"@types/uuid" "8.3.1"
uuid "8.3.2"
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"