Compare commits

...
83 Commits
Author SHA1 Message Date
PiEgg 4d04a9b711 🎉 Release: v2.4.1 2025-12-23 19:37:55 +08:00
PiEgg d071957968 📝 Docs: update 2.4.1 changelog 2025-12-23 19:37:33 +08:00
PiEgg d0eb3da45a Feature: add showMenubarIcon setting (#1366) 2025-12-23 18:06:17 +08:00
PiEgg eef736ff57 🐛 Fix(custom): data report 2025-12-22 21:34:10 +08:00
PiEgg a671ea4b26 📝 Docs(custom): update readme 2025-12-19 10:18:24 +08:00
PiEgg c658b9bdb1 📝 Docs(custom): update readme 2025-12-18 14:02:19 +08:00
PiEgg c8c9122e5b 📝 Docs(custom): update README 2025-12-18 10:42:58 +08:00
PiEgg 8c310e7a94 📦 Chore(custom): rm yarn.lock 2025-12-11 15:05:22 +08:00
PiEgg f84f5f1d92 🐛 Fix(custom): workflow env bug 2025-12-10 23:38:00 +08:00
PiEgg a0db473178 🐛 Fix(custom): build workflow error 2025-12-10 23:27:23 +08:00
PiEgg 89c24e7f8d 🎉 Release: v2.4.1-beta.1 2025-12-10 23:18:44 +08:00
PiEgg 54d15a6749 🐛 Fix(custom): the issue that x64 macOS app can't be opened
ISSUES CLOSED: #1363
2025-12-10 23:17:53 +08:00
PiEgg 2cc29833df 📦 Chore: update builder config && add legacy version file upload process 2025-12-09 17:42:22 +08:00
PiEgg f695e7ccaf 🎉 Release: v2.4.1-beta.0 2025-12-09 15:47:26 +08:00
PiEgg 366ac11ee2 📦 Chore: disabled build universal installer 2025-12-09 15:46:32 +08:00
PiEgg 9d4ea8277d 📦 Chore: change release url 2025-12-08 21:27:03 +08:00
PiEgg 56f61d458e 🐛 Fix: tray window clipboard image not show bug (#1362) 2025-12-08 21:16:25 +08:00
PiEgg 8e94b2a4d4 📦 Chore: fix upload dist arch bug 2025-12-08 18:47:05 +08:00
PiEgg db627a450f 📦 Chore: fix some workflow bug 2025-12-08 18:12:55 +08:00
PiEgg 070ce2b666 📦 Chore: upgrade electron version && migrate to electron-vite (#1361) 2025-12-08 17:42:35 +08:00
PiEgg b421c4b42a 🐛 Fix: pic-migrater post handler error 2025-11-28 21:18:52 +08:00
PiEgg d6c0a85a0f 🐛 Fix: fix docs link 2025-11-27 11:16:23 +08:00
PiEgg ca805f36f8 🎉 Release: v2.4.0 2025-11-23 08:30:58 +08:00
PiEgg cccd2954db 📝 Docs: add 2.4.0 changelog 2025-11-18 16:55:04 +08:00
PiEgg cfb6146de5 📦 Chore: update picgo core to 1.5.11 to solve url encode bug 2025-11-14 14:51:50 +08:00
PiEgg 45b3227456 📝 Docs: update readme & warp sponsor link 2025-11-03 17:29:14 +08:00
PiEgg 2450a524ff 📦 Chore: change funding yml 2025-10-30 15:49:08 +08:00
PiEgg 4a29bf2b50 📝 Docs: update docs 2025-10-28 14:45:59 +08:00
PiEgg 24e4a829d8 📝 Docs: update docs 2025-10-28 14:43:21 +08:00
PiEgg e0d45fa7a2 📝 Docs: add warp sponsor shoutout 2025-10-27 16:38:49 +08:00
PiEgg de441a892e 📝 Docs: add warp sponsor shoutout 2025-10-27 16:33:39 +08:00
PiEgg f1817eb1e1 🎉 Release: v2.4.0-beta.10 2025-06-08 10:38:22 +08:00
PiEgg 2bd5d0653b 🐛 Fix: encoded url filename unreadable 2025-06-08 10:37:31 +08:00
PiEgg 9841418779 Feature: finish form uploader for picgo server 2025-06-07 17:09:46 +08:00
PiEgg 7bc70628f9 🔨 Refactor: refactor the form uploader 2025-06-02 17:16:52 +08:00
happy-game a928b4c8ab Feature(server): add support for form upload in PicGo Server (#1327)
ISSUES CLOSED: #428
2025-03-17 09:01:15 +08:00
happy-game 7df37a2526 🐛 Fix: unable to use clip in wayland (#1301) 2024-12-11 17:29:09 +08:00
happy-game dacb926f17 🐛 Fix: auto-copy url can't be turned off (#1300) 2024-12-06 11:35:59 +08:00
PiEgg 1ea074e75f 🎉 Release: v2.4.0-beta.9 2024-12-02 20:02:06 +08:00
PiEgg 04140def7c 🐛 Fix: plugin list search bug
ISSUES CLOSED: #1297
2024-12-02 20:01:15 +08:00
PiEgg c9fe4023f0 🐛 Fix: clipboard filename missing second
ISSUES CLOSED: #1293
2024-11-25 14:51:41 +08:00
PiEgg c8ba547edb 🐛 Fix: copy text bug
ISSUES CLOSED: #1210, #1280
2024-10-05 08:34:04 +08:00
PiEgg 316928edc3 📦 Chore: update ci macos version 2024-07-16 23:03:20 +08:00
PiEgg ff35335126 🎉 Release: v2.4.0-beta.8 2024-07-16 22:58:25 +08:00
PiEgg ff7336b99e 🐛 Fix: tencent cos url encode bug
ISSUES CLOSED: #1265
2024-07-16 22:57:59 +08:00
PiEgg 917ec73027 🎉 Release: v2.4.0-beta.7 2024-04-22 09:53:45 +08:00
PiEgg 8e91582adc 🐛 Fix: config page scroll bug
ISSUES CLOSED: #1237
2024-04-19 09:39:44 +08:00
PiEgg aaec99f466 Feature: add startup mode
ISSUES CLOSED: #915
2024-02-28 15:59:20 +08:00
PiEgg 50e0a64519 🐛 Fix: tray menu open bug
ISSUES CLOSED: #1217
2024-02-25 09:55:41 +08:00
PiEgg 8cede85732 🎉 Release: v2.4.0-beta.6 2023-11-19 09:27:56 +08:00
PiEgg 050a3dd602 🐛 Fix: can't add new config for picbed
ISSUES CLOSED: #1184
2023-10-22 14:00:51 +08:00
木偶 cd07b33e6b 🐛 Fix: app.asar directroy copy error (#1180)
ISSUES CLOSED: #1179
2023-10-22 13:23:37 +08:00
PiEgg 6d4e1c27d5 🎉 Release: v2.4.0-beta.5 2023-09-10 17:41:19 +08:00
PiEgg 8fc65f4dcc ⬆️ Upgrade: picgo version 2023-09-10 17:40:28 +08:00
PiEgg 0f7b07dd7b Feature: add gallery toolbox menu (#1177) 2023-09-10 17:35:43 +08:00
PiEgg dca6667a81 🎉 Release: v2.4.0-beta.4 2023-08-26 12:37:03 +08:00
PiEgg 1b92f20a7d Feature: support "tips" option for uploader 2023-08-26 12:33:42 +08:00
barnettyang a2320c3503 Feature(tcyun): add slim section (#1165) 2023-08-23 16:46:40 +08:00
PiEgg 2db0fea6ee 🐛 Fix: open config file bug
ISSUES CLOSED: #1163
2023-08-15 20:22:50 +08:00
PiEgg 894d0a243e Feature: add configName for upload page 2023-07-23 11:03:07 +08:00
PiEgg 536b70832e 🎉 Release: v2.4.0-beta.3 2023-07-09 14:57:11 +08:00
PiEgg bc2e92836c 🐛 Fix: rename page bug
ISSUES CLOSED: #1130
2023-07-09 14:19:01 +08:00
浮心物语 a5ed1d107e 📝 Docs: update scoop install (#1109) 2023-07-09 13:07:47 +08:00
PiEgg e3566b5a8e 🐛 Fix: tailwind css bug 2023-07-09 13:02:16 +08:00
PiEgg 64eb832e06 🎉 Release: v2.4.0-beta.2 2023-07-09 10:13:22 +08:00
PiEgg 10e0584c96 🔨 Refactor: setting page 2023-07-09 10:12:49 +08:00
PiEgg 8d9a4000f3 🐛 Fix: fileName encode bug
ISSUES CLOSED: #1121
2023-05-07 13:48:45 +08:00
PiEgg 80c05a173e 🎉 Release: v2.4.0-beta.1 2023-05-03 22:14:03 +08:00
PiEgg 46f54e12b7 Feature: add showDockIcon option
ISSUES CLOSED: #1045
2023-05-03 22:13:48 +08:00
PiEgg 32eb17660d Feature: add showDockIcon option
ISSUES CLOSED: #1045
2023-05-03 21:53:07 +08:00
PiEgg dfbc96f1c0 Feature: add picgo toolbox for auto detect & fix problems 2023-05-03 16:41:28 +08:00
PiEgg d8b89da1ad 🚧 WIP: toolbox 2023-04-17 19:53:11 +08:00
PiEgg 063962d1ae 🐛 Fix: custom url template encode bug
ISSUES CLOSED: #1112
2023-04-17 19:52:38 +08:00
PiEgg 7363be798c 🐛 Fix: console.log EPIPE error
ISSUES CLOSED: #1101
2023-04-15 11:35:43 +08:00
PiEgg b7d2edbe23 🐛 Fix: isDarkMode() error when dragging file to tray icon
ISSUES CLOSED: #1107
2023-04-12 19:02:53 +08:00
PiEgg f75514d05d Feature: add settings.encodeOutputURL options
ISSUES CLOSED: #731
2023-04-09 17:49:08 +08:00
PiEgg 34657ae1fa 🐛 Fix: fix copy link encoding bug
ISSUES CLOSED: #731
2023-03-25 11:54:25 +08:00
PiEgg 455cb4937e 🐛 Fix: typescript nightly build bug
ISSUES CLOSED: #1082
2023-03-08 10:17:05 +08:00
PiEgg 4f392f3628 📦 Chore: change version files' upload dest & dist files' upload dest 2023-02-22 19:06:49 +08:00
PiEgg 6801334216 📝 Docs: update FAQ
ISSUES CLOSED: #1067
2023-02-04 11:59:07 +08:00
PiEgg 520d6d3fa8 Feature: support dragging any type of file to upload
ISSUES CLOSED: #1052
2023-01-11 21:26:36 +08:00
PiEgg 911e34e98d 🐛 Fix: i18n bug 2023-01-07 20:50:59 +08:00
PiEgg 66d8d714db 🔨 Refactor: upgrade vue2 -> vue3 2023-01-07 17:43:35 +08:00
176 changed files with 20780 additions and 17516 deletions
-4
View File
@@ -1,4 +0,0 @@
test/unit/coverage/**
test/unit/*.js
test/e2e/*.js
dist/
-37
View File
@@ -1,37 +0,0 @@
module.exports = {
root: true,
globals: {
__static: 'readonly'
},
env: {
node: true
},
parser: 'vue-eslint-parser',
extends: [
'plugin:vue/essential',
'@vue/standard',
'@vue/typescript'
],
plugins: ['@typescript-eslint'],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'off' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
indent: 'off',
'no-async-promise-executor': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/indent': ['error', 2]
},
parserOptions: {
parser: '@typescript-eslint/parser'
},
overrides: [
{
files: ['*.ts', '*.vue'],
rules: {
'no-undef': 'off' // https://typescript-eslint.io/docs/linting/troubleshooting/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
}
}
],
ignorePatterns: ['src/**/*.d.ts']
}
+1 -1
View File
@@ -1 +1 @@
custom: ["https://paypal.me/Molunerfinn"]
github: ["Molunerfinn"]
+128 -41
View File
@@ -1,58 +1,145 @@
# main.yml
name: Build & Release
# Workflow's name
name: Build
# Workflow's trigger
on:
push:
branches:
- master
tags:
- v*
workflow_dispatch:
inputs:
test_upload_dist:
description: "Test upload-dist.js script"
required: true
default: false
type: boolean
env:
NODE_VERSION: 22.x
# 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
# parallel build jobs
build:
name: Build on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
# create a build matrix for jobs
strategy:
# if one job fails, do not stop other jobs
fail-fast: false
matrix:
os: [ubuntu-latest, macos-11]
# create steps
os: [macos-latest, windows-latest, ubuntu-latest]
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
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
node-version: '16.x'
- name: Install system deps
if: matrix.os == 'ubuntu-latest'
version: 10
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Clean workspace on Windows
if: runner.os == 'Windows'
run: |
sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils
if (Test-Path dist) { Remove-Item -Recurse -Force dist }
if (Test-Path dist_electron) { Remove-Item -Recurse -Force dist_electron }
if (Test-Path node_modules) { Remove-Item -Recurse -Force node_modules }
# step3: yarn
- name: Yarn install
if (Test-Path "$env:LOCALAPPDATA\electron-builder") {
Remove-Item "$env:LOCALAPPDATA\electron-builder" -Recurse -Force -ErrorAction SilentlyContinue
}
if (Test-Path "$env:LOCALAPPDATA\electron") {
Remove-Item "$env:LOCALAPPDATA\electron" -Recurse -Force -ErrorAction SilentlyContinue
}
- name: Clean workspace on macOS & Linux
if: runner.os == 'macOS' || runner.os == 'Linux'
run: |
yarn
yarn global add xvfb-maybe
- name: Build & release app
run: |
yarn release
yarn upload-dist
rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Ubuntu Update with sudo
if: runner.os == 'Linux'
run: sudo apt-get update
- name: Build Windows x64 & ARM64 App
if: runner.os == 'Windows'
run: pnpm run build:win || true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
PICGO_ENV_COS_SECRET_ID: ${{ secrets.PICGO_ENV_COS_SECRET_ID }}
PICGO_ENV_COS_SECRET_KEY: ${{ secrets.PICGO_ENV_COS_SECRET_KEY }}
- name: Build macOS x64 & ARM64 App
if: runner.os == 'macOS'
run: pnpm run build:mac || true
shell: bash
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Build Linux x64 & ARM64 App
if: runner.os == 'Linux'
run: pnpm run build:linux || true
shell: bash
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: PicGo-${{ runner.os }}
path: dist/*.*
- name: Upload to release.picgo.app
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.test_upload_dist
run: |
pnpm run upload-dist
env:
PICGO_ENV_S3_SECRET_ID: ${{ secrets.PICGO_ENV_S3_SECRET_ID }}
PICGO_ENV_S3_SECRET_KEY: ${{ secrets.PICGO_ENV_S3_SECRET_KEY }}
PICGO_ENV_S3_ACCOUNT_ID: ${{ secrets.PICGO_ENV_S3_ACCOUNT_ID }}
PICGO_ENV_S3_LEGACY_ACCOUNT_ID: ${{ secrets.PICGO_ENV_S3_LEGACY_ACCOUNT_ID }}
PICGO_ENV_S3_LEGACY_SECRET_ID: ${{ secrets.PICGO_ENV_S3_LEGACY_SECRET_ID }}
PICGO_ENV_S3_LEGACY_SECRET_KEY: ${{ secrets.PICGO_ENV_S3_LEGACY_SECRET_KEY }}
release:
name: Publish GitHub Release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Publish GitHub Dev Release
if: github.event_name == 'workflow_dispatch'
uses: softprops/action-gh-release@v2
continue-on-error: true
with:
token: ${{ secrets.GH_TOKEN }}
tag_name: dev
draft: true
prerelease: false
files: |
!artifacts/**/*-unpacked/**
artifacts/**/*.exe
artifacts/**/*.dmg
artifacts/**/*.zip
artifacts/**/*.AppImage
artifacts/**/*.deb
artifacts/**/*.snap
artifacts/**/*.tar.gz
artifacts/**/*.yml
artifacts/**/*.blockmap
- name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
continue-on-error: true
with:
token: ${{ secrets.GH_TOKEN }}
generate_release_notes: true
draft: true
prerelease: false
files: |
!artifacts/**/*-unpacked/**
artifacts/**/*.exe
artifacts/**/*.dmg
artifacts/**/*.zip
artifacts/**/*.AppImage
artifacts/**/*.deb
artifacts/**/*.snap
artifacts/**/*.tar.gz
artifacts/**/*.yml
artifacts/**/*.blockmap
-55
View File
@@ -1,55 +0,0 @@
# main.yml
# Workflow's name
name: Build
# Workflow's trigger
on: workflow_dispatch
# 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-11]
# 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: '16.x'
- 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: |
yarn release
yarn upload-dist
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
PICGO_ENV_COS_SECRET_ID: ${{ secrets.PICGO_ENV_COS_SECRET_ID }}
PICGO_ENV_COS_SECRET_KEY: ${{ secrets.PICGO_ENV_COS_SECRET_KEY }}
+6
View File
@@ -19,3 +19,9 @@ dist_electron/
test.js
.env
scripts/*.yml
#Electron-builder output
/dist_electron
.serena/
dist/*
test.js
+1
View File
@@ -0,0 +1 @@
22
+1
View File
@@ -0,0 +1 @@
shamefully-hoist=true
+11 -3
View File
@@ -11,8 +11,13 @@
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
},
"preLaunchTask": "electron-debug",
"args": ["--remote-debugging-port=9223", "./dist_electron"],
"outFiles": ["${workspaceFolder}/dist_electron/**/*.js"]
"args": [
"--remote-debugging-port=9223",
"./dist"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
]
},
{
"name": "Electron: Renderer",
@@ -30,7 +35,10 @@
"compounds": [
{
"name": "Electron: All",
"configurations": ["Electron: Main", "Electron: Renderer"]
"configurations": [
"Electron: Main",
"Electron: Renderer"
]
}
]
}
+8 -2
View File
@@ -1,6 +1,7 @@
{
"eslint.enable": true,
"eslint.alwaysShowStatus": true,
"eslint.format.enable": true,
"eslint.validate": [
"javascript",
"javascriptreact",
@@ -20,6 +21,11 @@
"stylusSupremacy.sortProperties": "grouped",
"stylusSupremacy.quoteChar": "\"",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"prettier.enable": false,
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
}
+28
View File
@@ -0,0 +1,28 @@
# Repository Guidelines
## Project Structure & Module Organization
PicGo is an Electron + Vue 3 desktop client. Source lives in `src/`: `src/main` for main-process and IPC logic, `src/renderer` for Vue views, and `src/universal` for shared helpers (`types/`, `events/constants.ts`). `background.ts` wires Electron Builder. Static assets and locale YAML files stay in `public/` (add languages under `public/i18n/`), while `docs/` hosts user-facing guides. Automation scripts live in `scripts/`, and legacy tests sit under `test/unit` (Karma) and `test/e2e` (Spectron).
## Build, Test, and Development Commands
- `pnpm install` — install dependencies; `npm install` is unsupported.
- Always add/remove dependencies with `pnpm` (never edit package.json versions by hand then install).
- `pnpm dev` — electron-vite dev server for main/preload/renderer.
- `pnpm build` — electron-vite build outputs to `dist/main`, `dist/preload`, `dist/renderer`; `pnpm preview` for preview mode.
- Packaging config lives in `electron-builder.yml` (read by electron-builder via package.json `build` field/extraResources); set `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/` if downloads are slow.
- `pnpm lint` / `pnpm lint:fix` — run or auto-fix ESLint (Standard, TypeScript, Vue rules).
- `pnpm lint:dpdm` — fail fast on circular dependencies in `src/`.
- `pnpm gen-i18n` — regenerate typed locales after touching `public/i18n/*.yml`.
## Coding Style & Naming Conventions
Follow ESLint Standard defaults: two-space indentation, single quotes, trailing commas where allowed, and no stray semicolons. Author new modules in TypeScript. Keep renderer files browser-safe; route Node APIs through IPC helpers such as `src/main/events/picgoCoreIPC.ts`. Name Vue components in PascalCase (`UploadPanel.vue`) and use camelCase for utilities. Centralize IPC event names inside `src/universal/events/constants.ts`, and store enums/types under `src/universal/types/` so they stay reusable. Static assets are served from `public/` and resolved via `getStaticPath`/`getStaticFileUrl` (`src/universal/utils/staticPath.ts`); avoid using `__static` directly.
Static assets are served from `public/`. In the main process use `getStaticPath`/`getStaticFileUrl` (`src/universal/utils/staticPath.ts`). In the renderer, place assets under `public/` and resolve them via `import.meta.env.BASE_URL + filename` (helper: `src/renderer/utils/static.ts`); do not rely on `__static` in renderer code.
- Do not use `as any` under any circumstances; keep typings explicit and safe.
## Testing Guidelines
Place renderer unit specs in `test/unit/specs` with the `.spec.js` suffix; Karma picks them up via `require.context`. Run them with `npx karma start test/unit/karma.conf.js --single-run` and ensure new renderer folders are covered. Spectron e2e cases live in `test/e2e/specs`; build first (`pnpm build`), then run `npx mocha test/e2e/index.js` so Spectron can launch `dist/electron/main.js`. Document any test data, IPC stubs, or fixtures you add to keep suites reproducible.
## Commit & Pull Request Guidelines
Commits follow the PicGo conventional preset enforced by Husky (`pnpm lint:dpdm` + Commitlint). Stage your changes and run `pnpm cz` to craft messages that pass CI. Pull requests should explain the change, link related issues, and attach UI screenshots or recordings. Note how you validated the work (dev server, build, Karma, Spectron) and call out migration or configuration steps reviewers must perform.
## Internationalization Tips
Add locales by creating `public/i18n/<locale>.yml`, exposing its `LANG_DISPLAY_LABEL`, and registering it in `src/universal/i18n/index.ts`. Finish with `pnpm gen-i18n` so the generated typings stay in sync.
+227 -3
View File
@@ -1,3 +1,230 @@
## :tada: 2.4.1 (2025-12-23)
### :sparkles: Features
* add showMenubarIcon setting ([#1366](https://github.com/Molunerfinn/PicGo/issues/1366)) ([d0eb3da](https://github.com/Molunerfinn/PicGo/commit/d0eb3da))
### :bug: Bug Fixes
* **custom:** build workflow error ([a0db473](https://github.com/Molunerfinn/PicGo/commit/a0db473))
* **custom:** data report ([eef736f](https://github.com/Molunerfinn/PicGo/commit/eef736f))
* **custom:** workflow env bug ([f84f5f1](https://github.com/Molunerfinn/PicGo/commit/f84f5f1))
### :pencil: Documentation
* **custom:** update readme ([a671ea4](https://github.com/Molunerfinn/PicGo/commit/a671ea4))
* **custom:** update readme ([c658b9b](https://github.com/Molunerfinn/PicGo/commit/c658b9b))
* **custom:** update README ([c8c9122](https://github.com/Molunerfinn/PicGo/commit/c8c9122))
* update 2.4.1 changelog ([d071957](https://github.com/Molunerfinn/PicGo/commit/d071957))
### :package: Chore
* **custom:** rm yarn.lock ([8c310e7](https://github.com/Molunerfinn/PicGo/commit/8c310e7))
## :tada: 2.4.1-beta.1 (2025-12-10)
### :bug: Bug Fixes
* **custom:** the issue that x64 macOS app can't be opened ([54d15a6](https://github.com/Molunerfinn/PicGo/commit/54d15a6)), closes [#1363](https://github.com/Molunerfinn/PicGo/issues/1363)
### :package: Chore
* update builder config && add legacy version file upload process ([2cc2983](https://github.com/Molunerfinn/PicGo/commit/2cc2983))
## :tada: 2.4.1-beta.0 (2025-12-09)
### :bug: Bug Fixes
* fix docs link ([d6c0a85](https://github.com/Molunerfinn/PicGo/commit/d6c0a85))
* pic-migrater post handler error ([b421c4b](https://github.com/Molunerfinn/PicGo/commit/b421c4b))
* tray window clipboard image not show bug ([#1362](https://github.com/Molunerfinn/PicGo/issues/1362)) ([56f61d4](https://github.com/Molunerfinn/PicGo/commit/56f61d4))
### :package: Chore
* change release url ([9d4ea82](https://github.com/Molunerfinn/PicGo/commit/9d4ea82))
* disabled build universal installer ([366ac11](https://github.com/Molunerfinn/PicGo/commit/366ac11))
* fix some workflow bug ([db627a4](https://github.com/Molunerfinn/PicGo/commit/db627a4))
* fix upload dist arch bug ([8e94b2a](https://github.com/Molunerfinn/PicGo/commit/8e94b2a))
* upgrade electron version && migrate to electron-vite ([#1361](https://github.com/Molunerfinn/PicGo/issues/1361)) ([070ce2b](https://github.com/Molunerfinn/PicGo/commit/070ce2b))
# :tada: 2.4.0 (2025-11-23)
### :pencil: Documentation
* add 2.4.0 changelog ([cccd295](https://github.com/Molunerfinn/PicGo/commit/cccd295))
* add warp sponsor shoutout ([e0d45fa](https://github.com/Molunerfinn/PicGo/commit/e0d45fa))
* add warp sponsor shoutout ([de441a8](https://github.com/Molunerfinn/PicGo/commit/de441a8))
* update docs ([4a29bf2](https://github.com/Molunerfinn/PicGo/commit/4a29bf2))
* update docs ([24e4a82](https://github.com/Molunerfinn/PicGo/commit/24e4a82))
* update readme & warp sponsor link ([45b3227](https://github.com/Molunerfinn/PicGo/commit/45b3227))
### :package: Chore
* change funding yml ([2450a52](https://github.com/Molunerfinn/PicGo/commit/2450a52))
* update picgo core to 1.5.11 to solve url encode bug ([cfb6146](https://github.com/Molunerfinn/PicGo/commit/cfb6146))
# :tada: 2.4.0-beta.10 (2025-06-08)
### :sparkles: Features
* finish form uploader for picgo server ([9841418](https://github.com/Molunerfinn/PicGo/commit/9841418))
* **server:** add support for form upload in PicGo Server ([#1327](https://github.com/Molunerfinn/PicGo/issues/1327)) ([a928b4c](https://github.com/Molunerfinn/PicGo/commit/a928b4c)), closes [#428](https://github.com/Molunerfinn/PicGo/issues/428)
### :bug: Bug Fixes
* auto-copy url can't be turned off ([#1300](https://github.com/Molunerfinn/PicGo/issues/1300)) ([dacb926](https://github.com/Molunerfinn/PicGo/commit/dacb926))
* encoded url filename unreadable ([2bd5d06](https://github.com/Molunerfinn/PicGo/commit/2bd5d06))
* unable to use clip in wayland ([#1301](https://github.com/Molunerfinn/PicGo/issues/1301)) ([7df37a2](https://github.com/Molunerfinn/PicGo/commit/7df37a2))
# :tada: 2.4.0-beta.9 (2024-12-02)
### :bug: Bug Fixes
* clipboard filename missing second ([c9fe402](https://github.com/Molunerfinn/PicGo/commit/c9fe402)), closes [#1293](https://github.com/Molunerfinn/PicGo/issues/1293)
* copy text bug ([c8ba547](https://github.com/Molunerfinn/PicGo/commit/c8ba547)), closes [#1210](https://github.com/Molunerfinn/PicGo/issues/1210) [#1280](https://github.com/Molunerfinn/PicGo/issues/1280)
* plugin list search bug ([04140de](https://github.com/Molunerfinn/PicGo/commit/04140de)), closes [#1297](https://github.com/Molunerfinn/PicGo/issues/1297)
### :package: Chore
* update ci macos version ([316928e](https://github.com/Molunerfinn/PicGo/commit/316928e))
# :tada: 2.4.0-beta.8 (2024-07-16)
### :bug: Bug Fixes
* tencent cos url encode bug ([ff7336b](https://github.com/Molunerfinn/PicGo/commit/ff7336b)), closes [#1265](https://github.com/Molunerfinn/PicGo/issues/1265)
# :tada: 2.4.0-beta.7 (2024-04-22)
### :sparkles: Features
* add startup mode ([aaec99f](https://github.com/Molunerfinn/PicGo/commit/aaec99f)), closes [#915](https://github.com/Molunerfinn/PicGo/issues/915)
### :bug: Bug Fixes
* config page scroll bug ([8e91582](https://github.com/Molunerfinn/PicGo/commit/8e91582)), closes [#1237](https://github.com/Molunerfinn/PicGo/issues/1237)
* tray menu open bug ([50e0a64](https://github.com/Molunerfinn/PicGo/commit/50e0a64)), closes [#1217](https://github.com/Molunerfinn/PicGo/issues/1217)
# :tada: 2.4.0-beta.6 (2023-11-19)
### :bug: Bug Fixes
* app.asar directroy copy error ([#1180](https://github.com/Molunerfinn/PicGo/issues/1180)) ([cd07b33](https://github.com/Molunerfinn/PicGo/commit/cd07b33)), closes [#1179](https://github.com/Molunerfinn/PicGo/issues/1179)
* can't add new config for picbed ([050a3dd](https://github.com/Molunerfinn/PicGo/commit/050a3dd)), closes [#1184](https://github.com/Molunerfinn/PicGo/issues/1184)
# :tada: 2.4.0-beta.5 (2023-09-10)
### :sparkles: Features
* add gallery toolbox menu ([#1177](https://github.com/Molunerfinn/PicGo/issues/1177)) ([0f7b07d](https://github.com/Molunerfinn/PicGo/commit/0f7b07d))
# :tada: 2.4.0-beta.4 (2023-08-26)
### :sparkles: Features
* add configName for upload page ([894d0a2](https://github.com/Molunerfinn/PicGo/commit/894d0a2))
* support "tips" option for uploader ([1b92f20](https://github.com/Molunerfinn/PicGo/commit/1b92f20))
* **tcyun:** add slim section ([#1165](https://github.com/Molunerfinn/PicGo/issues/1165)) ([a2320c3](https://github.com/Molunerfinn/PicGo/commit/a2320c3))
### :bug: Bug Fixes
* open config file bug ([2db0fea](https://github.com/Molunerfinn/PicGo/commit/2db0fea)), closes [#1163](https://github.com/Molunerfinn/PicGo/issues/1163)
# :tada: 2.4.0-beta.3 (2023-07-09)
### :bug: Bug Fixes
* rename page bug ([bc2e928](https://github.com/Molunerfinn/PicGo/commit/bc2e928)), closes [#1130](https://github.com/Molunerfinn/PicGo/issues/1130)
* tailwind css bug ([e3566b5](https://github.com/Molunerfinn/PicGo/commit/e3566b5))
# :tada: 2.4.0-beta.2 (2023-07-09)
### :bug: Bug Fixes
* fileName encode bug ([8d9a400](https://github.com/Molunerfinn/PicGo/commit/8d9a400)), closes [#1121](https://github.com/Molunerfinn/PicGo/issues/1121)
# :tada: 2.4.0-beta.1 (2023-05-03)
### :sparkles: Features
* add picgo toolbox for auto detect & fix problems ([dfbc96f](https://github.com/Molunerfinn/PicGo/commit/dfbc96f))
* add settings.encodeOutputURL options ([f75514d](https://github.com/Molunerfinn/PicGo/commit/f75514d)), closes [#731](https://github.com/Molunerfinn/PicGo/issues/731)
* add showDockIcon option ([46f54e1](https://github.com/Molunerfinn/PicGo/commit/46f54e1)), closes [#1045](https://github.com/Molunerfinn/PicGo/issues/1045)
* add showDockIcon option ([32eb176](https://github.com/Molunerfinn/PicGo/commit/32eb176)), closes [#1045](https://github.com/Molunerfinn/PicGo/issues/1045)
* support dragging any type of file to upload ([520d6d3](https://github.com/Molunerfinn/PicGo/commit/520d6d3)), closes [#1052](https://github.com/Molunerfinn/PicGo/issues/1052)
### :bug: Bug Fixes
* console.log EPIPE error ([7363be7](https://github.com/Molunerfinn/PicGo/commit/7363be7)), closes [#1101](https://github.com/Molunerfinn/PicGo/issues/1101)
* custom url template encode bug ([063962d](https://github.com/Molunerfinn/PicGo/commit/063962d)), closes [#1112](https://github.com/Molunerfinn/PicGo/issues/1112)
* fix copy link encoding bug ([34657ae](https://github.com/Molunerfinn/PicGo/commit/34657ae)), closes [#731](https://github.com/Molunerfinn/PicGo/issues/731)
* i18n bug ([911e34e](https://github.com/Molunerfinn/PicGo/commit/911e34e))
* isDarkMode() error when dragging file to tray icon ([b7d2edb](https://github.com/Molunerfinn/PicGo/commit/b7d2edb)), closes [#1107](https://github.com/Molunerfinn/PicGo/issues/1107)
* typescript nightly build bug ([455cb49](https://github.com/Molunerfinn/PicGo/commit/455cb49)), closes [#1082](https://github.com/Molunerfinn/PicGo/issues/1082)
### :package: Chore
* change version files' upload dest & dist files' upload dest ([4f392f3](https://github.com/Molunerfinn/PicGo/commit/4f392f3))
### :pencil: Documentation
* update FAQ ([6801334](https://github.com/Molunerfinn/PicGo/commit/6801334)), closes [#1067](https://github.com/Molunerfinn/PicGo/issues/1067)
# :tada: 2.4.0-beta.0 (2023-01-05)
@@ -574,6 +801,3 @@
### :package: Chore
* add picgo bump-version ([37f1d34](https://github.com/Molunerfinn/PicGo/commit/37f1d34))
+25 -4
View File
@@ -1,6 +1,6 @@
## 常见问题
> 在使用 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://picgo.github.io/PicGo-Doc/guide/getting-started.html#%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B),这份 FAQ,以及那些被关闭的 [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed),应该能找到答案。
## 1. 七牛图床上传图片成功后,相册里无法显示或图片无`http://`前缀
@@ -10,11 +10,11 @@
## 2. 能否支持图床远端同步删除
不能。有些图床(比如微博图床、SM.MS、Imgur 等)不支持后台管理,为了架构统一不支持远端删除。
暂时不支持。有些图床(比如微博图床、SM.MS、Imgur 等)不支持后台管理,为了架构统一不支持远端删除。
## 3. 能否支持上传视频文件
目前不能。如果有人开发了相应的插件理论可以支持任意文件上传
目前部分图床支持上传视频文件,但并非所有图床都支持,请以实际使用的图床以及插件为准
## 4. 微博图床上传之后无法显示预览图
@@ -50,6 +50,27 @@
PicGo 在 Mac 上是一个顶部栏应用,在 dock 栏是不会有图标的。要打开主窗口,请右键或者双指点按顶部栏 PicGo 图标,选择「打开详细窗口」即可打开主窗口。
从 v2.4.1 开始,PicGo 支持在 macOS 下分别隐藏 Dock 栏图标(`showDockIcon`)和顶部栏图标(`showMenubarIcon`)。如果你把这两个配置都关闭(都设为 `false`),将会导致你无法通过 Dock 或顶部栏找到 PicGo 主界面。
手动恢复方法:
1. 找到并编辑 PicGo 的配置文件 `data.json`
- 如果还能打开设置页:PicGo 设置 -> 「打开配置文件」。
- 如果已经找不到界面:默认配置文件通常在 `~/Library/Application Support/PicGo/data.json`(如果你曾配置过自定义路径,则以配置里的 `configPath` 为准)。
2. 把以下任意一个字段改为 `true`(建议至少保留一个为 `true`),同时不要删改其他字段:
```json
{
"settings": {
// other settings ...
"showDockIcon": true,
"showMenubarIcon": true
}
}
```
3. 保存后重启 PicGo。
## 9. 上传失败,或者是服务器出错
1. PicGo 自带的图床都经过测试,上传出错一般都不是 PicGo 自身的原因。如果你用的是 GitHub 图床请参考上面的第 7 点。
@@ -118,7 +139,7 @@ options:
执行命令
```
xattr -c /Applications/PicGo.app/*
sudo xattr -d com.apple.quarantine /Applications/PicGo.app/
```
2. 如果安装打开后没有反应,请按下方顺序排查:
+115 -82
View File
@@ -1,132 +1,165 @@
<div align="center">
<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">
<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/actions/workflows/main.yml/badge.svg" alt="">
</a>
<a href="https://github.com/Molunerfinn/PicGo/releases">
<img src="https://img.shields.io/github/downloads/Molunerfinn/PicGo/total.svg?style=flat-square" alt="">
</a>
<a href="https://github.com/Molunerfinn/PicGo/releases/latest">
<img src="https://img.shields.io/github/release/Molunerfinn/PicGo.svg?style=flat-square" alt="">
</a>
<a href="https://github.com/PicGo/bump-version">
<img src="https://img.shields.io/badge/picgo-convention-blue.svg?style=flat-square" alt="">
<div align="center" markdown="1">
<sup>Special thanks to:</sup>
<br>
<a href="https://go.warp.dev/picgo">
<img alt="Warp sponsorship" width="400" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-03.png">
</a>
### [Warp, the intelligent terminal for developers](https://go.warp.dev/picgo)
[Available for macOS, Linux, & Windows](https://go.warp.dev/picgo)<br>
</div>
## 应用概述
---
**PicGo: 一个用于快速上传图片并获取图片 URL 链接的工具**
[中文](./README_zh-CN.md) | **English**
PicGo 本体支持如下图床:
<div align="center">
<img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/New%20LOGO-150.png" alt="PicGo Logo">
<h1>PicGo</h1>
<h3>The Ultimate Image Uploader for Efficient Creators</h3>
<p align="center">
<a href="https://github.com/Molunerfinn/PicGo/actions">
<img src="https://img.shields.io/badge/code%20style-standard-green.svg?style=flat-square" alt="">
</a>
<a href="https://github.com/Molunerfinn/PicGo/actions">
<img src="https://github.com/Molunerfinn/PicGo/actions/workflows/main.yml/badge.svg" alt="">
</a>
<a href="https://github.com/Molunerfinn/PicGo/releases">
<img src="https://img.shields.io/github/downloads/Molunerfinn/PicGo/total.svg?style=flat-square" alt="">
</a>
<a href="https://github.com/Molunerfinn/PicGo/releases/latest">
<img src="https://img.shields.io/github/release/Molunerfinn/PicGo.svg?style=flat-square" alt="">
</a>
<a href="https://github.com/PicGo/bump-version">
<img src="https://img.shields.io/badge/picgo-convention-blue.svg?style=flat-square" alt="">
</a>
</p>
</div>
- `七牛图床` v1.0
- `腾讯云 COS v4\v5 版本` v1.1 & v1.5.0
- `又拍云` v1.2.0
- `GitHub` v1.5.0
- `SM.MS V2` v2.3.0-beta.0
- `阿里云 OSS` v1.6.0
- `Imgur` v1.6.0
## 📖 Overview
**本体不再增加默认的图床支持。你可以自行开发第三方图床插件。详见 [PicGo-Core](https://picgo.github.io/PicGo-Core-Doc/)**
**PicGo aims to make image uploading a seamless part of your creative workflow.**
## 特色功能
Whether youre writing a blog post, taking notes, or authoring developer docs, PicGo helps you upload images in one step and automatically copies the resulting link—so you can stay focused on creating, not uploading.
- 支持拖拽图片上传
- 支持快捷键上传剪贴板里第一张图片
- 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) 与我交流 -->
### Supported Image hosts
**如果第一次使用,请参考应用 [使用文档](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)。**
PicGo supports mainstream Image hosts out of the box, and can be extended indefinitely through its plugin system:
## 下载安装
- **China cloud vendors**: Qiniu, Tencent Cloud COS, UPYUN, Alibaba Cloud OSS
- **International / open platforms**: GitHub, SM.MS, Imgur
- **More options via plugins**: AWS S3, Cloudflare R2, MinIO, and more
| 下载源 | 地址/安装方式 | 平台 | 备注 |
|---|---|---|---|
| GitHub Release | https://github.com/Molunerfinn/PicGo/releases | All | 国内下载速度可能会慢 |
| [腾讯云COS](https://cloud.tencent.com/product/cos) | https://github.com/Molunerfinn/PicGo/releases 附在更新日志结尾 | All | 感谢 [腾讯云COS](https://cloud.tencent.com/product/cos) 提供的赞助支持 |
| [山东大学镜像站](https://mirrors.sdu.edu.cn/) | https://mirrors.sdu.edu.cn/github-release/Molunerfinn_PicGo | All | 感谢 [山东大学镜像站](https://mirrors.sdu.edu.cn/) 提供的镜像支持 |
| [Scoop](https://scoop.sh/) | `scoop bucket add helbing https://github.com/helbing/scoop-bucket` & `scoop install picgo` | Windows | 感谢 @helbing 的贡献 |
| [Chocolatey](https://chocolatey.org/) | `choco install picgo` | Windows | 感谢 @iYato 的贡献 |
| [Homebrew](https://brew.sh/) | `brew install picgo --cask` | macOS | 感谢 @womeimingzi11 的贡献 |
| [AUR](https://aur.archlinux.org/packages/yay) | `yay -S picgo-appimage` | Arch-Linux | 感谢 @houbaron 的贡献 |
> **Note**: PicGo itself will no longer add new third-party Image hosts by default. You can build Image host plugins yourself—see [PicGo-Core](https://picgo.github.io/PicGo-Core-Doc/).
## 应用截图
## ✨ Key Features
PicGo is built around a fast, low-friction image upload experience:
### ⚡ Smooth writing flow
- **Auto-copy links**: once an upload finishes, the link is copied to your clipboard automatically.
- **Flexible formats**: Markdown, HTML, URL, custom templates—paste directly into any editor.
- **Zero-Context Switching**: Don't switch windows. Just paste images directly into your favorite editor, and let PicGo handle the upload in the background.
- _Enable this workflow via native support or community plugins:_ [Obsidian](https://obsidian.md) \ [VS Code](https://code.visualstudio.com/) \ [Typora](https://typora.io/) \ [Neovim](https://neovim.io/) \ [MarkText](https://marktext.me/) \ [SiYuan](https://b3log.org/siyuan/en/) \ And more...
### 🚀 Fast uploads
- **Multiple ways to upload**: drag & drop, paste from clipboard, hotkeys, and even right-click context menu upload on macOS/Windows.
- **Global hotkey**: press `Command+Shift+P` (macOS) / `Ctrl+Shift+P` (Windows/Linux) to open the upload window without leaving your current app. The global key can be customized.
### 🧩 Powerful plugin ecosystem
- **Highly extensible**: plugins already exist for AWS S3, Cloudflare R2, MinIO, and many other Image hosts.
- **Even more possibilities**: image compression, watermarking, renaming, Markdown image migration, and more.
- Explore plugins: [Awesome-PicGo](https://github.com/PicGo/Awesome-PicGo)
### 🛠 Developer-friendly
- **HTTP API**: upload via HTTP requests (v2.2.0+), making it easy to integrate with other tools.
- **Open source**: fully open-source and transparent.
- **Great documentation**: detailed docs help you get started quickly. For plugin development, see the [PicGo-Core docs](https://picgo.github.io/PicGo-Core-Doc/).
> Theres more to discover—development progress is tracked in [Projects](https://github.com/Molunerfinn/PicGo/projects).
If youre new to PicGo, start with the [User Guide](https://picgo.github.io/PicGo-Doc/guide/getting-started.html). If you run into issues, check the [FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md) and closed [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed).
## Download & Install
| Source | Link / Installation | Platform | Notes |
| --------------------------------------------------------- | ----------------------------------------------------------- | ---------- | --------------------------------------- |
| GitHub Releases | https://github.com/Molunerfinn/PicGo/releases | All | Downloads may be slow in mainland China |
| [Shandong University mirror](https://mirrors.sdu.edu.cn/) | https://mirrors.sdu.edu.cn/github-release/Molunerfinn_PicGo | All | Thanks to the mirror for hosting |
| [Scoop](https://scoop.sh/) | `scoop bucket add extras` & `scoop install picgo` | Windows | Thanks to @huangnauh and @Gladtbam |
| [Chocolatey](https://chocolatey.org/) | `choco install picgo` | Windows | Thanks to @iYato |
| [Homebrew](https://brew.sh/) | `brew install picgo --cask` | macOS | Thanks to @womeimingzi11 |
| [AUR](https://aur.archlinux.org/packages/yay) | `yay -S picgo-appimage` | Arch Linux | Thanks to @houbaron |
## Screenshots
![](https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/picgo-2.0.gif)
![picgo-menubar](https://user-images.githubusercontent.com/12621342/34242310-b5056510-e655-11e7-8568-60ffd4f71910.gif)
## 开发说明
## Development
> 目前仅针对 Mac、Windows。Linux 平台并未测试。
> Currently tested on macOS and Windows only. Linux has not been fully tested.
如果你想要学习、开发、修改或自行构建 PicGo,可以依照下面的指示:
If you want to learn, contribute, modify, or build PicGo yourself:
> 如果想学习 Electron-vue 的开发,可以查看我写的系列教程——[Electron-vue 开发实战](https://molunerfinn.com/tags/Electron-vue/)
> For an Electron-vue learning series, see: [Electron-vue development](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` 将导致未知错误!**
4. Mac 需要有 Xcode 环境,Windows 需要有 VS 环境。
5. 如果需要贡献代码,可以参考[贡献指南](./CONTRIBUTING.md)
1. Install Node.js and Git, and make sure youre familiar with npm basics.
2. Clone the repo: `git clone https://github.com/Molunerfinn/PicGo.git` and enter the directory.
3. Install dependencies with `pnpm`. If you dont have it yet, install it from the [pnpm website](https://pnpm.io/installation) first.
4. On macOS youll need Xcode; on Windows youll need Visual Studio.
5. For contributing, see [CONTRIBUTING.md](./CONTRIBUTING.md).
### 开发模式
### Development mode
输入 `npm run electron:serve` 进入开发模式,开发模式具有热重载特性。不过需要注意的是,开发模式不稳定,会有进程崩溃的情况。此时需要:
Run `pnpm run dev` to start the dev workflow with hot reload. Note: dev mode can be unstable and the process may crash—if that happens:
```bash
ctrl+c # 退出开发模式
npm run electron:serve # 重新进入开发模式
ctrl+c # stop dev mode
pnpm run dev # restart
```
**注:Windows 开发模式运行之后会在底部任务栏的右下角应用区出现 PicGo 的应用图标。**
> On Windows, after dev mode starts, PicGos tray icon will appear in the bottom-right system tray area.
### 生产模式
### Production build
如果你需要自行构建,可以 `npm run electron:build` 开始进行构建。构建成功后,会在 `dist_electron` 目录里出现构建成功的相应安装文件。
To build release artifacts locally, run `pnpm run build`. After a successful build, the installer files will be generated under `dist`.
**注意**:如果你的网络环境不太好,可能会出现 `electron-builder` 下载 `electron` 二进制文件失败的情况。这个时候需要在 `npm run electron:build` 之前指定一下 `electron` 的源为国内源:
**Note**: If your network is unstable, `electron-builder` may fail to download Electron binaries. You can set an alternative mirror before building:
```bash
export ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
# Windows 上,则可以使用 set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ (无需引号)
npm run electron:build
# On Windows: set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ (no quotes)
pnpm run build
```
只需第一次构建的时候指定一下国内源即可。后续构建不需要特地指定。二进制文件下载在 `~/.electron/` 目录下。如果想要更新 `electron` 构建版本,可以删除 `~/.electron/` 目录,然后重新运行上一步,让 `electron-builder `去下载最新的 `electron` 二进制文件。
Electron binaries are stored under `~/.electron/`. If you need to refresh them, delete that directory and rebuild.
## 其他相关
## Related Projects
- [vs-picgo](https://github.com/PicGo/vs-picgo)PicGo VS Code 版。
- [flutter-picgo](https://github.com/PicGo/flutter-picgo)PicGo 的手机版 App(支持 Android iOS )。
- [PicHoro](https://github.com/Kuingsmile/PicHoro):另一款支持 PicGo 配置的手机版 App(暂时只支持 Android)。
- [vs-picgo](https://github.com/PicGo/vs-picgo): PicGo for VS Code.
- [flutter-picgo](https://github.com/PicGo/flutter-picgo): mobile app (Android & iOS).
- [PicHoro](https://github.com/Kuingsmile/PicHoro): another mobile app compatible with PicGo config (Android only for now).
## 赞助
## Sponsorship
如果你喜欢 PicGo 并且它对你确实有帮助,欢迎给我打赏一杯咖啡哈~
If you like PicGo and it helps your workflow, feel free to buy me a coffee.
支付宝:
Alipay:
![](https://user-images.githubusercontent.com/12621342/34188165-e7cdf372-e56f-11e7-8732-1338c88b9bb7.jpg)
微信:
WeChat Pay:
![](https://user-images.githubusercontent.com/12621342/34188201-212cda84-e570-11e7-9b7a-abb298699d85.jpg)
GitHub Sponsors:
[![Sponsor PicGo on GitHub](https://img.shields.io/badge/Sponsor-PicGo-blue.svg?style=flat-square)](https://github.com/sponsors/Molunerfinn)
## License
[MIT](http://opensource.org/licenses/MIT)
+169
View File
@@ -0,0 +1,169 @@
<div align="center" markdown="1">
<sup>Special thanks to:</sup>
<br>
<a href="https://go.warp.dev/picgo">
<img alt="Warp sponsorship" width="400" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-03.png">
</a>
### [Warp, the intelligent terminal for developers](https://go.warp.dev/picgo)
[Available for macOS, Linux, & Windows](https://go.warp.dev/picgo)<br>
</div>
---
**中文** | [English](./README.md)
<div align="center">
<img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/New%20LOGO-150.png" alt="PicGo Logo">
<h1>PicGo</h1>
<h3>高效创作者的最佳图片上传工具</h3>
<p>The Ultimate Image Uploader for Efficient Creators</p>
<p align="center">
<a href="https://github.com/Molunerfinn/PicGo/actions">
<img src="https://img.shields.io/badge/code%20style-standard-green.svg?style=flat-square" alt="">
</a>
<a href="https://github.com/Molunerfinn/PicGo/actions">
<img src="https://github.com/Molunerfinn/PicGo/actions/workflows/main.yml/badge.svg" alt="">
</a>
<a href="https://github.com/Molunerfinn/PicGo/releases">
<img src="https://img.shields.io/github/downloads/Molunerfinn/PicGo/total.svg?style=flat-square" alt="">
</a>
<a href="https://github.com/Molunerfinn/PicGo/releases/latest">
<img src="https://img.shields.io/github/release/Molunerfinn/PicGo.svg?style=flat-square" alt="">
</a>
<a href="https://github.com/PicGo/bump-version">
<img src="https://img.shields.io/badge/picgo-convention-blue.svg?style=flat-square" alt="">
</a>
</p>
</div>
## 📖 应用概述
**PicGo 致力于将图片上传无缝集成到你的创作工作流中。**
无论你是写博客、做笔记还是编写开发文档,PicGo 都能帮你一键上传图片并自动复制链接,让你专注于内容创作本身,而不是繁琐的上传步骤。
### 核心支持
PicGo 原生支持主流图床平台,并可通过插件系统无限扩展:
- **国内云厂商**:七牛云、腾讯云 COS、又拍云、阿里云 OSS
- **国际/开源平台**GitHub、SM.MS、Imgur
- **更多支持**:通过插件支持 AWS S3、Cloudflare R2、MinIO 等第三方图床
> **注意**:PicGo 本体不再增加默认的第三方图床支持。你可以自行开发第三方图床插件。详见 [PicGo-Core](https://picgo.github.io/PicGo-Core-Doc/)。
## ✨ 特色功能
PicGo 打造了全方位的上传体验,让“传图”这件事变得前所未有的简单:
### ⚡️ 无缝写作流
- **自动复制链接**:上传成功后,链接会自动复制到你的剪贴板。
- **格式随心定义**:支持 Markdown、HTML、URL、自定义等多种格式,粘贴即用,完美适配你的编辑器。
- **零上下文切换**:无需切换窗口。在你常用的编辑器里直接粘贴图片,让 PicGo 在后台完成上传。
- _通过原生支持或社区插件开启该工作流:_ [Obsidian](https://obsidian.md) \ [VS Code](https://code.visualstudio.com/) \ [Typora](https://typora.io/) \ [Neovim](https://neovim.io/) \ [MarkText](https://marktext.me/) \ [SiYuan](https://b3log.org/siyuan/en/) \ 等等……
### 🚀 极速上传体验
- **多维上传方式**:支持拖拽图片、剪贴板粘贴、快捷键上传,甚至在 macOS/Windows 上支持右键菜单直接上传。
- **全局快捷键**:默认 `Command+Shift+P` (macOS) / `Ctrl+Shift+P` (Windows/Linux) 即可唤起上传,无需离开当前窗口。 快捷键可自定义。
### 🧩 强大的插件生态
- **高度可扩展**:PicGo 拥有丰富的插件系统,已有插件支持 AWS S3、Cloudflare R2、MinIO 等第三方图床。
- **更多可能**:支持图片压缩、水印、重命名、Markdown 图片迁移等功能插件。
- 探索更多插件:[Awesome-PicGo](https://github.com/PicGo/Awesome-PicGo)
### 🛠 开发者友好
- **HTTP API**:支持通过 HTTP 请求调用 PicGo 上传 (v2.2.0+),方便与其他工具集成。
- **开源透明**:代码完全开源,安全可靠。
- **丰富的文档**:详尽的开发文档助你快速上手。插件开发请参考 [PicGo-Core 文档](https://picgo.github.io/PicGo-Core-Doc/)。
> 更多功能等你自己去发现,开发进度可以查看 [Projects](https://github.com/Molunerfinn/PicGo/projects)。
**如果第一次使用,请参考应用 [使用文档](https://picgo.github.io/PicGo-Doc/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)。**
## 下载安装
| 下载源 | 地址/安装方式 | 平台 | 备注 |
| --------------------------------------------- | ----------------------------------------------------------- | ---------- | ----------------------------------------------------------------- |
| GitHub Release | https://github.com/Molunerfinn/PicGo/releases | All | 国内下载速度可能会慢 |
| [山东大学镜像站](https://mirrors.sdu.edu.cn/) | https://mirrors.sdu.edu.cn/github-release/Molunerfinn_PicGo | All | 感谢 [山东大学镜像站](https://mirrors.sdu.edu.cn/) 提供的镜像支持 |
| [Scoop](https://scoop.sh/) | `scoop bucket add extras` & `scoop install picgo` | Windows | 感谢 @huangnauh@Gladtbam 的贡献 |
| [Chocolatey](https://chocolatey.org/) | `choco install picgo` | Windows | 感谢 @iYato 的贡献 |
| [Homebrew](https://brew.sh/) | `brew install picgo --cask` | macOS | 感谢 @womeimingzi11 的贡献 |
| [AUR](https://aur.archlinux.org/packages/yay) | `yay -S picgo-appimage` | Arch-Linux | 感谢 @houbaron 的贡献 |
## 应用截图
![](https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/picgo-2.0.gif)
![picgo-menubar](https://user-images.githubusercontent.com/12621342/34242310-b5056510-e655-11e7-8568-60ffd4f71910.gif)
## 开发说明
> 目前仅针对 Mac、Windows。Linux 平台并未测试。
如果你想要学习、开发、修改或自行构建 PicGo,可以依照下面的指示:
> 如果想学习 Electron-vue 的开发,可以查看我写的系列教程——[Electron-vue 开发实战](https://molunerfinn.com/tags/Electron-vue/)
1. 你需要有 Node、Git 环境,了解 npm 的相关知识。
2. `git clone https://github.com/Molunerfinn/PicGo.git` 并进入项目。
3. `pnpm` 下载依赖。注意如果你没有 `pnpm`,请去 [官网](https://pnpm.io/installation) 下载安装后再使用。 **用 `pnpm install` 将导致未知错误!**
4. Mac 需要有 Xcode 环境,Windows 需要有 VS 环境。
5. 如果需要贡献代码,可以参考[贡献指南](./CONTRIBUTING.md)。
### 开发模式
输入 `pnpm run dev` 进入开发模式,开发模式具有热重载特性。不过需要注意的是,开发模式不稳定,会有进程崩溃的情况。此时需要:
```bash
ctrl+c # 退出开发模式
pnpm run dev # 重新进入开发模式
```
**注:Windows 开发模式运行之后会在底部任务栏的右下角应用区出现 PicGo 的应用图标。**
### 生产模式
如果你需要自行构建,可以 `pnpm run build` 开始进行构建。构建成功后,会在 `dist` 目录里出现构建成功的相应安装文件。
**注意**:如果你的网络环境不太好,可能会出现 `electron-builder` 下载 `electron` 二进制文件失败的情况。这个时候需要在 build 之前指定一下 `electron` 的源为国内源:
```bash
export ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
# 在 Windows 上,则可以使用 set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ (无需引号)
pnpm 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 的手机版 App(支持 Android 和 iOS )。
- [PicHoro](https://github.com/Kuingsmile/PicHoro):另一款支持 PicGo 配置的手机版 App(暂时只支持 Android)。
## 赞助
如果你喜欢 PicGo 并且它对你确实有帮助,欢迎给我打赏一杯咖啡哈~
支付宝:
![](https://user-images.githubusercontent.com/12621342/34188165-e7cdf372-e56f-11e7-8732-1338c88b9bb7.jpg)
微信:
![](https://user-images.githubusercontent.com/12621342/34188201-212cda84-e570-11e7-9b7a-abb298699d85.jpg)
GitHub Sponsors
[![Sponsor PicGo on GitHub](https://img.shields.io/badge/Sponsor-PicGo-blue.svg?style=flat-square)](https://github.com/sponsors/Molunerfinn)
## License
[MIT](http://opensource.org/licenses/MIT)
Copyright (c) 2017 - Now Molunerfinn
-32
View File
@@ -1,32 +0,0 @@
# Commented sections below can be used to run tests on the CI server
# https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing
version: 0.1.{build}
branches:
only:
- master
image: Visual Studio 2017
platform:
- x64
cache:
- '%APPDATA%\npm-cache'
- '%USERPROFILE%\.electron'
- '%USERPROFILE%\AppData\Local\Yarn\cache'
init:
- git config --global core.autocrlf input
install:
- ps: Install-Product node 16 x64
- git reset --hard HEAD
- yarn
- node --version
build_script:
#- yarn test
- yarn release
- yarn upload-dist
test: false
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

+129
View File
@@ -0,0 +1,129 @@
# PicGo 2.4.0 Changelog
## Features
- Add filename display in the gallery (#1050).
- Add default placeholders when images/URLs cannot be shown in the gallery or tray window (#1050).
![image](https://user-images.githubusercontent.com/12621342/210806238-e77bc3b1-5e32-4c9f-af5c-278c5706450d.png)
- Add filename display in the macOS tray window (#1054).
- Allow multiple configs per uploader type and choose one for upload (thanks @STDSuperman, #1016).
![](https://user-images.githubusercontent.com/44311619/203093104-9537e08a-2ef0-450d-a59d-c470dbcdd6c8.png)
- Allow the uploader selection menu to pick a specific config entry for that uploader type.
![image](https://user-images.githubusercontent.com/12621342/210804413-4f78804f-a451-4ca5-93a3-63d461261b18.png)
- Add Dock icon visibility option (#1045).
<img width="787" alt="image" src="https://github.com/Molunerfinn/PicGo/assets/12621342/bb9492f1-6522-45ce-ae5d-c614901d8b06" />
- Add PicGo Repair Toolbox for self-diagnosis.
<img width="324" alt="image" src="https://github.com/Molunerfinn/PicGo/assets/12621342/98cf6e64-7313-4ebe-83d0-aa86e4514109" />
- Add option to encode/escape output URLs (#731).
<img width="775" alt="image" src="https://github.com/Molunerfinn/PicGo/assets/12621342/6e0c13dd-3404-4b07-9c99-412321835b27" />
- Support drag-and-drop uploads for arbitrary file formats (#1052).
- Tencent COS: add Endpoint and Intelligent Compression settings (thanks @palmcivet @yc910920).
![image](https://github.com/Molunerfinn/PicGo/assets/12621342/6e623751-d330-47f8-89bc-986031452914)
- Add Markdown-rendered tips for uploader config (see tcyun uploader config).
![image](https://github.com/Molunerfinn/PicGo/assets/12621342/2f9b8734-717a-4f49-b397-1ddb9437bc3c)
- Show the active uploader config name in the upload screen.
![image](https://github.com/Molunerfinn/PicGo/assets/12621342/52c58fff-4810-4b91-92d7-5ade512eaca8)
- Gallery toolbar: bulk replace image URL host (#875).
Note: select images first; you can filter by uploader; e.g., replace `https://www.a.com/...` with `https://www.b.com/...`.
![image](https://github.com/Molunerfinn/PicGo/assets/12621342/ee314dfc-7699-4ceb-8638-cafe7948bd5a)
- Add “Startup Mode”: Silent (default) / Main Window; Windows/Linux also support Mini Window (#915).
<img width="577" alt="image" src="https://github.com/Molunerfinn/PicGo/assets/12621342/7e63bb5c-44b0-480e-824f-3c2edbed4fbb" />
- PicGo Server: support multipart form uploads (field `files`, #428, thanks @happy-game).
<img width="951" alt="image" src="https://github.com/user-attachments/assets/14244f1d-60f5-487f-bde6-6d0e009645fb" />
## Bug Fixes
- Fix Windows context-menu config script generation (#1019).
- Fix custom link URL encoding (#1112).
- Fix potential logging dead loop (#1101).
- Fix drag-to-tray upload error (#1107).
- Fix filename encoding (#1121).
- Prevent errors on GitHub uploads with duplicate filenames.
- Fix beta.2 style issues.
- Fix slow filename display in the rename window (#1130).
- Fix “open config file” actually opening the log file (#1163).
- Fix plugin config dialog not reading/saving correctly.
- Fix inability to add new uploader configs (created as edit) (#1198, #1196).
- Fix macOS context menu disappearing (thanks @muwoo, #1179).
- Fix macOS tray window flashing after right-click (thanks @QThans, #1217).
- Fix uploader config page not scrollable when many fields (#1237).
- Fix Tencent COS URL encoding (#1265).
- Fix plugin list search not working (#1297).
- Fix clipboard filename losing “seconds” (#1293).
- Fix macOS tray copy-link failing on click (#1280, #1210).
- Fix auto-copy URL toggle not turning off (#1294, thanks @happy-game).
- Fix Wayland clipboard image upload issue (#1261, thanks @happy-game).
- Fix URL uploads with Chinese characters being encoded in filenames (#1339).
## Other
- COS distribution paused due to malicious traffic charges.
- Upgrade to Vue3.
- Refactor parts of the code.
- Refactor parts of the code (again).
- Add more detailed hotkey logs (#1031).
- Known issue: some button styles were incorrect (fixed in later betas).
- Update README scoop installation section (thanks @wuhang2003).
----------
## Features
- 新增 相册页新增文件名展示,参考 #1050
- 新增 相册中和顶部栏窗口中无法展示的图片或者 url 将会展示默认图片,参考#1050
![image](https://user-images.githubusercontent.com/12621342/210806238-e77bc3b1-5e32-4c9f-af5c-278c5706450d.png)
- 新增 macOS 顶部栏窗口新增文件名展示,参考 #1054
- 新增 同种类型图床支持多份配置,上传时可以指定某一份配置进行上传。感谢 @STDSuperman ,参考 #1016
![](https://user-images.githubusercontent.com/44311619/203093104-9537e08a-2ef0-450d-a59d-c470dbcdd6c8.png)
- 新增 选择图床的菜单可以支持选择该图床类型某一项具体配置
![image](https://user-images.githubusercontent.com/12621342/210804413-4f78804f-a451-4ca5-93a3-63d461261b18.png)
- 新增 显示 dock 栏图标选项,参考 #1045
<img width="787" alt="image" src="https://github.com/Molunerfinn/PicGo/assets/12621342/bb9492f1-6522-45ce-ae5d-c614901d8b06" />
- 新增 PicGo 修复工具箱,可以自行排查一些问题
<img width="324" alt="image" src="https://github.com/Molunerfinn/PicGo/assets/12621342/98cf6e64-7313-4ebe-83d0-aa86e4514109" />
- 新增 对输出的 URL 进行 encode 转义的选项,参考 #731
<img width="775" alt="image" src="https://github.com/Molunerfinn/PicGo/assets/12621342/6e0c13dd-3404-4b07-9c99-412321835b27" />
- 新增 支持拖拽任意格式文件上传,参考 #1052
- 新增 腾讯云 COS 支持 `Endpoint` 配置 和 `极智压缩` 配置。 感谢 @palmcivet @yc910920 的贡献!
![image](https://github.com/Molunerfinn/PicGo/assets/12621342/6e623751-d330-47f8-89bc-986031452914)
- 新增 `tips` 配置渲染的支持,可以动态渲染 Uploader config 的 tips。支持 markdown 格式。参考 [tcyun uploader config](https://github.com/PicGo/PicGo-Core/blob/dev/src/plugins/uploader/tcyun.ts#L280)
![image](https://github.com/Molunerfinn/PicGo/assets/12621342/2f9b8734-717a-4f49-b397-1ddb9437bc3c)
- 新增 上传界面展示当前图床使用的配置名
![image](https://github.com/Molunerfinn/PicGo/assets/12621342/52c58fff-4810-4b91-92d7-5ade512eaca8)
- 新增 相册页工具栏,目前内置 `批量修改图片 URL HOST 的功能` 。参考 #875
注意:需要先选中指定的图片,然后会根据已选中的图片进行修改,你可以通过图床筛选功能只筛选出需要修改的图片。
例如,你有一批图片都是 `https://www.a.com/...` 打头的 URL,你想把 `www.a.com` 批量修改成 `www.b.com` ,就可以用这个功能
![image](https://github.com/Molunerfinn/PicGo/assets/12621342/ee314dfc-7699-4ceb-8638-cafe7948bd5a)
- 新增 `启动模式`,可以设置启动的时候是否要打开窗口。全平台支持 `静默启动`(默认值) & `打开主窗口`Windows 和 Linux 额外支持 `打开 Mini 窗口`。参考 #915
<img width="577" alt="image" src="https://github.com/Molunerfinn/PicGo/assets/12621342/7e63bb5c-44b0-480e-824f-3c2edbed4fbb" />
- 新增 PicGo Server 支持表单形式上传图片。参考 #428 ,感谢 @happy-game
表单字段为 files。参考截图:
<img width="951" alt="image" src="https://github.com/user-attachments/assets/14244f1d-60f5-487f-bde6-6d0e009645fb" />
## Bug Fixes
- 修复 windows 右键菜单配置生成脚本,参考 #1019
- 修复 自定义链接 URL encode 问题,参考 #1112
- 修复 日志写入可能存在死循环问题,参考 #1101
- 修复 拖拽文件到顶部栏图标报错问题,参考 #1107
- 修复 文件名 encode 问题。参考 #1121
- 修复 GitHub 重名文件上传不再报错。
- 修复 beta.2 版本部分样式问题
- 修复 重命名窗口某些情况下显示文件名过慢的问题,参考 #1130
- 修复 打开配置文件打开的是日志文件的 bug,参考 #1163
- 修复 插件配置弹窗打开后无法正确读取和保存配置的问题。
- 修复 无法新增图床配置的问题(新增变成了编辑)。参考 #1198#1196
- 修复 macOS 右键菜单消失问题。感谢 @muwoo 。参考 #1179
- 修复 macOS 顶部栏窗口右键之后一闪而过的问题。 感谢 @QThans。 参考 #1217
- 修复 图床配置页面配置项过多时无法滚动到底部的问题。参考 #1237
- 修复 腾讯云 COS URL encode 的问题。参考 #1265
- 修复 插件列表无法搜索的问题。 参考 #1297
- 修复 剪贴板文件名丢失 `秒` 的问题。 参考 #1293
- 修复 macOS 顶部栏点击图片复制链接失效的问题。 参考 #1280 , #1210
- 修复 自动复制 URL 这个开关无法被关闭的问题。参考 #1294 ,感谢 @happy-game
- 修复 wayland 里无法使用剪贴板图片的问题。参考 #1261 ,感谢 @happy-game
- 修复 直接通过 URL 上传图片的时候,带有汉字的 URL 上传后,文件名被 encode 的问题。 参考 #1339
## Other
- 由于 PicGo 存储的 COS 空间被恶意刷大量流量导致欠费,暂时停止 COS 渠道的 PicGo 分发。
- 更新至 Vue3
- 重构部分代码
- 重构部分代码
- 补充更详细的快捷键相关日志,参考 #1031
- 已知问题:部分按钮样式有点问题,下个版本会修复
- 更新 README中的 scoop 下载安装的部分,感谢 @wuhang2003
+31
View File
@@ -0,0 +1,31 @@
# PicGo 2.4.1 Changelog
## Features
- Add `showMenubarIcon` setting to control the visibility of the macOS menu bar icon. See #1222 for details.
## Bug Fixes
- Fix: An issue where the macOS menu bar window could not read images from the clipboard. See https://github.com/Molunerfinn/PicGo/issues/1310 for details.
- Fix: An issue where the macOS Intel build could not be opened. Please take a look at #1363 and #1310 for details.
## Other
- Upgraded Electron to v38 and migrated the underlying build framework to Electron-vite.
- Unified the main window UI on Windows/Linux to match the macOS style.
<img width="821" height="472" alt="image" src="https://github.com/user-attachments/assets/dbafadeb-7a9f-40e5-bd87-d4012c9a3902" />
- Updated ARM64 builds for Windows & Linux, and added Linux deb packages.
----------
## Features
- 新增 `showMenubarIcon` 配置项,用于控制 macOS 顶部栏图标的显示与隐藏。参考 #1222
## Bug Fixes
- 修复 macOS 顶部栏窗口无法获取剪贴板图片的问题。参考 https://github.com/Molunerfinn/PicGo/issues/1310
- 修复 macOS intel 架构 app 无法打开的问题。参考 #1363, #1310
## Other
- 更新 Electron 版本到 38 以及切换底层开发框架到 Electron-vite
- 更新 Windows\Linux 系统现在主窗口的样式跟 macOS 一致了
<img width="821" height="472" alt="image" src="https://github.com/user-attachments/assets/dbafadeb-7a9f-40e5-bd87-d4012c9a3902" />
- 更新 Windows & Linux 平台的 ARM64 架构构建产物;新增 Linux `deb` 构建产物。
+48
View File
@@ -0,0 +1,48 @@
# gen_changelog.md
This guide describes how to generate a consolidated changelog for any PicGo release series (e.g., 2.4.0, 2.5.0) from the GitHub release notes of its betas and finals.
## Source of truth
- Use GitHub release pages only (e.g., `https://github.com/Molunerfinn/PicGo/releases/tag/vX.Y.Z-beta.N` and `vX.Y.Z` if present). You can fetch these via MCP GitHub APIs (e.g., `get_release_by_tag`, `list_releases`) instead of manual browsing.
- Do **not** use `CHANGELOG.md` in the repo.
- Preserve all text and images exactly as written in the releases.
## Structure
- Target file: `changelog/X.Y.Z.md` (replace with the series version).
- Three top-level sections: `## Features`, `## Bug Fixes`, `## Other`.
- Do **not** nest content under individual beta version headers; merge all items into these sections.
- Keep items in chronological order (early → late) within each section, mirroring the beta sequence.
- Keep inline issue references, thanks, and notes as-is.
- Remove download-link sections entirely.
- After the English sections, insert a line with `----------`, then add a full Chinese translation with the same structure/content (including images).
## Formatting rules
- Markdown, plain ASCII.
- Bullet lists only; no numbered lists required.
- Indent images under their bullet with two spaces to keep association clear.
- Keep inline HTML image tags from the releases (e.g., `<img width="...">`) untouched.
- Keep line breaks and multi-line notes intact.
- Do not reword or summarize; copy content verbatim except for removing beta headers and download sections.
- Chinese translation must mirror the English bullets in order and content (keep images alongside the translated bullets).
## Regeneration steps
1) Collect all release bodies for the target series (e.g., `vX.Y.Z-beta.0…N` and, if present, `vX.Y.Z`) from GitHub releases.
2) For each release, copy bullets and images into the appropriate section:
- Features ↔ “Feature(s)” or “Features” blocks.
- Bug Fixes ↔ “Bug Fixes” blocks.
- Other ↔ “Other”, “Notice”, or misc notes that are not features/bugs.
3) Omit “国内可下载链接” (or any download links) entirely.
4) Drop beta subheadings; keep only section-level bullets in chronological order.
5) Ensure images remain adjacent to their bullets with indentation.
6) After the English sections are complete, insert `----------` on its own line.
7) Append the Chinese translation, preserving bullet order and images, under `## Features`, `## Bug Fixes`, `## Other` again (same section titles, just Chinese content; no “(Chinese)” suffix).
8) Save the result to `changelog/X.Y.Z.md`.
## Quick checklist
- [ ] All features present with images kept.
- [ ] All bug fixes present.
- [ ] All “Other” notes present.
- [ ] No beta headers.
- [ ] No download links.
- [ ] Chronological ordering preserved.
- [ ] Chinese translation present with matching bullets/images after `----------`.
+3 -3
View File
@@ -7,13 +7,13 @@
small(v-if="version") {{ version }}
h2.desc 图片上传+管理新体验
button.download(@click="goLink('https://github.com/Molunerfinn/picgo/releases')") 免费下载
button.download(@click="goLink('https://picgo.github.io/PicGo-Doc/zh/guide/')") 查看文档
button.download(@click="goLink('https://picgo.github.io/PicGo-Doc/guide/')") 查看文档
h3.desc
| 基于#[a(href="https://github.com/SimulatedGREG/electron-vue" target="_blank") electron-vue]开发
h3.desc
| 支持macOS,Windows,Linux
h3.desc
| 支持#[a(href="https://picgo.github.io/PicGo-Doc/zh/guide/config.html#%E6%8F%92%E4%BB%B6%E8%AE%BE%E7%BD%AE%EF%BC%88v2-0%EF%BC%89" target="_blank") 插件系统]让PicGo更强大
| 支持#[a(href="https://picgo.github.io/PicGo-Doc/guide/config.html#%E6%8F%92%E4%BB%B6%E8%AE%BE%E7%BD%AE%EF%BC%88v2-0%EF%BC%89" target="_blank") 插件系统]让PicGo更强大
#container.container-fluid
.row.ex-width
img.gallery.col-xs-10.col-xs-offset-1.col-md-offset-2.col-md-8(src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/picgo-site/first.png")
@@ -30,7 +30,7 @@
</template>
<script>
export default {
name: '',
name: 'HomePage',
data () {
return {
version: '',
+107
View File
@@ -0,0 +1,107 @@
import type { Configuration } from 'electron-builder'
const config: Configuration = {
appId: 'com.molunerfinn.picgo',
productName: 'PicGo',
// publish: [
// {
// provider: 'github',
// owner: 'Molunerfinn',
// repo: 'PicGo',
// releaseType: 'draft'
// }
// ],
// temporarily disable auto update feature
publish: [],
files: [
'dist_electron/**/*',
'public/**/*',
'package.json',
'!node_modules/@babel/**/*',
"!**/node_modules/typescript{,/**}"
],
extraResources: [
{
from: 'public',
to: 'public'
}
],
dmg: {
contents: [
{
x: 410,
y: 150,
type: 'link',
path: '/Applications'
},
{
x: 130,
y: 150,
type: 'file'
}
]
},
mac: {
icon: 'build/icons/512x512.png',
extendInfo: {
LSUIElement: 0
},
target: [
{
target: 'dmg',
arch: ['x64', 'arm64']
}
],
artifactName: 'PicGo-${version}-${arch}.${ext}'
},
win: {
icon: 'build/icons/icon.ico',
artifactName: 'PicGo-${version}-${arch}.exe',
target: [
{
target: 'nsis',
arch: ['x64']
},
{
target: 'nsis',
arch: ['ia32']
},
{
target: 'nsis',
arch: ['arm64']
}
]
},
nsis: {
shortcutName: 'PicGo',
oneClick: false,
allowToChangeInstallationDirectory: true,
buildUniversalInstaller: false,
include: 'build/installer.nsh'
},
linux: {
executableName: 'PicGo',
icon: 'build/icons/512x512.png',
artifactName: 'PicGo-${version}-${arch}.${ext}',
target: [
{
target: 'AppImage',
arch: ['x64', 'arm64']
},
{
target: 'deb',
arch: ['x64', 'arm64']
},
{
target: 'snap',
arch: ['x64']
}
],
maintainer: 'Molunerfinn',
category: 'Utility',
publish: []
}
}
export default config
+58
View File
@@ -0,0 +1,58 @@
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
// temp for webUtils
import electronRenderer from '@molunerfinn/vite-plugin-electron-renderer'
import { resolve } from 'path'
const alias = {
'@': resolve(__dirname, 'src/renderer'),
'~': resolve(__dirname, 'src'),
'#': resolve(__dirname, 'src/universal'),
root: resolve(__dirname, '.'),
apis: resolve(__dirname, 'src/main/apis'),
'@core': resolve(__dirname, 'src/main/apis/core')
}
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
resolve: { alias },
build: {
outDir: 'dist_electron/main',
rollupOptions: {
input: {
index: resolve(__dirname, 'src/background.ts')
}
}
}
},
preload: {
plugins: [externalizeDepsPlugin()],
resolve: { alias },
build: {
outDir: 'dist_electron/preload',
rollupOptions: {
input: {
index: resolve(__dirname, 'src/preload/index.ts')
}
}
}
},
renderer: {
root: 'src/renderer',
publicDir: resolve(__dirname, 'src/renderer/public'),
resolve: { alias },
plugins: [vue(), electronRenderer()],
build: {
outDir: 'dist_electron/renderer',
rollupOptions: {
input: {
index: resolve(__dirname, 'src/renderer/index.html')
}
}
},
server: {
port: 5173
}
}
})
+126
View File
@@ -0,0 +1,126 @@
const path = require('node:path')
const eslintJs = require('@eslint/js')
const tsPlugin = require('@typescript-eslint/eslint-plugin')
const tsParser = require('@typescript-eslint/parser')
const importPlugin = require('eslint-plugin-import')
const promisePlugin = require('eslint-plugin-promise')
const vuePlugin = require('eslint-plugin-vue')
const isProduction = process.env.NODE_ENV === 'production'
const vueConfigs = vuePlugin.configs['flat/recommended'].map(config => ({
...config,
languageOptions: {
...(config.languageOptions || {}),
parserOptions: {
...(config.languageOptions?.parserOptions || {}),
parser: tsParser,
ecmaVersion: 'latest',
sourceType: 'module',
extraFileExtensions: ['.vue']
}
}
}))
const tsConfigs = tsPlugin.configs['flat/recommended'].map(config => ({
...config,
files: config.files || ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
languageOptions: {
...(config.languageOptions || {}),
parser: tsParser,
parserOptions: {
...(config.languageOptions?.parserOptions || {}),
ecmaVersion: 'latest',
sourceType: 'module',
extraFileExtensions: ['.vue'],
project: path.join(__dirname, 'tsconfig.json'),
tsconfigRootDir: __dirname
}
},
rules: {
...(config.rules || {}),
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrors: 'none'
}],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/no-empty-object-type': 'off'
}
}))
module.exports = [
{
ignores: [
'dist/**',
'dist_electron/**',
'build/**',
'test/unit/coverage/**',
'test/unit/*.js',
'test/e2e/*.js',
'node_modules/**'
]
},
{
name: 'eslint/base',
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
__static: 'readonly'
}
},
plugins: {
import: importPlugin,
promise: promisePlugin
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.d.ts', '.vue']
}
}
},
rules: {
...eslintJs.configs.recommended.rules,
...importPlugin.configs.recommended.rules,
...promisePlugin.configs.recommended.rules,
'import/named': 'off',
'import/no-named-as-default-member': 'off',
'import/no-unresolved': 'off',
'promise/catch-or-return': 'off',
'promise/always-return': 'off',
'no-console': 'off',
'no-debugger': isProduction ? 'error' : 'off',
'no-async-promise-executor': 'off',
'no-empty': ['error', { allowEmptyCatch: true }],
'no-unused-vars': 'off'
}
},
...vueConfigs,
{
files: ['*.vue', '**/*.vue'],
rules: {
'vue/no-v-html': 'off',
'vue/attribute-hyphenation': 'off'
}
},
...tsConfigs,
{
files: ['**/*.{ts,tsx,vue}'],
rules: {
'no-undef': 'off'
}
},
{
files: ['**/*.d.ts'],
rules: {
'no-var': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-explicit-any': 'off'
}
}
]
+107 -86
View File
@@ -1,27 +1,114 @@
{
"name": "picgo",
"version": "2.4.0-beta.0",
"version": "2.4.1",
"private": true,
"main": "dist_electron/main/index.js",
"description": "A powerful & simple image uploader for creators.",
"author": {
"name": "Molunerfinn",
"email": "marksz@teamsz.xyz"
},
"scripts": {
"dev": "vue-cli-service electron:serve",
"build": "vue-cli-service electron:build",
"lint": "vue-cli-service lint",
"build": "electron-vite build && electron-builder --config electron-builder.config.ts",
"build:win": "npm run build && electron-builder --config electron-builder.config.ts --win",
"build:mac": "npm run build && electron-builder --config electron-builder.config.ts --mac",
"build:linux": "npm run build && electron-builder --config electron-builder.config.ts --linux",
"lint": "eslint --ext .js,.jsx,.ts,.tsx,.vue src/",
"tsc": "tsc --noEmit",
"bump": "bump-version",
"cz": "git-cz",
"dev": "electron-vite dev",
"preview": "electron-vite preview",
"gen-i18n": "node ./scripts/gen-i18n-types.js",
"lint:fix": "eslint --fix --ext .js,.jsx,.ts,.tsx,.vue src/",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps",
"cz": "git-cz",
"bump": "bump-version",
"release": "vue-cli-service electron:build --publish always",
"upload-dist": "node ./scripts/upload-dist-to-cos.js",
"gen-i18n": "node ./scripts/gen-i18n-types.js"
"upload-dist": "node ./scripts/upload-dist.js",
"lint:dpdm": "dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/background.ts"
},
"main": "background.js",
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@picgo/i18n": "^1.0.0",
"@picgo/store": "^2.1.0",
"@picgo/video-duration": "^1.0.1",
"axios": "^0.19.0",
"clip-filepaths": "^0.3.0",
"compare-versions": "^4.1.3",
"core-js": "^3.27.1",
"dayjs": "^1.11.19",
"element-plus": "^2.3.7",
"epipebomb": "^1.0.0",
"fs-extra": "^10.0.0",
"js-yaml": "^4.1.0",
"keycode": "^2.2.0",
"lodash": "^4.17.21",
"lodash-id": "^0.14.0",
"lowdb": "^1.0.0",
"marked": "^7.0.4",
"mime-types": "^3.0.2",
"mitt": "^3.0.1",
"multer": "^1.4.5-lts.1",
"picgo": "^1.6.4",
"qrcode.vue": "^3.3.3",
"semver": "^7.7.3",
"shell-path": "2.1.0",
"systeminformation": "^5.27.14",
"tunnel": "^0.0.6",
"uuid": "^9.0.0",
"vue": "^3.3.4",
"vue-router": "^4.2.2",
"vue3-lazyload": "^0.3.6",
"vue3-photo-preview": "^0.3.0",
"write-file-atomic": "^7.0.0"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.276.0",
"@aws-sdk/lib-storage": "^3.276.0",
"@eslint/js": "^9.39.1",
"@molunerfinn/vite-plugin-electron-renderer": "^0.14.7",
"@picgo/bump-version": "^2.0.0",
"@types/electron-devtools-installer": "^2.2.0",
"@types/fs-extra": "^9.0.13",
"@types/inquirer": "^6.5.0",
"@types/js-yaml": "^4.0.5",
"@types/lodash": "^4.17.21",
"@types/lowdb": "^1.0.9",
"@types/multer": "^1.4.12",
"@types/node": "^20",
"@types/request-promise-native": "^1.0.17",
"@types/semver": "^7.3.8",
"@types/tunnel": "^0.0.3",
"@types/uuid": "^9.0.2",
"@types/write-file-atomic": "^4.0.0",
"@typescript-eslint/eslint-plugin": "^8.49.0",
"@typescript-eslint/parser": "^8.49.0",
"@vitejs/plugin-vue": "^6.0.2",
"autoprefixer": "^10.4.14",
"commitizen": "^4.3.1",
"conventional-changelog": "^3.1.18",
"cz-customizable": "^7.5.1",
"dotenv": "^16.0.1",
"dpdm": "^3.13.1",
"electron": "^38",
"electron-builder": "26.1.0",
"electron-devtools-installer": "^3.2.0",
"electron-vite": "^4.0.1",
"eslint": "^9.39.1",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-vue": "^10.6.2",
"husky": "^3.1.0",
"postcss": "^8.4.23",
"stylus": "^0.54.7",
"stylus-loader": "^3.0.2",
"tailwindcss": "^3.3.2",
"typescript": "^5.9.3",
"vite": "^7.2.6"
},
"commitlint": {
"extends": [
"./node_modules/@picgo/bump-version/commitlint-picgo"
]
},
"config": {
"commitizen": {
@@ -31,75 +118,9 @@
"config": "./node_modules/@picgo/bump-version/.cz-config.js"
}
},
"commitlint": {
"extends": [
"./node_modules/@picgo/bump-version/commitlint-picgo"
]
},
"dependencies": {
"@picgo/i18n": "^1.0.0",
"@picgo/store": "^2.0.4",
"axios": "^0.19.0",
"compare-versions": "^4.1.3",
"core-js": "^3.3.2",
"element-ui": "^2.13.0",
"fs-extra": "^10.0.0",
"js-yaml": "^4.1.0",
"keycode": "^2.2.0",
"lodash-id": "^0.14.0",
"lowdb": "^1.0.0",
"picgo": "^1.5.0",
"qrcode.vue": "^1.7.0",
"shell-path": "2.1.0",
"uuidv4": "^6.2.11",
"vue": "^2.6.10",
"vue-gallery": "^2.0.1",
"vue-lazyload": "^1.2.6",
"vue-router": "^3.1.3",
"write-file-atomic": "^4.0.1"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.16.7",
"@picgo/bump-version": "^1.1.2",
"@types/fs-extra": "^9.0.13",
"@types/inquirer": "^6.5.0",
"@types/js-yaml": "^4.0.5",
"@types/lowdb": "^1.0.9",
"@types/node": "^16.10.2",
"@types/request-promise-native": "^1.0.17",
"@types/semver": "^7.3.8",
"@types/write-file-atomic": "^4.0.0",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"@vue/cli-plugin-babel": "^4.0.0",
"@vue/cli-plugin-eslint": "^4.0.0",
"@vue/cli-plugin-router": "^4.0.0",
"@vue/cli-plugin-typescript": "^4.5.13",
"@vue/cli-service": "^4.0.0",
"@vue/eslint-config-standard": "^6.1.0",
"@vue/eslint-config-typescript": "^7.0.0",
"@vue/runtime-dom": "^3.2.45",
"conventional-changelog": "^3.1.18",
"cz-customizable": "^6.2.0",
"dotenv": "^16.0.1",
"electron": "^16.0.6",
"electron-devtools-installer": "^3.2.0",
"eslint": "^7.32.0",
"eslint-config-standard": ">=16.0.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.0.0",
"husky": "^3.1.0",
"stylus": "^0.54.7",
"stylus-loader": "^3.0.2",
"typescript": "^4.4.3",
"vue-cli-plugin-electron-builder": "^2.1.1",
"vue-property-decorator": "^8.3.0",
"vue-template-compiler": "^2.6.10"
},
"resolutions": {
"@types/node": "^16.10.2",
"vue-cli-plugin-electron-builder/**/electron-builder": "23.3.3"
"husky": {
"hooks": {
"commit-msg": "npm run lint:dpdm && commitlint -E HUSKY_GIT_PARAMS"
}
}
}
+12205
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
onlyBuiltDependencies:
- core-js
- ejs
- electron
- electron-winstaller
- esbuild
- husky
- vue-demi
# for multi-arch builds, include both x64 and arm64 versions of electron
# will be deprecated in future(use different arch machine to build different arch binaries)
supportedArchitectures:
os:
- current
cpu:
- x64
- arm64
+2 -1
View File
@@ -1,5 +1,6 @@
module.exports = {
plugins: {
autoprefixer: {}
autoprefixer: {},
tailwindcss: {}
}
}
+51 -5
View File
@@ -1,4 +1,4 @@
LANG_DISPLAY_LABEL: 'English'
LANG_DISPLAY_LABEL: "English"
ABOUT: About
OPEN_MAIN_WINDOW: Open Main Window
CHOOSE_DEFAULT_PICBED: Choose Default Picbed
@@ -24,10 +24,11 @@ FIND_NEW_VERSION: Find New Version
NO_MORE_NOTICE: No More Notice
SHOW_DEVTOOLS: Show Devtools
CURRENT_PICBED: Current Picbed
OPEN_TOOLBOX: Open Toolbox
# ---renderer i18n begin---
CHOOSE_YOUR_DEFAULT_PICBED: 'Choose ${d} as your default picbed:'
CHOOSE_YOUR_DEFAULT_PICBED: "Choose ${d} as your default picbed:"
UPLOAD_AREA: Upload Area
GALLERY: Gallery
PICBEDS_SETTINGS: Picbeds Settings
@@ -53,6 +54,10 @@ CHANGE_IMAGE_URL_SUCCEED: Change Image URL Succeed
COPY_LINK_SUCCEED: Copy Link Succeed
BATCH_COPY_LINK_SUCCEED: Batch Copy Link Succeed
FILE_RENAME: File Rename
COPY_FILE_PATH: Copy file path
OPEN_FILE_PATH: Open file path
SUCCESS: Success
FAILED: Failed
# settings
@@ -80,7 +85,7 @@ SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: Auto Copy URL After Upload
SETTINGS_TIPS_PLACEHOLDER_URL: Use $url to represent url position
SETTINGS_TIPS_PLACEHOLDER_FILENAME: Use $fileName to represent file name position
SETTINGS_TIPS_PLACEHOLDER_EXTNAME: Use $extName to represent file's ext position
SETTINGS_TIPS_SUCH_AS: 'Such as: $url/$fileName'
SETTINGS_TIPS_SUCH_AS: "Such as: $url/$fileName"
SETTINGS_UPLOAD_PROXY: Upload Proxy
SETTINGS_PLUGIN_INSTALL_PROXY: Proxy for Plugin Install
SETTINGS_PLUGIN_INSTALL_MIRROR: Mirror for Plugin Install
@@ -111,12 +116,20 @@ SETTINGS_NOT_CONFIG_OPTIONS: Not Config Options
SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: Use Builtin Clipboard to Upload
SETTINGS_CHOOSE_LANGUAGE: Choose Language
UPLOADER_CONFIG_NAME: Configuration Name
BUILTIN_CLIPBOARD_TIPS: Use builtin clipboard function to upload instead of using scripts
UPLOADER_CONFIG_PLACEHOLDER: Please Enter Configuration Name
SELECTED_SETTING_HINT: Selected
SETTINGS_ENCODE_OUTPUT_URL: Encode Output(or Copyed) URL
SETTINGS_SHOW_DOCK_ICON: Show Dock icon
SETTINGS_SHOW_MENUBAR_ICON: Show Menubar icon
SETTINGS_SHOW_MENUBAR_ICON_TIPS: If both "Show Dock icon" and "Show Menubar icon" are turned off, you won't be able to find PicGo's main window. Edit the config file and set showDockIcon or showMenubarIcon to true to recover.
SETTINGS_STARTUP_MODE: Startup Mode
SETTINGS_STARTUP_MODE_MAIN_WINDOW: Open Main Window
SETTINGS_STARTUP_MODE_MINI_WINDOW: Open Mini Window
SETTINGS_STARTUP_MODE_HIDE: Silent Startup
# shortcut-page
BUILTIN_CLIPBOARD_TIPS: Use builtin clipboard function to upload instead of using scripts
SHORTCUT_NAME: Shortcut Name
SHORTCUT_BIND: Shortcut Binding
SHORTCUT_STATUS: Status
@@ -129,6 +142,13 @@ SHORTCUT_DISABLE: Disable
SHORTCUT_EDIT: Edit
SHORTCUT_CHANGE_UPLOAD: Change Upload Shortcut
# gallery-page
CHANGE_IMAGE_URL_HOST: Change Image URL HOST
NEW_IMAGE_URL_HOST: New Image URL HOST
SELECTED_IMAGE_URL_HOST: Selected Image URL HOST
CHANGE_IMAGE_URL_HOST_RESULT: The result of changing image URL HOST
CHANGE_IMAGE_URL_HOST_WARN: You must select at least one picture first
# tray-page
WAIT_TO_UPLOAD: Wait to Upload
@@ -186,6 +206,32 @@ DISABLE_PLUGIN: Disable Plugin
UNINSTALL_PLUGIN: Uninstall Plugin
UPDATE_PLUGIN: Update Plugin
# toolbox
TOOLBOX: Toolbox
TOOLBOX_TITLE: Troubleshoot PicGo runtime issues
TOOLBOX_SUB_TITLE: Scan the following items immediately to fix usage issues
TOOLBOX_CHECK_CONFIG_FILE_BROKEN: Check if the configuration file is damaged
TOOLBOX_CHECK_GALLERY_FILE_BROKEN: Check if the album file is damaged
TOOLBOX_CHECK_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: Check if there is a problem with clipboard picture upload
TOOLBOX_CHECK_PROBLEM_WITH_PROXY: Check if the proxy settings are normal
TOOLBOX_FIX_DONE_NEED_RELOAD: Repair completed, need to restart to take effect, restart or not
TOOLBOX_CANT_AUTO_FIX: Unable to automatically repair, please repair the following problems yourself
TOOLBOX_START_SCAN: Start scanning
TOOLBOX_RE_SCAN: Re scanning
TOOLBOX_START_FIX: Start fixing
TOOLBOX_SUCCESS_TIPS: Congratulations, no problems were found
TOOLBOX_CHECK_CONFIG_FILE_PATH_TIPS: "The configuration file path is: ${path}"
TOOLBOX_CHECK_CONFIG_FILE_BROKEN_TIPS: The configuration file is damaged
TOOLBOX_CHECK_GALLERY_FILE_PATH_TIPS: "The album file path is: ${path}"
TOOLBOX_CHECK_GALLERY_FILE_BROKEN_TIPS: The album file is damaged
TOOLBOX_CHECK_PROXY_SUCCESS_TIPS: Proxy settings normal
TOOLBOX_CHECK_PROXY_NO_PROXY_TIPS: No proxy settings
TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_CORRECT: Proxy settings incorrect
TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_WORKING: Proxy settings unavailable
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_TIPS: "The temporary folder path for clipboard pictures is: ${path}"
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS: "The temporary folder for clipboard pictures does not exist: ${path}"
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS: "Please create the folder yourself: ${path}"
# tips
TIPS_NOTICE: Tips
TIPS_WARNING: Warning
@@ -196,7 +242,7 @@ TIPS_PLUGIN_OVERWRITE_GALLERY: Plugin is trying to overwrite the album gallery,
TIPS_UPLOAD_NOT_PICTURES: The latest clipboard item is not a picture
TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicGo config file broken, has been restored to default
TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicGo config file broken, has been restored to backup
TIPS_PICGO_BACKUP_FILE_VERSION: 'Backup file version: ${v}'
TIPS_PICGO_BACKUP_FILE_VERSION: "Backup file version: ${v}"
TIPS_CUSTOM_CONFIG_FILE_PATH_ERROR: Custom config file parse error, please check the path content
TIPS_SHORTCUT_MODIFIED_SUCCEED: Shortcut modified successfully
TIPS_SHORTCUT_MODIFIED_CONFLICT: Shortcut conflict, please reset
+47 -1
View File
@@ -24,6 +24,7 @@ FIND_NEW_VERSION: 发现新版本
NO_MORE_NOTICE: 以后不再提醒
SHOW_DEVTOOLS: 打开开发者工具
CURRENT_PICBED: 当前图床
OPEN_TOOLBOX: 打开修复工具箱
# ---renderer i18n begin---
@@ -53,6 +54,10 @@ CHANGE_IMAGE_URL_SUCCEED: 修改图片URL成功
COPY_LINK_SUCCEED: 复制链接成功
BATCH_COPY_LINK_SUCCEED: 批量复制链接成功
FILE_RENAME: 文件改名
COPY_FILE_PATH: 复制文件路径
OPEN_FILE_PATH: 打开文件路径
SUCCESS: 成功
FAILED: 失败
# settings
@@ -114,6 +119,14 @@ BUILTIN_CLIPBOARD_TIPS: 使用内置剪贴板函数而不是调用脚本获取
UPLOADER_CONFIG_NAME: 图床配置名
UPLOADER_CONFIG_PLACEHOLDER: 请输入配置名称
SELECTED_SETTING_HINT: 已选中
SETTINGS_ENCODE_OUTPUT_URL: 输出(复制) URL 时进行转义
SETTINGS_SHOW_DOCK_ICON: 显示 Dock 栏图标
SETTINGS_SHOW_MENUBAR_ICON: 显示顶部栏图标
SETTINGS_SHOW_MENUBAR_ICON_TIPS: 若“显示 Dock 栏图标”和“显示顶部栏图标”都关闭,将会无法找到 PicGo 主界面。需要手动修改配置文件里的 showDockIcon 或 showMenubarIcon 为 true 才能恢复。
SETTINGS_STARTUP_MODE: 启动模式
SETTINGS_STARTUP_MODE_MAIN_WINDOW: 打开主窗口
SETTINGS_STARTUP_MODE_MINI_WINDOW: 打开 Mini 窗口
SETTINGS_STARTUP_MODE_HIDE: 静默启动
# shortcut-page
@@ -129,6 +142,13 @@ SHORTCUT_DISABLE: 禁用
SHORTCUT_EDIT: 编辑
SHORTCUT_CHANGE_UPLOAD: 修改上传快捷键
# gallery-page
CHANGE_IMAGE_URL_HOST: 修改图片 URL HOST
NEW_IMAGE_URL_HOST: 新的图片 URL HOST
SELECTED_IMAGE_URL_HOST: 已选中的图片 URL HOST
CHANGE_IMAGE_URL_HOST_RESULT: 修改图片 URL HOST 结果
CHANGE_IMAGE_URL_HOST_WARN: 你必须先选中至少一张图片
# tray-page
WAIT_TO_UPLOAD: 等待上传
@@ -186,6 +206,32 @@ DISABLE_PLUGIN: 禁用插件
UNINSTALL_PLUGIN: 卸载插件
UPDATE_PLUGIN: 更新插件
# toolbox
TOOLBOX: 工具箱
TOOLBOX_TITLE: 排查 PicGo 运行时问题
TOOLBOX_SUB_TITLE: 立即扫描以下项目,修复使用问题
TOOLBOX_CHECK_CONFIG_FILE_BROKEN: 检查配置文件是否损坏
TOOLBOX_CHECK_GALLERY_FILE_BROKEN: 检查相册文件是否损坏
TOOLBOX_CHECK_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: 检查剪贴板图片上传是否存在问题
TOOLBOX_CHECK_PROBLEM_WITH_PROXY: 检查代理设置是否正常
TOOLBOX_FIX_DONE_NEED_RELOAD: 修复完成,需要重启生效,是否重启
TOOLBOX_CANT_AUTO_FIX: 无法自动修复,请自行修复以下问题
TOOLBOX_START_SCAN: 开始扫描
TOOLBOX_RE_SCAN: 重新扫描
TOOLBOX_START_FIX: 开始修复
TOOLBOX_SUCCESS_TIPS: 恭喜你,没有检查出问题
TOOLBOX_CHECK_CONFIG_FILE_PATH_TIPS: 配置文件路径是:${path}
TOOLBOX_CHECK_CONFIG_FILE_BROKEN_TIPS: 配置文件已损坏
TOOLBOX_CHECK_GALLERY_FILE_PATH_TIPS: 相册文件路径是:${path}
TOOLBOX_CHECK_GALLERY_FILE_BROKEN_TIPS: 相册文件已损坏
TOOLBOX_CHECK_PROXY_SUCCESS_TIPS: 代理设置正常
TOOLBOX_CHECK_PROXY_NO_PROXY_TIPS: 无代理设置
TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_CORRECT: 代理设置不正确
TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_WORKING: 代理设置不可用
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_TIPS: 剪贴板图片临时文件夹路径是:${path}
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS: 剪贴板图片临时文件夹不存在:${path}
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS: 请自行创建文件夹:${path}
# tips
TIPS_NOTICE: 注意
TIPS_WARNING: 警告
@@ -196,7 +242,7 @@ TIPS_PLUGIN_OVERWRITE_GALLERY: 有插件正在试图覆盖相册列表,是否
TIPS_UPLOAD_NOT_PICTURES: 剪贴板最新的一条记录不是图片
TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicGo 配置文件损坏,已经恢复为默认配置
TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicGo 配置文件损坏,已经恢复为备份配置
TIPS_PICGO_BACKUP_FILE_VERSION: '备份文件版本: ${v}'
TIPS_PICGO_BACKUP_FILE_VERSION: "备份文件版本: ${v}"
TIPS_CUSTOM_CONFIG_FILE_PATH_ERROR: 自定义文件解析出错,请检查路径内容是否正确
TIPS_SHORTCUT_MODIFIED_SUCCEED: 快捷键已经修改成功
TIPS_SHORTCUT_MODIFIED_CONFLICT: 快捷键冲突,请重新设置
+48 -2
View File
@@ -24,6 +24,7 @@ FIND_NEW_VERSION: 發現新版本
NO_MORE_NOTICE: 以後不再提醒
SHOW_DEVTOOLS: 開啟開發者工具
CURRENT_PICBED: 當前圖床
OPEN_TOOLBOX: 開啟修復工具箱
# ---renderer i18n begin---
@@ -52,7 +53,11 @@ CHANGE_IMAGE_URL: 修改圖片URL
CHANGE_IMAGE_URL_SUCCEED: 修改圖片URL成功
COPY_LINK_SUCCEED: 複製連結成功
BATCH_COPY_LINK_SUCCEED: 批量複製連結成功
FILE_RENAME: 文件改名
FILE_RENAME: 檔案改名
COPY_FILE_PATH: 複製檔案路徑
OPEN_FILE_PATH: 打開檔案路徑
SUCCESS: 成功
FAILED: 失敗
# settings
@@ -114,6 +119,14 @@ BUILTIN_CLIPBOARD_TIPS: 使用內建剪貼簿函數而不是調用腳本取得
UPLOADER_CONFIG_NAME: 圖床配置名
UPLOADER_CONFIG_PLACEHOLDER: 請輸入配置名稱
SELECTED_SETTING_HINT: 已選中
SETTINGS_ENCODE_OUTPUT_URL: 輸出(複製) URL 時進行轉義
SETTINGS_SHOW_DOCK_ICON: 顯示 Dock 欄圖示
SETTINGS_SHOW_MENUBAR_ICON: 顯示頂部欄圖示
SETTINGS_SHOW_MENUBAR_ICON_TIPS: 若「顯示 Dock 欄圖示」與「顯示頂部欄圖示」都關閉,將會無法找到 PicGo 主介面。需要手動修改配置檔案裡的 showDockIcon 或 showMenubarIcon 為 true 才能恢復。
SETTINGS_STARTUP_MODE: 啟動模式
SETTINGS_STARTUP_MODE_MAIN_WINDOW: 打開主視窗
SETTINGS_STARTUP_MODE_MINI_WINDOW: 打開 Mini 視窗
SETTINGS_STARTUP_MODE_HIDE: 靜默啟動
# shortcut-page
@@ -129,6 +142,13 @@ SHORTCUT_DISABLE: 禁用
SHORTCUT_EDIT: 編輯
SHORTCUT_CHANGE_UPLOAD: 修改上傳快捷鍵
# gallery-page
CHANGE_IMAGE_URL_HOST: 修改圖片 URL HOST
NEW_IMAGE_URL_HOST: 新的圖片 URL HOST
SELECTED_IMAGE_URL_HOST: 已選中的圖片 URL HOST
CHANGE_IMAGE_URL_HOST_RESULT: 修改圖片 URL HOST 結果
CHANGE_IMAGE_URL_HOST_WARN: 你必須先選中至少一張圖片
# tray-page
WAIT_TO_UPLOAD: 等待上傳
@@ -186,6 +206,32 @@ DISABLE_PLUGIN: 禁用插件
UNINSTALL_PLUGIN: 卸載插件
UPDATE_PLUGIN: 更新插件
# toolbox
TOOLBOX: 工具箱
TOOLBOX_TITLE: 排查 PicGo 執行時問題
TOOLBOX_SUB_TITLE: 立即掃描以下項目,修復使用問題
TOOLBOX_CHECK_CONFIG_FILE_BROKEN: 檢查配置文件是否損壞
TOOLBOX_CHECK_GALLERY_FILE_BROKEN: 檢查相冊文件是否損壞
TOOLBOX_CHECK_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: 檢查剪貼板圖片上傳是否存在問題
TOOLBOX_CHECK_PROBLEM_WITH_PROXY: 檢查代理設置是否正常
TOOLBOX_FIX_DONE_NEED_RELOAD: 修復完成,需要重啓生效,是否重啓
TOOLBOX_CANT_AUTO_FIX: 無法自動修復,請自行修復以下問題
TOOLBOX_START_SCAN: 開始掃描
TOOLBOX_RE_SCAN: 重新掃描
TOOLBOX_START_FIX: 開始修復
TOOLBOX_SUCCESS_TIPS: 恭喜你,沒有檢查出問題
TOOLBOX_CHECK_CONFIG_FILE_PATH_TIPS: 配置文件路徑是:${path}
TOOLBOX_CHECK_CONFIG_FILE_BROKEN_TIPS: 配置文件已損壞
TOOLBOX_CHECK_GALLERY_FILE_PATH_TIPS: 相冊文件路徑是:${path}
TOOLBOX_CHECK_GALLERY_FILE_BROKEN_TIPS: 相冊文件已損壞
TOOLBOX_CHECK_PROXY_SUCCESS_TIPS: 代理設置正常
TOOLBOX_CHECK_PROXY_NO_PROXY_TIPS: 無代理設置
TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_CORRECT: 代理設置不正確
TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_WORKING: 代理設置不可用
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_TIPS: 剪貼板圖片臨時文件夾路徑是:${path}
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS: 剪貼板圖片臨時文件夾不存在:${path}
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS: 請自行創建文件夾:${path}
# tips
TIPS_NOTICE: 注意
TIPS_WARNING: 警告
@@ -196,7 +242,7 @@ TIPS_PLUGIN_OVERWRITE_GALLERY: 有插件正在試圖覆蓋相簿列表,是否
TIPS_UPLOAD_NOT_PICTURES: 剪貼簿最新的一條記錄不是圖片
TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicGo 設定檔案已損壞,已經恢復為預設設定
TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicGo 設定檔案已損壞,已經恢復為備份設定
TIPS_PICGO_BACKUP_FILE_VERSION: '備份檔案版本: ${v}'
TIPS_PICGO_BACKUP_FILE_VERSION: "備份檔案版本: ${v}"
TIPS_CUSTOM_CONFIG_FILE_PATH_ERROR: 自訂設定檔案解析出錯,請檢查路徑內容是否正確
TIPS_SHORTCUT_MODIFIED_SUCCEED: 快捷鍵已經修改成功
TIPS_SHORTCUT_MODIFIED_CONFLICT: 快捷鍵衝突,請重新設定
-1
View File
@@ -6,7 +6,6 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>PicGo</title>
</head>
<body>
<noscript>
+12 -12
View File
@@ -17,18 +17,18 @@ if [ "$XDG_SESSION_TYPE" = "x11" ]; then
echo $filePath
fi
elif [ "$XDG_SESSION_TYPE" = "wayland" ]; then
command -v wl-copy >/dev/null 2>&1 || { echo >&1 "no wl-clipboard"; exit 1; }
filePath=`wl-copy -o 2>/dev/null | grep ^file:// | cut -c8-`
if [ ! -n "$filePath" ] ;then
if
wl-copy -t image/png -o >/dev/null 2>&1
then
wl-copy -t image/png image/png -o >$1 2>/dev/null
echo $1
else
echo "no image"
fi
command -v wl-paste >/dev/null 2>&1 || { echo >&1 "no wl-clipboard"; exit 1; }
isImage=`wl-paste --list-types | grep image`
if [ -n "$isImage" ]; then
wl-paste --type image/png > $1 2>/dev/null
echo $1
else
echo $filePath
echo "no image"
exit 1
fi
else
# fallback for unsupported session types
echo >&2 "Error: Unsupported session type '$XDG_SESSION_TYPE'."
echo >&2 "Solution: The variable of XDG_SESSION_TYPE must set as 'x11' or 'wayland'."
exit 1
fi
+39 -23
View File
@@ -1,45 +1,61 @@
// different platform has different format
// macos
// macos (dmg, x64 + arm64)
const darwin = [{
appNameWithPrefix: 'PicGo-',
ext: '.dmg',
arch: '-arm64',
appNameWithPrefix: 'PicGo',
ext: 'dmg',
arch: 'arm64',
'version-file': 'latest-mac.yml'
}, {
appNameWithPrefix: 'PicGo-',
ext: '.dmg',
arch: '-x64',
appNameWithPrefix: 'PicGo',
ext: 'dmg',
arch: 'x64',
'version-file': 'latest-mac.yml'
}]
// linux (AppImage, deb, snap)
const linux = [{
appNameWithPrefix: 'PicGo-',
ext: '.AppImage',
arch: '',
appNameWithPrefix: 'PicGo',
ext: 'AppImage',
arch: 'arm64',
'version-file': 'latest-linux-arm64.yml'
}, {
appNameWithPrefix: 'PicGo',
ext: 'AppImage',
arch: 'x86_64',
'version-file': 'latest-linux.yml'
}, {
appNameWithPrefix: 'picgo_',
ext: '.snap',
arch: '_amd64',
appNameWithPrefix: 'PicGo',
ext: 'deb',
arch: 'arm64',
'version-file': 'latest-linux-arm64.yml'
}, {
appNameWithPrefix: 'PicGo',
ext: 'deb',
arch: 'amd64',
'version-file': 'latest-linux.yml'
}, {
appNameWithPrefix: 'PicGo',
ext: 'snap',
arch: 'amd64',
'version-file': 'latest-linux.yml'
}]
// windows
// windows (nsis, x64 + ia32 + arm64)
const win32 = [{
appNameWithPrefix: 'PicGo-Setup-',
ext: '.exe',
arch: '-ia32',
appNameWithPrefix: 'PicGo',
ext: 'exe',
arch: 'ia32',
'version-file': 'latest.yml'
}, {
appNameWithPrefix: 'PicGo-Setup-',
ext: '.exe',
arch: '-x64',
appNameWithPrefix: 'PicGo',
ext: 'exe',
arch: 'x64',
'version-file': 'latest.yml'
}, {
appNameWithPrefix: 'PicGo-Setup-',
ext: '.exe',
arch: '', // 32 & 64
appNameWithPrefix: 'PicGo',
ext: 'exe',
arch: 'arm64',
'version-file': 'latest.yml'
}]
+33 -17
View File
@@ -1,23 +1,39 @@
const pkg = require('../package.json')
const version = pkg.version
// TODO: use the same name format
const generateURL = (platform, ext, prefix = 'PicGo-') => {
return `https://picgo-1251750343.cos.ap-chengdu.myqcloud.com/${version}/${prefix}${version}${platform}${ext}`
}
const platformExtList = [
['-arm64', '.dmg', 'PicGo-'],
['-x64', '.dmg', 'PicGo-'],
['', '.AppImage', 'PicGo-'],
['-ia32', '.exe', 'PicGo-Setup-'],
['-x64', '.exe', 'PicGo-Setup-'],
['', '.exe', 'PicGo-Setup-'],
['_amd64', '.snap', 'picgo_']
const generateURL = (arch, ext, prefix = 'PicGo-') => `https://release.picgo.app/${version}/${prefix}${version}${arch}${ext}`
const windows = [
{ label: '32 bit', arch: '-ia32', ext: '.exe' },
{ label: '64 bit', arch: '-x64', ext: '.exe' },
{ label: 'ARM64', arch: '-arm64', ext: '.exe' }
]
const links = platformExtList.map(([arch, ext, prefix]) => {
const markdownLink = `[${prefix}${version}${arch}${ext}](${generateURL(arch, ext, prefix)})`
return markdownLink
})
const macos = [
{ label: 'Intel', arch: '-x64', ext: '.dmg' },
{ label: 'Apple Silicon', arch: '-arm64', ext: '.dmg' }
]
console.log(links.join('\n'))
const linux = {
AppImage: [
{ label: '64 bit', arch: '-x64', ext: '.AppImage' },
{ label: 'ARM64', arch: '-arm64', ext: '.AppImage' }
],
Deb: [
{ label: '64 bit', arch: '-x64', ext: '.deb' },
{ label: 'ARM64', arch: '-arm64', ext: '.deb' }
],
Snap: [
{ label: '64 bit', arch: '-x64', ext: '.snap' }
]
}
const renderLine = (items) => items.map(({ label, arch, ext }) => `[${label}](${generateURL(arch, ext)})`).join(' | ')
const sections = [
`### Windows\n- ${renderLine(windows)}`,
`### macOS\n- ${renderLine(macos)}`,
`### Linux\n- AppImage: ${renderLine(linux.AppImage)}\n- Deb: ${renderLine(linux.Deb)}\n- Snap: ${renderLine(linux.Snap)}`
]
console.log(sections.join('\n\n'))
-103
View File
@@ -1,103 +0,0 @@
// upload dist bundled-app to cos
require('dotenv').config()
const crypto = require('crypto')
const fs = require('fs')
const mime = require('mime-types')
const pkg = require('../package.json')
const configList = require('./config')
const axios = require('axios').default
const path = require('path')
const distPath = path.join(__dirname, '../dist_electron')
const BUCKET = 'picgo-1251750343'
// const AREA = 'ap-chengdu'
const VERSION = pkg.version
const FILE_PATH = `${VERSION}/`
const SECRET_ID = process.env.PICGO_ENV_COS_SECRET_ID
const SECRET_KEY = process.env.PICGO_ENV_COS_SECRET_KEY
// https://cloud.tencent.com/document/product/436/7778#signature
/**
* @param {string} fileName
* @returns
*/
const generateSignature = (fileName, folder = FILE_PATH) => {
const secretKey = SECRET_KEY
// const area = AREA
const bucket = BUCKET
const path = folder
const today = Math.floor(new Date().getTime() / 1000)
const tomorrow = today + 86400
const signTime = `${today};${tomorrow}`
const signKey = crypto.createHmac('sha1', secretKey).update(signTime).digest('hex')
const httpString = `put\n/${path}${fileName}\n\nhost=${bucket}.cos.accelerate.myqcloud.com\n`
const sha1edHttpString = crypto.createHash('sha1').update(httpString).digest('hex')
const stringToSign = `sha1\n${signTime}\n${sha1edHttpString}\n`
const signature = crypto.createHmac('sha1', signKey).update(stringToSign).digest('hex')
return {
signature,
signTime
}
}
/**
*
* @param {string} fileName
* @param {Buffer} fileBuffer
* @param {{ signature: string, signTime: string }} signature
* @returns
*/
const getReqOptions = (fileName, fileBuffer, signature, folder = FILE_PATH) => {
return {
method: 'PUT',
url: `http://${BUCKET}.cos.accelerate.myqcloud.com/${encodeURI(folder)}${encodeURI(fileName)}`,
headers: {
Host: `${BUCKET}.cos.accelerate.myqcloud.com`,
Authorization: `q-sign-algorithm=sha1&q-ak=${SECRET_ID}&q-sign-time=${signature.signTime}&q-key-time=${signature.signTime}&q-header-list=host&q-url-param-list=&q-signature=${signature.signature}`,
contentType: mime.lookup(fileName),
useAgent: `PicGo;${pkg.version};null;null`
},
maxContentLength: Infinity,
maxBodyLength: Infinity,
data: fileBuffer,
resolveWithFullResponse: true
}
}
const uploadFile = async () => {
try {
const platform = process.platform
if (configList[platform]) {
let versionFileHasUploaded = false
for (const [index, config] of configList[platform].entries()) {
const fileName = `${config.appNameWithPrefix}${VERSION}${config.arch}${config.ext}`
const filePath = path.join(distPath, fileName)
const versionFilePath = path.join(distPath, config['version-file'])
let versionFileName = config['version-file']
if (VERSION.toLocaleLowerCase().includes('beta')) {
versionFileName = versionFileName.replace('.yml', '.beta.yml')
}
// upload dist file
const signature = generateSignature(fileName)
const reqOptions = getReqOptions(fileName, fs.readFileSync(filePath), signature)
console.log('[PicGo Dist] Uploading...', fileName, `${index + 1}/${configList[platform].length}`)
await axios.request(reqOptions)
// upload version file
if (!versionFileHasUploaded) {
const signature = generateSignature(versionFileName, '')
const reqOptions = getReqOptions(versionFileName, fs.readFileSync(versionFilePath), signature, '')
console.log('[PicGo Version File] Uploading...', versionFileName)
await axios.request(reqOptions)
versionFileHasUploaded = true
}
}
} else {
console.warn('platform not supported!', platform)
}
} catch (e) {
console.error(e)
}
}
uploadFile()
+208
View File
@@ -0,0 +1,208 @@
// upload dist bundled-app to r2
// upload version file to cos
require('dotenv').config()
// const crypto = require('crypto')
// const axios = require('axios').default
const fs = require('fs')
const pkg = require('../package.json')
const configList = require('./config')
const mime = require('mime-types')
const path = require('path')
const distPath = path.join(__dirname, '../dist')
const S3Client = require('@aws-sdk/client-s3').S3Client
const Upload = require('@aws-sdk/lib-storage').Upload
// const BUCKET = 'picgo-1251750343'
// const COS_SECRET_ID = process.env.PICGO_ENV_COS_SECRET_ID
// const COS_SECRET_KEY = process.env.PICGO_ENV_COS_SECRET_KEY
const S3_BUCKET = 'release'
const S3_LEGACY_BUCKET = 'picgo'
// const AREA = 'ap-chengdu'
const VERSION = pkg.version
const FILE_PATH = `${VERSION}/`
const S3_SECRET_ID = process.env.PICGO_ENV_S3_SECRET_ID
const S3_SECRET_KEY = process.env.PICGO_ENV_S3_SECRET_KEY
const S3_ACCOUNT_ID = process.env.PICGO_ENV_S3_ACCOUNT_ID
const S3_LEGACY_SECRET_ID = process.env.PICGO_ENV_S3_LEGACY_SECRET_ID
const S3_LEGACY_SECRET_KEY = process.env.PICGO_ENV_S3_LEGACY_SECRET_KEY
const S3_LEGACY_ACCOUNT_ID = process.env.PICGO_ENV_S3_LEGACY_ACCOUNT_ID
const S3Options = {
credentials: {
accessKeyId: S3_SECRET_ID,
secretAccessKey: S3_SECRET_KEY
},
endpoint: `https://${S3_ACCOUNT_ID}.r2.cloudflarestorage.com`,
sslEnabled: true,
region: 'auto'
}
// for legacy release file fetch
const S3LegacyOptions = {
credentials: {
accessKeyId: S3_LEGACY_SECRET_ID,
secretAccessKey: S3_LEGACY_SECRET_KEY
},
endpoint: `https://${S3_LEGACY_ACCOUNT_ID}.r2.cloudflarestorage.com`,
sslEnabled: true,
region: 'auto'
}
// https://cloud.tencent.com/document/product/436/7778#signature
// /**
// * @param {string} fileName
// * @returns
// */
// const generateSignature = (fileName, folder = FILE_PATH) => {
// const secretKey = COS_SECRET_ID
// // const area = AREA
// const bucket = BUCKET
// const path = folder
// const today = Math.floor(new Date().getTime() / 1000)
// const tomorrow = today + 86400
// const signTime = `${today};${tomorrow}`
// const signKey = crypto.createHmac('sha1', secretKey).update(signTime).digest('hex')
// const httpString = `put\n/${path}${fileName}\n\nhost=${bucket}.cos.accelerate.myqcloud.com\n`
// const sha1edHttpString = crypto.createHash('sha1').update(httpString).digest('hex')
// const stringToSign = `sha1\n${signTime}\n${sha1edHttpString}\n`
// const signature = crypto.createHmac('sha1', signKey).update(stringToSign).digest('hex')
// return {
// signature,
// signTime
// }
// }
// /**
// *
// * @param {string} fileName
// * @param {Buffer} fileBuffer
// * @param {{ signature: string, signTime: string }} signature
// * @returns
// */
// const getReqOptions = (fileName, fileBuffer, signature, folder = FILE_PATH) => {
// return {
// method: 'PUT',
// url: `http://${BUCKET}.cos.accelerate.myqcloud.com/${encodeURI(folder)}${encodeURI(fileName)}`,
// headers: {
// Host: `${BUCKET}.cos.accelerate.myqcloud.com`,
// Authorization: `q-sign-algorithm=sha1&q-ak=${COS_SECRET_KEY}&q-sign-time=${signature.signTime}&q-key-time=${signature.signTime}&q-header-list=host&q-url-param-list=&q-signature=${signature.signature}`,
// contentType: mime.lookup(fileName),
// useAgent: `PicGo;${pkg.version};null;null`
// },
// maxContentLength: Infinity,
// maxBodyLength: Infinity,
// data: fileBuffer,
// resolveWithFullResponse: true
// }
// }
/**
* a backup for version file
*/
// const uploadVersionFile = async () => {
// try {
// const platform = process.platform
// if (configList[platform]) {
// let versionFileHasUploaded = false
// for (const [, config] of configList[platform].entries()) {
// const versionFilePath = path.join(distPath, config['version-file'])
// let versionFileName = config['version-file']
// if (VERSION.toLocaleLowerCase().includes('beta')) {
// versionFileName = versionFileName.replace('.yml', '.beta.yml')
// }
// // upload version file
// if (!versionFileHasUploaded) {
// const signature = generateSignature(versionFileName, '')
// const reqOptions = getReqOptions(versionFileName, fs.readFileSync(versionFilePath), signature, '')
// console.log('[PicGo Version File] Uploading...', versionFileName)
// await axios.request(reqOptions)
// versionFileHasUploaded = true
// }
// }
// } else {
// console.warn('platform not supported!', platform)
// }
// } catch (e) {
// console.error(e)
// }
// }
const uploadDist = async () => {
try {
const platform = process.platform
if (configList[platform]) {
const uploadedVersionFiles = new Set()
for (const [index, config] of configList[platform].entries()) {
const fileName = `${config.appNameWithPrefix}-${VERSION}-${config.arch}.${config.ext}`
const filePath = path.join(distPath, fileName)
const versionFilePath = path.join(distPath, config['version-file'])
let versionFileName = config['version-file']
if (VERSION.toLocaleLowerCase().includes('beta')) {
versionFileName = versionFileName.replace('.yml', '.beta.yml')
}
console.log('[PicGo Dist] Preparing to upload', fileName)
const client = new S3Client(S3Options)
if (fs.existsSync(filePath)) {
const uploadDistToS3 = new Upload({
client,
params: {
Bucket: S3_BUCKET,
Key: `${FILE_PATH}${fileName}`,
Body: fs.createReadStream(filePath),
ContentType: 'application/octet-stream'
}
})
// upload dist file
console.log('[PicGo Dist] Uploading...', fileName, `${index + 1}/${configList[platform].length}`)
uploadDistToS3.on('httpUploadProgress', progress => {
console.log(`[PicGo Dist] Uploading... ${progress.loaded}/${progress.total}`)
})
await uploadDistToS3.done()
} else {
console.warn('[PicGo Dist] File not found:', fileName)
}
// upload version file
if (!uploadedVersionFiles.has(versionFileName) && fs.existsSync(versionFilePath)) {
const uploadVersionFileToS3 = new Upload({
client,
params: {
Bucket: S3_BUCKET,
Key: `${versionFileName}`,
Body: fs.createReadStream(versionFilePath),
ContentType: mime.lookup(versionFileName)
}
})
// upload to legacy bucket as well
// will be deprecated in 2.5.0
const legacyClient = new S3Client(S3LegacyOptions)
const uploadVersionFileToLegacyS3 = new Upload({
client: legacyClient,
params: {
Bucket: S3_LEGACY_BUCKET,
Key: `${versionFileName}`,
Body: fs.createReadStream(versionFilePath),
ContentType: mime.lookup(versionFileName)
}
})
console.log('[PicGo Version File] Uploading...', versionFileName)
await uploadVersionFileToS3.done()
await uploadVersionFileToLegacyS3.done()
uploadedVersionFiles.add(versionFileName)
console.log('[PicGo Version File] Upload successfully')
}
}
} else {
console.warn('platform not supported!', platform)
}
} catch (e) {
console.error(e)
}
}
const main = async () => {
await uploadDist()
}
main()
+2
View File
@@ -1,5 +1,7 @@
import { initStaticPath } from '~/main/utils/env'
import { bootstrap } from '~/main/lifeCycle'
initStaticPath()
bootstrap.launchApp()
/**
-49
View File
@@ -1,49 +0,0 @@
import Vue from 'vue'
import App from './renderer/App.vue'
import router from './renderer/router'
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'
// import { T, i18n } from '#/i18n/index'
// import { handleURLParams } from '@/utils/beforeOpen'
import { i18nManager, T } from './renderer/i18n/index'
webFrame.setVisualZoomLevelLimits(1, 1)
// do here before vue init
// handleURLParams()
Vue.config.productionTip = false
Vue.prototype.$builtInPicBed = [
'smms',
'imgur',
'qiniu',
'tcyun',
'upyun',
'aliyun',
'github'
]
Vue.prototype.$$db = db
Vue.prototype.$http = axios
Vue.prototype.$bus = bus
Vue.prototype.$T = T
Vue.prototype.$i18n = i18nManager
Vue.use(ElementUI)
Vue.use(VueLazyLoad, {
error: `file://${__static.replace(/\\/g, '/')}/unknown-file-type.svg`
})
Vue.mixin(mainMixin)
new Vue({
router,
render: h => h(App)
}).$mount('#app')
initTalkingData()
+2 -3
View File
@@ -10,14 +10,13 @@ import axios from 'axios'
import windowManager from '../window/windowManager'
import { showNotification } from '~/main/utils/common'
import { isDev } from '~/universal/utils/common'
import { STORE_PATH } from '~/main/utils/env'
// for test
const REMOTE_NOTICE_URL = isDev ? 'http://localhost:8181/remote-notice.json' : 'https://picgo-1251750343.cos.accelerate.myqcloud.com/remote-notice.yml'
const REMOTE_NOTICE_URL = isDev ? 'http://localhost:8181/remote-notice.json' : 'https://release.picgo.app/remote-notice.yml'
const REMOTE_NOTICE_LOCAL_STORAGE_FILE = 'picgo-remote-notice.json'
const STORE_PATH = app.getPath('userData')
const REMOTE_NOTICE_LOCAL_STORAGE_PATH = path.join(STORE_PATH, REMOTE_NOTICE_LOCAL_STORAGE_FILE)
class RemoteNoticeHandler {
+6
View File
@@ -0,0 +1,6 @@
import { SHORTKEY_COMMAND_UPLOAD } from '../../core/bus/constants'
import { uploadClipboardFiles } from '../uploader/apis'
export const BuiltinShortKeyMap: Record<string, FN> = {
[SHORTKEY_COMMAND_UPLOAD]: uploadClipboardFiles
}
+18 -4
View File
@@ -8,6 +8,8 @@ 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 { BuiltinShortKeyMap } from './builtin'
import { SHORTKEY_BUILTIN_PREFIX } from '../../core/bus/constants'
class ShortKeyHandler {
private isInModifiedMode: boolean = false
@@ -29,10 +31,14 @@ class ShortKeyHandler {
.forEach(command => {
const config = commands[command]
// if disabled, don't register #534
logger.info(`register builtin shortKey command: [${command}] - [${config.key}]`)
if (config.enable) {
logger.info(`register builtin shortKey command: [${command}] - [${config.key}] successfully`)
globalShortcut.register(config.key, () => {
this.handler(command)
})
} else {
logger.warn(`builtin shortKey command: [${command}] - [${config.key}] register failed, it's disabled`)
}
})
}
@@ -76,7 +82,7 @@ class ShortKeyHandler {
} else {
logger.warn(`${command} do not provide a key to bind`)
}
// if the configfile already had this command
// if the config file already had this command
// then writeFlag -> false
if (writeFlag) {
picgo.saveConfig({
@@ -130,17 +136,25 @@ class ShortKeyHandler {
private async handler (command: string) {
if (this.isInModifiedMode) {
logger.warn(`in modified mode, ignore shortKey command: [${command}]`)
return
}
if (command.includes('picgo:')) {
bus.emit(command)
if (command.includes(SHORTKEY_BUILTIN_PREFIX)) {
const handler = BuiltinShortKeyMap[command]
if (handler) {
logger.info(`get builtin shortKey handler for command: [${command}]`)
return handler()
} else {
logger.warn(`can't find builtin shortKey handler for command: [${command}]`)
}
} else if (command.includes('picgo-plugin-')) {
const handler = shortKeyService.getShortKeyHandler(command)
if (handler) {
logger.info(`get plugin shortKey handler for command: [${command}]`)
return handler(picgo, GuiApi.getInstance())
}
} else {
logger.warn(`can not find command: ${command}`)
logger.warn(`can not find command: [${command}]`)
}
}
+114 -58
View File
@@ -1,12 +1,11 @@
import fs from 'fs-extra'
import {
app,
Menu,
Tray,
dialog,
clipboard,
systemPreferences,
Notification
Notification,
nativeTheme
} from 'electron'
import uploader from 'apis/app/uploader'
import db, { GalleryDB } from '~/main/apis/core/datastore'
@@ -14,15 +13,17 @@ import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
import pasteTemplate from '~/main/utils/pasteTemplate'
import pkg from 'root/package.json'
import { ensureFilePath, handleCopyUrl } from '~/main/utils/common'
import { ensureFilePath, getClipboardFilePathList, handleCopyUrl } from '~/main/utils/common'
import { privacyManager } from '~/main/utils/privacyManager'
// import { T } from '#/i18n'
import { T } from '~/main/i18n'
import { isMacOSVersionGreaterThanOrEqualTo } from '~/main/utils/getMacOSVersion'
import { buildPicBedListMenu } from '~/main/events/remotes/menu'
import { buildPicBedListMenu } from '~/main/events/remotes/picBedListMenu'
import { isLinux, isMacOS } from '~/universal/utils/common'
import { getStaticPath } from '#/utils/staticPath'
let contextMenu: Menu | null
let menu: Menu | null
let tray: Tray | null
// need to build new menu
export function createContextMenu () {
if (process.platform === 'darwin' || process.platform === 'win32') {
const submenu = buildPicBedListMenu()
@@ -131,14 +132,17 @@ export function createContextMenu () {
}
])
}
return contextMenu!
}
const getTrayIcon = () => {
if (process.platform === 'darwin') {
const isMacOSGreaterThan11 = isMacOSVersionGreaterThanOrEqualTo('11')
return isMacOSGreaterThan11 ? `${__static}/menubar-newdarwinTemplate.png` : `${__static}/menubar.png`
return isMacOSGreaterThan11
? getStaticPath('menubar-newdarwinTemplate.png')
: getStaticPath('menubar.png')
} else {
return `${__static}/menubar-nodarwin.png`
return getStaticPath('menubar-nodarwin.png')
}
}
@@ -152,36 +156,27 @@ export function createTray () {
windowManager.get(IWindowList.TRAY_WINDOW)!.hide()
}
createContextMenu()
tray!.popUpContextMenu(contextMenu!)
setTimeout(() => {
tray!.popUpContextMenu(contextMenu!)
}, 0)
})
tray.on('click', (event, bounds) => {
if (process.platform === 'darwin') {
toggleWindow(bounds)
setTimeout(async () => {
const img = clipboard.readImage()
const obj: ImgInfo[] = []
if (!img.isEmpty()) {
// 从剪贴板来的图片默认转为png
// https://github.com/electron/electron/issues/9035
const imgPath = clipboard.read('public.file-url')
if (imgPath) {
const imgPathList = getClipboardFilePathList()
if (imgPathList.length > 0) {
for (const imgPath of imgPathList) {
const decodePath = ensureFilePath(imgPath)
if (decodePath === imgPath) {
obj.push({
imgUrl: imgPath
})
} else {
if (decodePath !== '') {
// 带有中文的路径,无法直接被img.src所使用,会被转义
const base64 = await fs.readFile(decodePath.replace('file://', ''), { encoding: 'base64' })
obj.push({
imgUrl: `data:image/png;base64,${base64}`
})
}
}
} else {
obj.push({
imgUrl: decodePath
})
}
} else {
const img = clipboard.readImage()
if (!img.isEmpty()) {
const imgUrl = img.toDataURL()
// console.log(imgUrl)
obj.push({
width: img.getSize().width,
height: img.getSize().height,
@@ -189,7 +184,9 @@ export function createTray () {
})
}
}
windowManager.get(IWindowList.TRAY_WINDOW)!.webContents.send('clipboardFiles', obj)
windowManager
.get(IWindowList.TRAY_WINDOW)!
.webContents.send('clipboardFiles', obj)
}, 0)
} else {
if (windowManager.has(IWindowList.TRAY_WINDOW)) {
@@ -205,20 +202,21 @@ export function createTray () {
})
tray.on('drag-enter', () => {
if (systemPreferences.isDarkMode()) {
tray!.setImage(`${__static}/upload-dark.png`)
if (nativeTheme.shouldUseDarkColors) {
tray!.setImage(getStaticPath('upload-dark.png'))
} else {
tray!.setImage(`${__static}/upload.png`)
tray!.setImage(getStaticPath('upload.png'))
}
})
tray.on('drag-end', () => {
tray!.setImage(`${__static}/menubar.png`)
const menubarPic = getTrayIcon()
tray!.setImage(menubarPic)
})
// drop-files only be supported in macOS
// so the tray window must be available
tray.on('drop-files', async (event: Event, files: string[]) => {
tray.on('drop-files', async (event, files: string[]) => {
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!
const imgs = await uploader
@@ -227,7 +225,9 @@ export function createTray () {
if (imgs !== false) {
const pasteText: string[] = []
for (let i = 0; i < imgs.length; i++) {
pasteText.push(pasteTemplate(pasteStyle, imgs[i], db.get('settings.customLink')))
pasteText.push(
pasteTemplate(pasteStyle, imgs[i], db.get('settings.customLink'))
)
const notification = new Notification({
title: T('UPLOAD_SUCCEED'),
body: imgs[i].imgUrl!
@@ -243,35 +243,91 @@ export function createTray () {
}
})
// toggleWindow()
} else if (process.platform === 'linux') {
// click事件在Ubuntu上无法触发,Unity不支持(在Mac和Windows上可以触发)
// 需要使用 setContextMenu 设置菜单
} else if (isLinux) {
// click事件在Ubuntu上无法触发,Unity不支持(在Mac和Windows上可以触发)
// 需要使用 setContextMenu 设置菜单
createContextMenu()
tray!.setContextMenu(contextMenu)
}
}
const destroyTray = () => {
if (tray) {
tray.removeAllListeners()
tray.destroy()
tray = null
}
if (windowManager.has(IWindowList.TRAY_WINDOW)) {
windowManager.get(IWindowList.TRAY_WINDOW)!.hide()
}
}
/**
* macOS only: show/hide the menubar (tray) icon.
* For other platforms this keeps the existing behavior (always show tray).
*/
export function handleMenubarIcon (visible?: boolean) {
if (!isMacOS) {
if (!tray) {
createTray()
}
return
}
const shouldShow = visible !== undefined ? visible : (db.get('settings.showMenubarIcon') !== false)
if (shouldShow) {
if (!tray) {
createTray()
}
} else {
destroyTray()
}
}
export function handleDockIcon () {
if (isMacOS) {
if (db.get('settings.showDockIcon') !== false) {
app.dock?.show()
app.dock?.setMenu(createContextMenu())
} else {
app.dock?.hide()
}
}
}
export function createMenu () {
if (menu) {
return menu
}
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()
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)
+9 -8
View File
@@ -3,13 +3,13 @@ import {
WebContents
} from 'electron'
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
import { IRPCActionType, IWindowList } from '#/types/enum'
import uploader from '.'
import pasteTemplate from '~/main/utils/pasteTemplate'
import db, { GalleryDB } from '~/main/apis/core/datastore'
import { handleCopyUrl } from '~/main/utils/common'
import { handleUrlEncode } from '#/utils/common'
import { handleCopyUrl, handleUrlEncodeWithSetting } from '~/main/utils/common'
import { T } from '~/main/i18n/index'
import logger from '@core/picgo/logger'
// import dayjs from 'dayjs'
const handleClipboardUploading = async (): Promise<false | ImgInfo[]> => {
@@ -22,6 +22,7 @@ const handleClipboardUploading = async (): Promise<false | ImgInfo[]> => {
}
export const uploadClipboardFiles = async (): Promise<string> => {
logger.info('upload clipboard file')
const img = await handleClipboardUploading()
if (img !== false) {
if (img.length > 0) {
@@ -41,9 +42,9 @@ export const uploadClipboardFiles = async (): Promise<string> => {
trayWindow?.webContents?.send('clipboardFiles', [])
trayWindow?.webContents?.send('uploadFiles', img)
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents?.send('updateGallery')
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents?.send(IRPCActionType.UPDATE_GALLERY)
}
return handleUrlEncode(img[0].imgUrl as string)
return handleUrlEncodeWithSetting(img[0].imgUrl as string)
} else {
const notification = new Notification({
title: T('UPLOAD_FAILED'),
@@ -57,7 +58,7 @@ export const uploadClipboardFiles = async (): Promise<string> => {
}
}
export const uploadChoosedFiles = async (webContents: WebContents, files: IFileWithPath[]): Promise<string[]> => {
export const uploadSelectedFiles = async (webContents: WebContents, files: IFileWithPath[]): Promise<string[]> => {
const input = files.map(item => item.path)
const imgs = await uploader.setWebContents(webContents).upload(input)
const result = []
@@ -75,13 +76,13 @@ export const uploadChoosedFiles = async (webContents: WebContents, files: IFileW
notification.show()
}, i * 100)
await GalleryDB.getInstance().insert(imgs[i])
result.push(handleUrlEncode(imgs[i].imgUrl!))
result.push(handleUrlEncodeWithSetting(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')
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents?.send(IRPCActionType.UPDATE_GALLERY)
}
return result
} else {
+26 -38
View File
@@ -11,9 +11,9 @@ import db from '~/main/apis/core/datastore'
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
import util from 'util'
import { IPicGo } from 'picgo'
import { showNotification, calcDurationRange, getClipboardFilePath } from '~/main/utils/common'
import { RENAME_FILE_NAME, TALKING_DATA_EVENT } from '~/universal/events/constants'
import type { IPicGo } from 'picgo'
import { showNotification, getClipboardFilePathList } from '~/main/utils/common'
import { GET_RENAME_FILE_NAME, RENAME_FILE_NAME } from '~/universal/events/constants'
import logger from '@core/picgo/logger'
import { T } from '~/main/i18n'
import fse from 'fs-extra'
@@ -21,19 +21,14 @@ import path from 'path'
import { privacyManager } from '~/main/utils/privacyManager'
import writeFile from 'write-file-atomic'
import { CLIPBOARD_IMAGE_FOLDER } from '~/universal/utils/static'
const waitForShow = (webcontent: WebContents) => {
return new Promise<void>((resolve) => {
webcontent.on('did-finish-load', () => {
resolve()
})
})
}
import { cleanupFormUploaderFiles } from '~/main/utils/cleanupFormUploaderFiles'
import { IpcMainEvent } from 'electron/main'
import { dataReportManager } from '~/main/utils/dataReport'
const waitForRename = (window: BrowserWindow, id: number): Promise<string|null> => {
return new Promise((resolve) => {
const windowId = window.id
ipcMain.once(`${RENAME_FILE_NAME}${id}`, (evt: Event, newName: string) => {
ipcMain.once(`${RENAME_FILE_NAME}${id}`, (evt: IpcMainEvent, newName: string) => {
resolve(newName)
window.close()
})
@@ -45,20 +40,6 @@ const waitForRename = (window: BrowserWindow, id: number): Promise<string|null>
})
}
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
@@ -93,14 +74,19 @@ class Uploader {
let name: undefined | string | null
let fileName: string | undefined
if (autoRename) {
fileName = dayjs().add(index, 'ms').format('YYYYMMDDHHmmSSS') + item.extname
fileName = dayjs().add(index, 'ms').format('YYYYMMDDHHmmssSSS') + item.extname
} else {
fileName = item.fileName
}
if (rename) {
const window = windowManager.create(IWindowList.RENAME_WINDOW)!
await waitForShow(window.webContents)
window.webContents.send(RENAME_FILE_NAME, fileName, item.fileName, window.webContents.id)
logger.info('wait for rename window ready...')
ipcMain.on(GET_RENAME_FILE_NAME, (evt) => {
if (evt.sender.id === window.webContents.id) {
logger.info('rename window ready, wait for rename...')
window.webContents.send(RENAME_FILE_NAME, fileName, item.fileName, window.webContents.id)
}
})
name = await waitForRename(window, window.webContents.id)
}
item.fileName = name || fileName
@@ -121,20 +107,20 @@ class Uploader {
async uploadWithBuildInClipboard (): Promise<ImgInfo[]|false> {
let filePath = ''
try {
const imgPath = getClipboardFilePath()
if (!imgPath) {
const imgPath = getClipboardFilePathList()
if (!imgPath.length) {
const nativeImage = clipboard.readImage()
if (nativeImage.isEmpty()) {
return false
}
const buffer = nativeImage.toPNG()
const baseDir = picgo.baseDir
const fileName = `${dayjs().format('YYYYMMDDHHmmSSS')}.png`
const fileName = `${dayjs().format('YYYYMMDDHHmmssSSS')}.png`
filePath = path.join(baseDir, CLIPBOARD_IMAGE_FOLDER, fileName)
await writeFile(filePath, buffer)
return await this.upload([filePath])
} else {
return await this.upload([imgPath])
return await this.upload(imgPath)
}
} catch (e: any) {
logger.error(e)
@@ -156,12 +142,11 @@ class Uploader {
const output = await picgo.upload(img)
if (Array.isArray(output) && output.some((item: ImgInfo) => item.imgUrl)) {
if (this.webContents) {
handleTalkingData(this.webContents, {
dataReportManager.reportUploadData(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)
duration: Date.now() - startTime,
outputList: output
});
}
return output.filter(item => item.imgUrl)
} else {
@@ -177,6 +162,9 @@ class Uploader {
})
}, 500)
return false
} finally {
ipcMain.removeAllListeners(GET_RENAME_FILE_NAME)
cleanupFormUploaderFiles(img)
}
}
}
+7 -13
View File
@@ -1,17 +1,11 @@
const isDevelopment = process.env.NODE_ENV !== 'production'
import { buildRendererUrl } from '~/main/utils/env'
export const TRAY_WINDOW_URL = isDevelopment
? (process.env.WEBPACK_DEV_SERVER_URL as string)
: 'picgo://./index.html'
export const TRAY_WINDOW_URL = buildRendererUrl()
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 SETTING_WINDOW_URL = buildRendererUrl('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 MINI_WINDOW_URL = buildRendererUrl('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'
export const RENAME_WINDOW_URL = buildRendererUrl('rename-page')
export const TOOLBOX_WINDOW_URL = buildRendererUrl('toolbox-page')
+86 -33
View File
@@ -1,21 +1,32 @@
// import path from 'path'
import {
SETTING_WINDOW_URL,
TRAY_WINDOW_URL,
MINI_WINDOW_URL,
RENAME_WINDOW_URL
RENAME_WINDOW_URL,
TOOLBOX_WINDOW_URL
} from './constants'
import { IRemoteNoticeTriggerHook, IWindowList } from '#/types/enum'
import { IStartupMode, IWindowList } from '#/types/enum'
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'
import { remoteNoticeHandler } from '../remoteNotice'
// import { i18n } from '~/main/i18n'
import { T } from '~/main/i18n'
import { isLinux } from '~/universal/utils/common'
import { getStaticPath } from '#/utils/staticPath'
// import { URLSearchParams } from 'url'
const windowList = new Map<IWindowList, IWindowListItem>()
const defaultWebPreferences = {
// preload: path.join(__dirname, '../preload/index.js'),
nodeIntegration: true,
contextIsolation: false,
nodeIntegrationInWorker: true,
backgroundThrottling: false
}
const handleWindowParams = (windowURL: string) => {
// const [baseURL, hash = ''] = windowURL.split('#')
// const search = new URLSearchParams()
@@ -25,6 +36,21 @@ const handleWindowParams = (windowURL: string) => {
return windowURL
}
export const isWindowShouldShowOnStartup = (currentWindow: IWindowList) => {
const startupMode = db.get('settings.startupMode') || (isLinux ? IStartupMode.SHOW_MINI_WINDOW : IStartupMode.HIDE)
switch (currentWindow) {
case IWindowList.MINI_WINDOW: {
return startupMode === IStartupMode.SHOW_MINI_WINDOW
}
case IWindowList.SETTING_WINDOW: {
return startupMode === IStartupMode.SHOW_MAIN_WINDOW
}
default: {
return false
}
}
}
windowList.set(IWindowList.TRAY_WINDOW, {
isValid: process.platform !== 'linux',
multiple: false,
@@ -39,10 +65,7 @@ windowList.set(IWindowList.TRAY_WINDOW, {
transparent: true,
vibrancy: 'ultra-dark',
webPreferences: {
nodeIntegration: !!process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
nodeIntegrationInWorker: true,
backgroundThrottling: false,
...defaultWebPreferences,
webSecurity: false
}
}
@@ -59,6 +82,7 @@ windowList.set(IWindowList.SETTING_WINDOW, {
isValid: true,
multiple: false,
options () {
const showDockIcon = db.get('settings.showDockIcon') !== false
const options: IBrowserWindowOptions = {
height: 450,
width: 800,
@@ -68,30 +92,23 @@ windowList.set(IWindowList.SETTING_WINDOW, {
fullscreenable: false,
resizable: false,
title: 'PicGo',
vibrancy: 'ultra-dark',
transparent: true,
skipTaskbar: !showDockIcon,
titleBarStyle: 'hidden',
webPreferences: {
backgroundThrottling: false,
nodeIntegration: !!process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
nodeIntegrationInWorker: true,
...defaultWebPreferences,
webSecurity: false
}
}
if (process.platform !== 'darwin') {
options.show = false
options.frame = false
options.backgroundColor = '#3f3c37'
options.transparent = false
options.icon = `${__static}/logo.png`
options.icon = getStaticPath('logo.png')
options.skipTaskbar = false
}
return options
},
callback (window, windowManager) {
window.once('show', () => {
remoteNoticeHandler.triggerHook(IRemoteNoticeTriggerHook.SETTING_WINDOW_OPEN)
})
window.loadURL(handleWindowParams(SETTING_WINDOW_URL))
window.on('closed', () => {
bus.emit(TOGGLE_SHORTKEY_MODIFIED_MODE, false)
@@ -113,22 +130,19 @@ windowList.set(IWindowList.MINI_WINDOW, {
const obj: IBrowserWindowOptions = {
height: 64,
width: 64,
show: process.platform === 'linux',
show: isLinux,
frame: false,
fullscreenable: false,
skipTaskbar: true,
resizable: false,
transparent: process.platform !== 'linux',
icon: `${__static}/logo.png`,
icon: getStaticPath('logo.png'),
webPreferences: {
backgroundThrottling: false,
nodeIntegration: !!process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
nodeIntegrationInWorker: true
...defaultWebPreferences
}
}
if (db.get('settings.miniWindowOntop')) {
if (db.get('settings.miniWindowOnTop')) {
obj.alwaysOnTop = true
}
return obj
@@ -148,19 +162,14 @@ windowList.set(IWindowList.RENAME_WINDOW, {
show: true,
fullscreenable: false,
resizable: false,
vibrancy: 'ultra-dark',
backgroundColor: 'rgba(26,40,42,0.9)',
webPreferences: {
nodeIntegration: !!process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
nodeIntegrationInWorker: true,
backgroundThrottling: false
...defaultWebPreferences
}
}
if (process.platform !== 'darwin') {
options.show = true
options.backgroundColor = '#3f3c37'
options.autoHideMenuBar = true
options.transparent = false
}
return options
},
@@ -183,4 +192,48 @@ windowList.set(IWindowList.RENAME_WINDOW, {
}
})
windowList.set(IWindowList.TOOLBOX_WINDOW, {
isValid: true,
multiple: false,
options () {
const options: IBrowserWindowOptions = {
height: 450,
width: 800,
show: false,
frame: true,
center: true,
fullscreenable: false,
resizable: false,
backgroundColor: 'rgba(26,40,42,0.9)',
title: `PicGo-${T('TOOLBOX')}`,
icon: getStaticPath('logo.png'),
webPreferences: {
...defaultWebPreferences,
webSecurity: false
}
}
if (process.platform !== 'darwin') {
options.autoHideMenuBar = true
}
return options
},
async callback (window, windowManager) {
window.loadURL(TOOLBOX_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 - 400
let positionY
// if is the settingWindow
if (bounds.height > 400) {
positionY = bounds.y + bounds.height / 2 - 225
} else { // if is the miniWindow
positionY = bounds.y + bounds.height / 2
}
window.setPosition(positionX, positionY, false)
}
}
})
export default windowList
@@ -23,6 +23,10 @@ class WindowManager implements IWindowManager {
this.windowIdMap.set(window.id, name)
}
windowConfig.callback(window, this)
// https://github.com/electron/electron/issues/1594
window.on('page-title-updated', (evt) => {
evt.preventDefault()
})
window.on('close', () => {
this.deleteById(id)
})
+2 -2
View File
@@ -5,7 +5,7 @@ import {
UPLOAD_WITH_CLIPBOARD_FILES,
UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE,
GET_WINDOW_ID,
GET_WINDOW_ID_REPONSE,
GET_WINDOW_ID_RESPONSE,
GET_SETTING_WINDOW_ID,
GET_SETTING_WINDOW_ID_RESPONSE
} from './constants'
@@ -56,7 +56,7 @@ export const uploadWithFiles = (pathList: IFileWithPath[]): Promise<{
// miniWindow or settingWindow or trayWindow
export const getWindowId = (): Promise<number> => {
return new Promise((resolve) => {
bus.once(GET_WINDOW_ID_REPONSE, (id: number) => {
bus.once(GET_WINDOW_ID_RESPONSE, (id: number) => {
resolve(id)
})
bus.emit(GET_WINDOW_ID)
+5 -1
View File
@@ -1,5 +1,5 @@
export const GET_WINDOW_ID = 'GET_WINDOW_ID' // get a current window
export const GET_WINDOW_ID_REPONSE = 'GET_WINDOW_ID_REPONSE'
export const GET_WINDOW_ID_RESPONSE = 'GET_WINDOW_ID_RESPONSE'
export const GET_SETTING_WINDOW_ID = 'GET_SETTING_WINDOW_ID' // get setting window
export const GET_SETTING_WINDOW_ID_RESPONSE = 'GET_SETTING_WINDOW_ID_RESPONSE'
export const UPLOAD_WITH_FILES = 'UPLOAD_WITH_FILES'
@@ -7,3 +7,7 @@ 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'
// shortkey command
export const SHORTKEY_BUILTIN_PREFIX = 'picgo:'
export const SHORTKEY_COMMAND_UPLOAD = `${SHORTKEY_BUILTIN_PREFIX}upload`
+13 -3
View File
@@ -1,11 +1,11 @@
import fs from 'fs-extra'
import writeFile from 'write-file-atomic'
import path from 'path'
import { app as APP } from 'electron'
import { getLogger } from '@core/utils/localLogger'
import dayjs from 'dayjs'
import { T } from '~/main/i18n'
const STORE_PATH = APP.getPath('userData')
import { FORM_IMAGE_FOLDER } from '~/universal/utils/static'
import { STORE_PATH } from '~/main/utils/env'
const configFilePath = path.join(STORE_PATH, 'data.json')
const configFileBackupPath = path.join(STORE_PATH, 'data.bak.json')
export const defaultConfigPath = configFilePath
@@ -131,9 +131,19 @@ function getGalleryDBPath (): {
}
}
function getFormImageFolderPath (): string {
const STORE_PATH = dbPathDir()
const formImagesPath = path.join(STORE_PATH, FORM_IMAGE_FOLDER)
if (!fs.existsSync(formImagesPath)) {
fs.mkdirSync(formImagesPath, { recursive: true })
}
return formImagesPath
}
export {
dbChecker,
dbPathChecker,
dbPathDir,
getGalleryDBPath
getGalleryDBPath,
getFormImageFolderPath
}
+1 -1
View File
@@ -9,7 +9,7 @@ if (!fs.pathExistsSync(STORE_PATH)) {
fs.mkdirpSync(STORE_PATH)
}
const CONFIG_PATH: string = dbPathChecker()
const DB_PATH: string = getGalleryDBPath().dbPath
export const DB_PATH: string = getGalleryDBPath().dbPath
class ConfigStore {
private db: JSONStore
+21 -7
View File
@@ -18,14 +18,14 @@ import {
} from '~/universal/events/constants'
import { DBStore } from '@picgo/store'
import { T } from '~/main/i18n'
import { IRPCActionType } from '~/universal/types/enum'
// 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')
console.log('init gui api')
}
public static getInstance (): GuiApi {
@@ -49,7 +49,7 @@ class GuiApi implements IGuiApi {
})
}
private getWebcontentsByWindowId (id: number) {
private getWebContentsByWindowId (id: number) {
return BrowserWindow.fromId(id)?.webContents
}
@@ -58,9 +58,9 @@ class GuiApi implements IGuiApi {
placeholder: ''
}) {
await this.showSettingWindow()
this.getWebcontentsByWindowId(this.settingWindowId)?.send(SHOW_INPUT_BOX, options)
this.getWebContentsByWindowId(this.settingWindowId)?.send(SHOW_INPUT_BOX, options)
return new Promise<string>((resolve) => {
ipcMain.once(SHOW_INPUT_BOX, (event: Event, value: string) => {
ipcMain.once(SHOW_INPUT_BOX, (event, value: string) => {
resolve(value)
})
})
@@ -74,7 +74,7 @@ class GuiApi implements IGuiApi {
async upload (input: IUploadOption) {
this.windowId = await getWindowId()
const webContents = this.getWebcontentsByWindowId(this.windowId)
const webContents = this.getWebContentsByWindowId(this.windowId)
const imgs = await uploader.setWebContents(webContents!).upload(input)
if (imgs !== false) {
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
@@ -93,7 +93,7 @@ class GuiApi implements IGuiApi {
}
handleCopyUrl(pasteText.join('\n'))
webContents?.send('uploadFiles', imgs)
webContents?.send('updateGallery')
webContents?.send(IRPCActionType.UPDATE_GALLERY)
return imgs
}
return []
@@ -130,6 +130,20 @@ class GuiApi implements IGuiApi {
})
}
/**
* v2.4.0+
* @param options
*/
async showConfigDialog<T extends IStringKeyMap> (options: IPicGoPluginShowConfigDialogOption) {
await this.showSettingWindow()
this.getWebContentsByWindowId(this.settingWindowId)?.send(IRPCActionType.OPEN_CONFIG_DIALOG, options)
return new Promise<T | false>((resolve) => {
ipcMain.once(IRPCActionType.OPEN_CONFIG_DIALOG, (event, value: T | false) => {
resolve(value)
})
})
}
/**
* get picgo config/data path
*/
+4 -5
View File
@@ -1,7 +1,7 @@
import bus from '@core/bus'
import {
uploadClipboardFiles,
uploadChoosedFiles
uploadSelectedFiles
} from 'apis/app/uploader/apis'
import {
createMenu
@@ -14,14 +14,13 @@ import {
UPLOAD_WITH_CLIPBOARD_FILES,
UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE,
GET_WINDOW_ID,
GET_WINDOW_ID_REPONSE,
GET_WINDOW_ID_RESPONSE,
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,
@@ -40,13 +39,13 @@ async function busCallUploadClipboardFiles () {
async function busCallUploadFiles (pathList: IFileWithPath[]) {
const win = windowManager.getAvailableWindow()
const urls = await uploadChoosedFiles(win.webContents, pathList)
const urls = await uploadSelectedFiles(win.webContents, pathList)
bus.emit(UPLOAD_WITH_FILES_RESPONSE, urls)
}
function busCallGetWindowId () {
const win = windowManager.getAvailableWindow()
bus.emit(GET_WINDOW_ID_REPONSE, win.id)
bus.emit(GET_WINDOW_ID_RESPONSE, win.id)
}
function busCallGetSettingWindowId () {
+10 -13
View File
@@ -7,7 +7,7 @@ import {
BrowserWindow
} from 'electron'
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
import { IRPCActionType, IWindowList } from '#/types/enum'
import uploader from 'apis/app/uploader'
import pasteTemplate from '~/main/utils/pasteTemplate'
import db, { GalleryDB } from '~/main/apis/core/datastore'
@@ -25,22 +25,20 @@ import {
SHOW_UPLOAD_PAGE_MENU,
OPEN_USER_STORE_FILE,
OPEN_URL,
RELOAD_APP,
SHOW_PLUGIN_PAGE_MENU,
SET_MINI_WINDOW_POS,
GET_PICBEDS
} from '#/events/constants'
import {
uploadClipboardFiles,
uploadChoosedFiles
uploadSelectedFiles
} from '~/main/apis/app/uploader/apis'
import picgoCoreIPC from './picgoCoreIPC'
import { handleCopyUrl } from '~/main/utils/common'
import { buildMainPageMenu, buildMiniPageMenu, buildPluginPageMenu, buildPicBedListMenu } from './remotes/menu'
import path from 'path'
import { T } from '~/main/i18n'
const STORE_PATH = app.getPath('userData')
import { STORE_PATH } from '~/main/utils/env'
export default {
listen () {
@@ -63,18 +61,19 @@ export default {
await GalleryDB.getInstance().insert(img[0])
trayWindow.webContents.send('clipboardFiles', [])
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('updateGallery')
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send(IRPCActionType.UPDATE_GALLERY)
}
}
trayWindow.webContents.send('uploadFiles')
})
ipcMain.on('uploadClipboardFilesFromUploadPage', () => {
console.log('handle')
uploadClipboardFiles()
})
ipcMain.on('uploadChoosedFiles', async (evt: IpcMainEvent, files: IFileWithPath[]) => {
return uploadChoosedFiles(evt.sender, files)
return uploadSelectedFiles(evt.sender, files)
})
ipcMain.on('updateShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, oldKey: string, from: string) => {
@@ -137,7 +136,7 @@ export default {
const miniWindow = windowManager.get(IWindowList.MINI_WINDOW)!
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
if (db.get('settings.miniWindowOntop')) {
if (db.get('settings.miniWindowOnTop')) {
miniWindow.setAlwaysOnTop(true)
}
@@ -167,7 +166,9 @@ export default {
server.restart()
})
ipcMain.on(OPEN_DEVTOOLS, (event: IpcMainEvent) => {
event.sender.openDevTools()
event.sender.openDevTools({
mode: 'detach'
})
})
// menu & window methods
ipcMain.on(SHOW_MINI_PAGE_MENU, () => {
@@ -217,10 +218,6 @@ export default {
ipcMain.on(OPEN_URL, (evt: IpcMainEvent, url: string) => {
shell.openExternal(url)
})
ipcMain.on(RELOAD_APP, () => {
app.relaunch()
app.exit(0)
})
ipcMain.on(SET_MINI_WINDOW_POS, (evt: IpcMainEvent, pos: IMiniWindowPos) => {
const window = BrowserWindow.getFocusedWindow()
window?.setBounds(pos)
+7 -9
View File
@@ -7,6 +7,7 @@ import {
ipcMain,
clipboard
} from 'electron'
import fs from 'fs-extra'
import { IPasteStyle, IPicGoHelperType, IWindowList } from '#/types/enum'
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
import picgo from '@core/picgo'
@@ -29,7 +30,8 @@ import {
OPEN_WINDOW,
GET_LANGUAGE_LIST,
SET_CURRENT_LANGUAGE,
GET_CURRENT_LANGUAGE
GET_CURRENT_LANGUAGE,
GET_PICBED_CONFIG
} from '#/events/constants'
import { GalleryDB } from 'apis/core/datastore'
@@ -38,11 +40,7 @@ import pasteTemplate from '../utils/pasteTemplate'
import { i18nManager, T } from '~/main/i18n'
import { rpcServer } from './rpc'
// eslint-disable-next-line
const requireFunc = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require
// const PluginHandler = requireFunc('picgo/lib/PluginHandler').default
const STORE_PATH = path.dirname(dbPathChecker())
// const CONFIG_PATH = path.join(STORE_PATH, '/data.json')
interface GuiMenuItem {
label: string
@@ -83,7 +81,7 @@ const getPluginList = (): IPicGoPlugin[] => {
for (const 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 pluginPKG = fs.readJSONSync(path.join(pluginPath, 'package.json'), 'utf-8')
const uploaderName = plugin.uploader || ''
const transformerName = plugin.transformer || ''
let menu: Omit<IGuiMenuItem, 'handle'>[] = []
@@ -223,14 +221,14 @@ const handleNPMError = (): IDispose => {
}
const handleGetPicBedConfig = () => {
ipcMain.on('getPicBedConfig', (event: IpcMainEvent, type: string) => {
ipcMain.on(GET_PICBED_CONFIG, (event: IpcMainEvent, type: string) => {
const name = picgo.helper.uploader.get(type)?.name || type
if (picgo.helper.uploader.get(type)?.config) {
const _config = picgo.helper.uploader.get(type)!.config!(picgo)
const config = handleConfigWithFunction(_config)
event.sender.send('getPicBedConfig', config, name)
event.sender.send(GET_PICBED_CONFIG, config, name)
} else {
event.sender.send('getPicBedConfig', [], name)
event.sender.send(GET_PICBED_CONFIG, [], name)
}
})
}
+13 -59
View File
@@ -1,7 +1,6 @@
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
import { Menu, BrowserWindow, app, dialog } from 'electron'
import getPicBeds from '~/main/utils/getPicBeds'
import picgo from '@core/picgo'
import {
uploadClipboardFiles
@@ -9,11 +8,11 @@ import {
import { privacyManager } from '~/main/utils/privacyManager'
import pkg from 'root/package.json'
import GuiApi from 'apis/gui'
import { PICGO_CONFIG_PLUGIN, PICGO_HANDLE_PLUGIN_ING, PICGO_TOGGLE_PLUGIN, SHOW_MAIN_PAGE_DONATION, SHOW_MAIN_PAGE_QRCODE } from '~/universal/events/constants'
import { PICGO_CONFIG_PLUGIN, PICGO_HANDLE_PLUGIN_DONE, PICGO_HANDLE_PLUGIN_ING, PICGO_TOGGLE_PLUGIN, SHOW_MAIN_PAGE_DONATION, SHOW_MAIN_PAGE_QRCODE } from '~/universal/events/constants'
import picgoCoreIPC from '~/main/events/picgoCoreIPC'
import { PicGo as PicGoCore } from 'picgo'
import { T } from '~/main/i18n'
import { changeCurrentUploader } from '~/main/utils/handleUploaderConfig'
import { buildPicBedListMenu } from './picBedListMenu'
interface GuiMenuItem {
label: string
@@ -95,10 +94,19 @@ const buildMainPageMenu = (win: BrowserWindow) => {
win?.webContents?.send(SHOW_MAIN_PAGE_QRCODE)
}
},
{
label: T('OPEN_TOOLBOX'),
click () {
const window = windowManager.create(IWindowList.TOOLBOX_WINDOW)
window?.show()
}
},
{
label: T('SHOW_DEVTOOLS'),
click () {
win?.webContents?.openDevTools()
win?.webContents?.openDevTools({
mode: 'detach'
})
}
},
{
@@ -112,61 +120,6 @@ const buildMainPageMenu = (win: BrowserWindow) => {
return Menu.buildFromTemplate(template)
}
const buildPicBedListMenu = () => {
const picBeds = getPicBeds()
const currentPicBed = picgo.getConfig('picBed.uploader')
const currentPicBedName = picBeds.find(item => item.type === currentPicBed)?.name
const picBedConfigList = picgo.getConfig<IUploaderConfig>('uploader')
const currentPicBedMenuItem = [{
label: `${T('CURRENT_PICBED')} - ${currentPicBedName}`,
enabled: false
}, {
type: 'separator'
}]
let submenu = picBeds.filter(item => item.visible).map(item => {
const configList = picBedConfigList?.[item.type]?.configList
const defaultId = picBedConfigList?.[item.type]?.defaultId
const hasSubmenu = !!configList
return {
label: item.name,
type: !hasSubmenu ? 'checkbox' : undefined,
checked: !hasSubmenu ? (currentPicBed === item.type) : undefined,
submenu: hasSubmenu
? configList.map((config) => {
return {
label: config._configName || 'Default',
// if only one config, use checkbox, or radio will checked as default
// see: https://github.com/electron/electron/issues/21292
type: 'checkbox',
checked: config._id === defaultId && (item.type === currentPicBed),
click: function () {
changeCurrentUploader(item.type, config, config._id)
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')
}
}
}
})
: undefined,
click: !hasSubmenu
? function () {
picgo.saveConfig({
'picBed.current': item.type,
'picBed.uploader': item.type
})
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')
}
}
: undefined
}
})
// @ts-ignore
submenu = currentPicBedMenuItem.concat(submenu)
// @ts-ignore
return Menu.buildFromTemplate(submenu)
}
// TODO: separate to single file
const handleRestoreState = (item: string, name: string): void => {
@@ -210,6 +163,7 @@ const buildPluginPageMenu = (plugin: IPicGoPlugin) => {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
window.webContents.send(PICGO_HANDLE_PLUGIN_ING, plugin.fullName)
window.webContents.send(PICGO_TOGGLE_PLUGIN, plugin.fullName, false)
window.webContents.send(PICGO_HANDLE_PLUGIN_DONE, plugin.fullName)
if (plugin.config.transformer.name) {
handleRestoreState('transformer', plugin.config.transformer.name)
}
+62
View File
@@ -0,0 +1,62 @@
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
import { Menu } from 'electron'
import getPicBeds from '~/main/utils/getPicBeds'
import picgo from '@core/picgo'
import { T } from '~/main/i18n'
import { changeCurrentUploader } from '~/main/utils/handleUploaderConfig'
export const buildPicBedListMenu = () => {
const picBeds = getPicBeds()
const currentPicBed = picgo.getConfig('picBed.uploader')
const currentPicBedName = picBeds.find(item => item.type === currentPicBed)?.name
const picBedConfigList = picgo.getConfig<IUploaderConfig>('uploader')
const currentPicBedMenuItem = [{
label: `${T('CURRENT_PICBED')} - ${currentPicBedName}`,
enabled: false
}, {
type: 'separator'
}]
let submenu = picBeds.filter(item => item.visible).map(item => {
const configList = picBedConfigList?.[item.type]?.configList
const defaultId = picBedConfigList?.[item.type]?.defaultId
const hasSubmenu = !!configList
return {
label: item.name,
type: !hasSubmenu ? 'checkbox' : undefined,
checked: !hasSubmenu ? (currentPicBed === item.type) : undefined,
submenu: hasSubmenu
? configList.map((config) => {
return {
label: config._configName || 'Default',
// if only one config, use checkbox, or radio will checked as default
// see: https://github.com/electron/electron/issues/21292
type: 'checkbox',
checked: config._id === defaultId && (item.type === currentPicBed),
click: function () {
changeCurrentUploader(item.type, config, config._id)
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')
}
}
}
})
: undefined,
click: !hasSubmenu
? function () {
picgo.saveConfig({
'picBed.current': item.type,
'picBed.uploader': item.type
})
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')
}
}
: undefined
}
})
// @ts-ignore
submenu = currentPicBedMenuItem.concat(submenu)
// @ts-ignore
return Menu.buildFromTemplate(submenu)
}
+36 -58
View File
@@ -1,83 +1,61 @@
import { ipcMain, IpcMainEvent } from 'electron'
import { IRPCActionType } from '~/universal/types/enum'
import { RPC_ACTIONS } from '#/events/constants'
import {
deleteUploaderConfig,
getUploaderConfigList,
selectUploaderConfig,
updateUploaderConfig
} from '~/main/utils/handleUploaderConfig'
import { configRouter } from './routes/config'
import { versionRouter } from './routes/version'
import { toolboxRouter } from './routes/toolbox'
import { systemRouter } from './routes/system'
import { galleryToolboxRouter } from './routes/galleryToolbox'
class RPCServer {
start () {
ipcMain.on(RPC_ACTIONS, (event: IpcMainEvent, action: IRPCActionType, args: any[], callbackId: string) => {
try {
switch (action) {
case IRPCActionType.GET_PICBED_CONFIG_LIST: {
const configList = this.getPicBedConfigList(args as IGetUploaderConfigListArgs)
this.sendBack(event, action, configList, callbackId)
break
}
case IRPCActionType.DELETE_PICBED_CONFIG: {
const res = this.deleteUploaderConfig(args as IDeleteUploaderConfigArgs)
this.sendBack(event, action, res, callbackId)
break
}
case IRPCActionType.SELECT_UPLOADER: {
this.selectUploaderConfig(args as ISelectUploaderConfigArgs)
this.sendBack(event, action, true, callbackId)
break
}
case IRPCActionType.UPDATE_UPLOADER_CONFIG: {
this.updateUploaderConfig(args as IUpdateUploaderConfigArgs)
this.sendBack(event, action, true, callbackId)
break
}
default: {
this.sendBack(event, action, null, callbackId)
break
}
}
} catch (e) {
this.sendBack(event, action, null, callbackId)
class RPCServer implements IRPCServer {
private routes: IRPCRoutes = new Map()
private rpcEventHandler = async (event: IpcMainEvent, action: IRPCActionType, args: any[], callbackId: string) => {
try {
const handler = this.routes.get(action)
if (!handler) {
return this.sendBack(event, action, null, callbackId)
}
})
const res = await handler?.(args, event)
this.sendBack(event, action, res, callbackId)
} catch (e) {
this.sendBack(event, action, null, callbackId)
}
}
/**
* if sendback data is null, then it means that the action is not supported or error occurs
* if there is no callbackId, then do not send back
*/
private sendBack (event: IpcMainEvent, action: IRPCActionType, data: any, callbackId: string) {
event.sender.send(RPC_ACTIONS, data, action, callbackId)
if (callbackId) {
event.sender.send(RPC_ACTIONS, data, action, callbackId)
}
}
private getPicBedConfigList (args: IGetUploaderConfigListArgs) {
const [type] = args
const config = getUploaderConfigList(type)
return config
start () {
ipcMain.on(RPC_ACTIONS, this.rpcEventHandler)
}
private deleteUploaderConfig (args: IDeleteUploaderConfigArgs) {
const [type, id] = args
const config = deleteUploaderConfig(type, id)
return config
use (routes: IRPCRoutes) {
for (const [action, handler] of routes) {
this.routes.set(action, handler)
}
}
private selectUploaderConfig (args: ISelectUploaderConfigArgs) {
const [type, id] = args
const config = selectUploaderConfig(type, id)
return config
}
private updateUploaderConfig (args: IUpdateUploaderConfigArgs) {
const [type, id, config] = args
const res = updateUploaderConfig(type, id, config)
return res
stop () {
ipcMain.off(RPC_ACTIONS, this.rpcEventHandler)
}
}
const rpcServer = new RPCServer()
rpcServer.use(configRouter.routes())
rpcServer.use(versionRouter.routes())
rpcServer.use(toolboxRouter.routes())
rpcServer.use(systemRouter.routes())
rpcServer.use(galleryToolboxRouter.routes())
export {
rpcServer
}
+13
View File
@@ -0,0 +1,13 @@
import { IRPCActionType } from '~/universal/types/enum'
export class RPCRouter implements IRPCRouter {
private routeMap: IRPCRoutes = new Map()
add = <T>(action: IRPCActionType, handler: IRPCHandler<T>) => {
this.routeMap.set(action, handler)
return this
}
routes () {
return this.routeMap
}
}
+31
View File
@@ -0,0 +1,31 @@
import { IRPCActionType } from '~/universal/types/enum'
import { RPCRouter } from '../router'
import { deleteUploaderConfig, getUploaderConfigList, selectUploaderConfig, updateUploaderConfig } from '~/main/utils/handleUploaderConfig'
const configRouter = new RPCRouter()
configRouter
.add(IRPCActionType.GET_PICBED_CONFIG_LIST, async (args) => {
const [type] = args as IGetUploaderConfigListArgs
const config = getUploaderConfigList(type)
return config
})
.add(IRPCActionType.DELETE_PICBED_CONFIG, async (args) => {
const [type, id] = args as IDeleteUploaderConfigArgs
const config = deleteUploaderConfig(type, id)
return config
})
.add(IRPCActionType.SELECT_UPLOADER, async (args) => {
const [type, id] = args as ISelectUploaderConfigArgs
selectUploaderConfig(type, id)
return true
})
.add(IRPCActionType.UPDATE_UPLOADER_CONFIG, async (args) => {
const [type, id, config] = args as IUpdateUploaderConfigArgs
updateUploaderConfig(type, id, config)
return true
})
export {
configRouter
}
@@ -0,0 +1,77 @@
import { logger } from '@picgo/i18n'
import { IPicGo } from 'picgo'
import { T } from '~/main/i18n'
import { getHost, removeProtocolAndSuffix, replaceHost } from '~/main/utils/common'
export const galleryMenu = () => {
return [{
label: T('CHANGE_IMAGE_URL_HOST'),
async handle (ctx: IPicGo, guiApi: IGuiApi, selectedList: ImgInfo[] = []) {
const hostList = [...new Set(selectedList.map((item) => {
return getHost(item.imgUrl)
}).filter(Boolean) as string[])]
if (hostList.length === 0) {
guiApi.showNotification({
title: T('CHANGE_IMAGE_URL_HOST'),
body: T('CHANGE_IMAGE_URL_HOST_WARN')
})
logger.warn(T('CHANGE_IMAGE_URL_HOST_WARN'))
return
}
const config: IPicGoPluginConfig[] = [
{
alias: T('SELECTED_IMAGE_URL_HOST'),
name: 'selectedHost',
type: 'checkbox',
choices: hostList.map(item => ({
name: item,
value: item,
checked: true
})),
required: true
},
{
alias: T('NEW_IMAGE_URL_HOST'),
name: 'newHost',
type: 'input',
message: 'www.example.com',
default: '',
required: true
}
]
const options: IPicGoPluginShowConfigDialogOption = {
title: T('CHANGE_IMAGE_URL_HOST'),
config
}
const res = await guiApi.showConfigDialog<{
selectedHost: string[]
newHost: string
}>(options)
if (res) {
const selectedHost = res.selectedHost
const newHost = removeProtocolAndSuffix(res.newHost)
const changedList = selectedList.map((item) => {
try {
const url = new URL(item.imgUrl || '')
const host = url.host
if (selectedHost.includes(host)) {
item.imgUrl = replaceHost(item.imgUrl!, host, newHost)
return item
} else {
return false
}
} catch (e: any) {
ctx.log.error(e)
return false
}
}).filter(Boolean) as ImgInfo[]
const updateRes = await guiApi.galleryDB.updateMany(changedList)
guiApi.showNotification({
title: T('CHANGE_IMAGE_URL_HOST_RESULT'),
body: `${T('SUCCESS')}: ${updateRes.success} ${T('FAILED')}: ${updateRes.total - updateRes.success}}`
})
}
}
}]
}
@@ -0,0 +1,6 @@
import { galleryMenu as changeHostGalleryMenu } from './changeHost'
export const builtInGalleryToolboxMenu = () => {
const menuList = [...changeHostGalleryMenu()]
return menuList
}
@@ -0,0 +1,20 @@
import { IRPCActionType, IWindowList } from '~/universal/types/enum'
import { RPCRouter } from '../../router'
import windowManager from '~/main/apis/app/window/windowManager'
import { galleryMenuListManager } from './menuListManager'
const galleryToolboxRouter = new RPCRouter()
galleryToolboxRouter.add(IRPCActionType.GET_GALLERY_MENU_LIST, async (args) => {
const [selectedList] = args as IGetGalleryMenuListArgs
const win = windowManager.get(IWindowList.SETTING_WINDOW)!
const menu = galleryMenuListManager.getMenu(selectedList)
menu.popup({
window: win
})
})
export {
galleryToolboxRouter
}
@@ -0,0 +1,48 @@
import { Menu, MenuItemConstructorOptions } from 'electron'
import { builtInGalleryToolboxMenu } from './builtIn'
import picgo from '@core/picgo'
import GuiApi from 'apis/gui'
import windowManager from '~/main/apis/app/window/windowManager'
import { IRPCActionType, IWindowList } from '~/universal/types/enum'
import logger from '~/main/apis/core/picgo/logger'
class GalleryMenuListManager {
private menuList: MenuItemConstructorOptions[] = []
private menu: Menu | null = null
private getBuiltInMenuList (selectedList: IGalleryItem[]): MenuItemConstructorOptions[] {
const builtInMenu = builtInGalleryToolboxMenu().map(item => {
return {
label: item.label,
async click () {
try {
await item.handle(picgo, GuiApi.getInstance(), selectedList)
} catch (e: any) {
logger.error(e)
} finally {
windowManager.get(IWindowList.SETTING_WINDOW)?.webContents.send(IRPCActionType.UPDATE_GALLERY)
}
}
}
}) as MenuItemConstructorOptions[]
this.menuList = [...builtInMenu]
return this.menuList
}
private getMenuItemList (selectedList: IGalleryItem[]) {
// current only support built-in menu
return this.getBuiltInMenuList(selectedList)
}
public getMenu (selectedList: IGalleryItem[]): Menu {
this.menu = Menu.buildFromTemplate(this.getMenuItemList(selectedList))
return this.menu
}
}
const galleryMenuListManager = new GalleryMenuListManager()
export {
galleryMenuListManager
}
+41
View File
@@ -0,0 +1,41 @@
import { IRPCActionType, IWindowList } from '~/universal/types/enum'
import { RPCRouter } from '../router'
import { app, clipboard, shell } from 'electron'
import windowManager from '~/main/apis/app/window/windowManager'
import { handleMenubarIcon } from '~/main/apis/app/system'
const systemRouter = new RPCRouter()
systemRouter
.add(IRPCActionType.RELOAD_APP, async () => {
app.relaunch()
app.exit(0)
})
.add(IRPCActionType.OPEN_FILE, async (args) => {
const [filePath] = args as IOpenFileArgs
shell.openPath(filePath)
})
.add(IRPCActionType.COPY_TEXT, async (args) => {
const [text] = args as ICopyTextArgs
return clipboard.writeText(text)
})
.add(IRPCActionType.SHOW_DOCK_ICON, async (args) => {
const [visible] = args as IShowDockIconArgs
app.dock?.[visible ? 'show' : 'hide']()
const win = windowManager.get(IWindowList.SETTING_WINDOW)
if (!visible) {
win?.show()
win?.focus()
win?.setSkipTaskbar(true)
} else {
win?.setSkipTaskbar(false)
}
})
.add(IRPCActionType.SHOW_MENUBAR_ICON, async (args) => {
const [visible] = args as IShowMenubarIconArgs
handleMenubarIcon(visible)
})
export {
systemRouter
}
@@ -0,0 +1,77 @@
import fs from 'fs-extra'
import path from 'path'
import { dbPathChecker, defaultConfigPath } from '~/main/apis/core/datastore/dbChecker'
import { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'
import { CLIPBOARD_IMAGE_FOLDER } from '~/universal/utils/static'
import { sendToolboxResWithType } from './utils'
import { T } from '~/main/i18n'
const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD)
const defaultClipboardImagePath = path.join(defaultConfigPath, CLIPBOARD_IMAGE_FOLDER)
export const checkClipboardUploadMap: IToolboxCheckerMap<
IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD
> = {
[IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: async (event) => {
sendToolboxRes(event, {
status: IToolboxItemCheckStatus.LOADING
})
const configFilePath = dbPathChecker()
if (fs.existsSync(configFilePath)) {
const dirPath = path.dirname(configFilePath)
const clipboardImagePath = path.join(dirPath, CLIPBOARD_IMAGE_FOLDER)
if (fs.existsSync(clipboardImagePath)) {
sendToolboxRes(event, {
status: IToolboxItemCheckStatus.SUCCESS,
msg: T('TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_TIPS', {
path: clipboardImagePath
}),
value: clipboardImagePath
})
} else {
sendToolboxRes(event, {
status: IToolboxItemCheckStatus.ERROR,
msg: T('TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS', {
path: clipboardImagePath
}),
value: path.dirname(clipboardImagePath)
})
}
} else {
sendToolboxRes(event, {
status: IToolboxItemCheckStatus.ERROR,
msg: T('TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS', {
path: defaultClipboardImagePath
}),
value: path.dirname(defaultClipboardImagePath)
})
}
}
}
export const fixClipboardUploadMap: IToolboxFixMap<
IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD
> = {
[IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: async () => {
const configFilePath = dbPathChecker()
const dirPath = path.dirname(configFilePath)
const clipboardImagePath = path.join(dirPath, CLIPBOARD_IMAGE_FOLDER)
try {
fs.mkdirsSync(clipboardImagePath)
return {
type: IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD,
status: IToolboxItemCheckStatus.SUCCESS
}
} catch (e) {
return {
type: IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD,
status: IToolboxItemCheckStatus.ERROR,
msg: T('TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS', {
path: clipboardImagePath
}),
value: path.dirname(clipboardImagePath)
}
}
}
}
@@ -0,0 +1,87 @@
import fs from 'fs-extra'
import { IpcMainEvent } from 'electron'
import { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'
import { sendToolboxResWithType } from './utils'
import { dbPathChecker } from '~/main/apis/core/datastore/dbChecker'
import { GalleryDB, DB_PATH } from '~/main/apis/core/datastore'
import path from 'path'
import { T } from '~/main/i18n'
export const checkFileMap: IToolboxCheckerMap<
IToolboxItemType.IS_CONFIG_FILE_BROKEN | IToolboxItemType.IS_GALLERY_FILE_BROKEN
> = {
[IToolboxItemType.IS_CONFIG_FILE_BROKEN]: async (event: IpcMainEvent) => {
const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.IS_CONFIG_FILE_BROKEN)
sendToolboxRes(event, {
status: IToolboxItemCheckStatus.LOADING
})
const configFilePath = dbPathChecker()
try {
if (fs.existsSync(configFilePath)) {
await fs.readJSON(configFilePath)
sendToolboxRes(event, {
status: IToolboxItemCheckStatus.SUCCESS,
msg: T('TOOLBOX_CHECK_CONFIG_FILE_PATH_TIPS', {
path: configFilePath
}),
value: configFilePath
})
}
} catch (e) {
sendToolboxRes(event, {
status: IToolboxItemCheckStatus.ERROR,
msg: T('TOOLBOX_CHECK_CONFIG_FILE_BROKEN_TIPS'),
value: path.dirname(configFilePath)
})
}
},
[IToolboxItemType.IS_GALLERY_FILE_BROKEN]: async (event) => {
const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.IS_GALLERY_FILE_BROKEN)
sendToolboxRes(event, {
status: IToolboxItemCheckStatus.LOADING
})
const galleryDB = GalleryDB.getInstance()
if (galleryDB.errorList.length === 0) {
sendToolboxRes(event, {
status: IToolboxItemCheckStatus.SUCCESS,
msg: T('TOOLBOX_CHECK_GALLERY_FILE_PATH_TIPS', {
path: DB_PATH
}),
value: path.dirname(DB_PATH)
})
} else {
sendToolboxRes(event, {
status: IToolboxItemCheckStatus.ERROR,
msg: T('TOOLBOX_CHECK_GALLERY_FILE_BROKEN_TIPS'),
value: path.dirname(DB_PATH)
})
}
}
}
export const fixFileMap: IToolboxFixMap<
IToolboxItemType.IS_CONFIG_FILE_BROKEN | IToolboxItemType.IS_GALLERY_FILE_BROKEN
> = {
[IToolboxItemType.IS_CONFIG_FILE_BROKEN]: async () => {
try {
fs.unlinkSync(dbPathChecker())
} catch (e) {
// do nothing
}
return {
type: IToolboxItemType.IS_CONFIG_FILE_BROKEN,
status: IToolboxItemCheckStatus.SUCCESS
}
},
[IToolboxItemType.IS_GALLERY_FILE_BROKEN]: async () => {
try {
fs.unlinkSync(DB_PATH)
} catch (e) {
// do nothing
}
return {
type: IToolboxItemType.IS_GALLERY_FILE_BROKEN,
status: IToolboxItemCheckStatus.SUCCESS
}
}
}
@@ -0,0 +1,92 @@
import fs from 'fs-extra'
import { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'
import { sendToolboxResWithType } from './utils'
import tunnel from 'tunnel'
import { dbPathChecker } from '~/main/apis/core/datastore/dbChecker'
import { IConfig } from 'picgo'
import axios, { AxiosRequestConfig } from 'axios'
import { T } from '~/main/i18n'
const getProxy = (proxyStr: string): AxiosRequestConfig['proxy'] | false => {
if (proxyStr) {
try {
const proxyOptions = new URL(proxyStr)
return {
host: proxyOptions.hostname,
port: parseInt(proxyOptions.port || '0', 10),
protocol: proxyOptions.protocol
}
} catch (e) {
}
}
return false
}
const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.HAS_PROBLEM_WITH_PROXY)
export const checkProxyMap: IToolboxCheckerMap<
IToolboxItemType.HAS_PROBLEM_WITH_PROXY
> = {
[IToolboxItemType.HAS_PROBLEM_WITH_PROXY]: async (event) => {
sendToolboxRes(event, {
status: IToolboxItemCheckStatus.LOADING
})
const configFilePath = dbPathChecker()
if (fs.existsSync(configFilePath)) {
let config: IConfig | undefined
try {
config = await fs.readJSON(configFilePath) as IConfig
} catch (e) {
}
if (!config) {
return sendToolboxRes(event, {
status: IToolboxItemCheckStatus.SUCCESS,
msg: T('TOOLBOX_CHECK_PROXY_NO_PROXY_TIPS')
})
}
const proxy = config.picBed?.proxy
if (!proxy) {
return sendToolboxRes(event, {
status: IToolboxItemCheckStatus.SUCCESS,
msg: T('TOOLBOX_CHECK_PROXY_NO_PROXY_TIPS')
})
} else {
const proxyOptions = getProxy(proxy)
if (!proxyOptions) {
return sendToolboxRes(event, {
status: IToolboxItemCheckStatus.ERROR,
msg: T('TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_CORRECT')
})
} else {
const httpsAgent = tunnel.httpsOverHttp({
proxy: {
host: proxyOptions.host,
port: proxyOptions.port
}
})
try {
await axios.get('https://www.google.com', {
httpsAgent
})
return sendToolboxRes(event, {
status: IToolboxItemCheckStatus.SUCCESS,
msg: T('TOOLBOX_CHECK_PROXY_SUCCESS_TIPS')
})
} catch (e) {
console.log(e)
return sendToolboxRes(event, {
status: IToolboxItemCheckStatus.ERROR,
msg: T('TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_WORKING')
})
}
}
}
}
sendToolboxRes(event, {
status: IToolboxItemCheckStatus.SUCCESS,
msg: T('TOOLBOX_CHECK_PROXY_NO_PROXY_TIPS')
})
}
}
@@ -0,0 +1,48 @@
import { IRPCActionType, IToolboxItemType } from '~/universal/types/enum'
import { RPCRouter } from '../../router'
import { checkFileMap, fixFileMap } from './checkFile'
import { checkClipboardUploadMap, fixClipboardUploadMap } from './checkClipboardUpload'
import { checkProxyMap } from './checkProxy'
const toolboxRouter = new RPCRouter()
const toolboxCheckMap: Partial<IToolboxCheckerMap<IToolboxItemType>> = {
...checkFileMap,
...checkClipboardUploadMap,
...checkProxyMap
}
const toolboxFixMap: Partial<IToolboxFixMap<IToolboxItemType>> = {
...fixFileMap,
...fixClipboardUploadMap
}
toolboxRouter
.add(IRPCActionType.TOOLBOX_CHECK, async (args, event) => {
const [type] = args as IToolboxCheckArgs
if (type) {
const handler = toolboxCheckMap[type]
if (handler) {
handler(event)
}
} else {
// do check all
for (const key in toolboxCheckMap) {
const handler = toolboxCheckMap[key as IToolboxItemType]
if (handler) {
handler(event)
}
}
}
})
.add(IRPCActionType.TOOLBOX_CHECK_FIX, async (args, event) => {
const [type] = args as IToolboxCheckArgs
const handler = toolboxFixMap[type]
if (handler) {
return await handler(event)
}
})
export {
toolboxRouter
}
@@ -0,0 +1,9 @@
import { IpcMainEvent } from 'electron'
import { IRPCActionType, IToolboxItemType } from '~/universal/types/enum'
export const sendToolboxResWithType = (type: IToolboxItemType) => (event: IpcMainEvent, res?: Omit<IToolboxCheckRes, 'type'>) => {
return event.sender.send(IRPCActionType.TOOLBOX_CHECK_RES, {
...res,
type
})
}
+16
View File
@@ -0,0 +1,16 @@
import { IRPCActionType } from '~/universal/types/enum'
import { RPCRouter } from '../router'
import { getLatestVersion } from '~/main/utils/getLatestVersion'
const versionRouter = new RPCRouter()
versionRouter
.add(IRPCActionType.GET_LATEST_VERSION, async (args) => {
const [type] = args as IGetLatestVersionArgs
const config = await getLatestVersion(type)
return config
})
export {
versionRouter
}
+6 -5
View File
@@ -2,19 +2,20 @@ import yaml from 'js-yaml'
import { ObjectAdapter, I18n } from '@picgo/i18n'
import path from 'path'
import fs from 'fs-extra'
import { getStaticPath } from '#/utils/staticPath'
import { builtinI18nList } from '#/i18n'
class I18nManager {
private i18n: I18n | null = null
private builtinI18nFolder = path.join(__static, 'i18n')
private outterI18nFolder = ''
private builtinI18nFolder = getStaticPath('i18n')
private outerI18nFolder = ''
private localesMap: Map<string, ILocales> = new Map()
private currentLanguage: string = 'zh-CN'
readonly defaultLanguage: string = 'zh-CN'
private i18nFileList: II18nItem[] = builtinI18nList
setOutterI18nFolder (folder: string) {
this.outterI18nFolder = folder
setOuterI18nFolder (folder: string) {
this.outerI18nFolder = folder
}
addI18nFile (file: string, label: string) {
@@ -30,7 +31,7 @@ class I18nManager {
}
let localesPath = path.join(this.builtinI18nFolder, `${lang}.yml`)
if (!fs.existsSync(localesPath)) {
localesPath = path.join(this.outterI18nFolder, `${lang}.yml`)
localesPath = path.join(this.outerI18nFolder, `${lang}.yml`)
if (!fs.existsSync(localesPath)) {
localesPath = path.join(this.builtinI18nFolder, `${this.defaultLanguage}.yml`)
}
+19 -1
View File
@@ -9,7 +9,7 @@ const logger = getLogger(LOG_PATH)
// since the error may occur in picgo-core
// so we can't use the log from picgo
const handleProcessError = (error: Error) => {
const handleProcessError = (error: Error | string) => {
logger('error', error)
}
@@ -20,3 +20,21 @@ process.on('uncaughtException', error => {
process.on('unhandledRejection', (error: any) => {
handleProcessError(error)
})
// thanks to https://github.com/camunda/camunda-modeler/pull/3314
function bootstrapEPIPESuppression () {
let suppressing = false
function logEPIPEErrorOnce () {
if (suppressing) {
return
}
suppressing = true
handleProcessError('Detected EPIPE error; suppressing further EPIPE errors')
}
require('epipebomb')(process.stdout, logEPIPEErrorOnce)
require('epipebomb')(process.stderr, logEPIPEErrorOnce)
}
bootstrapEPIPESuppression()
+34 -17
View File
@@ -5,9 +5,6 @@ import {
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 ipcList from '~/main/events/ipcList'
@@ -19,11 +16,11 @@ import {
migrateGalleryFromVersion230
} from '~/main/migrate'
import {
uploadChoosedFiles,
uploadSelectedFiles,
uploadClipboardFiles
} from 'apis/app/uploader/apis'
import {
createTray
handleDockIcon, handleMenubarIcon
} from 'apis/app/system'
import server from '~/main/server/index'
import updateChecker from '~/main/utils/updateChecker'
@@ -36,8 +33,11 @@ import picgo from 'apis/core/picgo'
import fixPath from './fixPath'
import { initI18n } from '~/main/utils/handleI18n'
import { remoteNoticeHandler } from 'apis/app/remoteNotice'
import { isMacOS } from '../utils/getMacOSVersion'
import { isWindowShouldShowOnStartup } from '../apis/app/window/windowList'
import { initStaticPath, isDev } from '../utils/env'
const isDevelopment = process.env.NODE_ENV !== 'production'
const isDevelopment = isDev
const handleStartUpFiles = (argv: string[], cwd: string) => {
const files = getUploadFiles(argv, cwd, logger)
@@ -48,7 +48,7 @@ const handleStartUpFiles = (argv: string[], cwd: string) => {
} else {
logger.info('cli -> uploading files from cli', ...files.map(item => item.path))
const win = windowManager.getAvailableWindow()
uploadChoosedFiles(win.webContents, files)
uploadSelectedFiles(win.webContents, files)
}
return true
} else {
@@ -72,18 +72,32 @@ class LifeCycle {
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: any) {
console.error('Vue Devtools failed to install:', e.toString())
console.error('Vue Devtools failed to install:', e?.toString())
}
}
windowManager.create(IWindowList.TRAY_WINDOW)
windowManager.create(IWindowList.SETTING_WINDOW)
createTray()
const settingWindow = windowManager.create(IWindowList.SETTING_WINDOW)
settingWindow?.once('show', () => {
remoteNoticeHandler.triggerHook(IRemoteNoticeTriggerHook.SETTING_WINDOW_OPEN)
})
if (isWindowShouldShowOnStartup(IWindowList.SETTING_WINDOW)) {
settingWindow?.show()
settingWindow?.focus()
}
if (!isMacOS) {
if (isWindowShouldShowOnStartup(IWindowList.MINI_WINDOW)) {
const miniWindow = windowManager.create(IWindowList.MINI_WINDOW)
miniWindow?.show()
miniWindow?.focus()
}
}
handleMenubarIcon()
handleDockIcon()
db.set('needReload', false)
updateChecker()
// 不需要阻塞
@@ -105,11 +119,7 @@ class LifeCycle {
await remoteNoticeHandler.init()
remoteNoticeHandler.triggerHook(IRemoteNoticeTriggerHook.APP_START)
}
if (!app.isReady()) {
app.on('ready', readyFunction)
} else {
readyFunction()
}
app.whenReady().then(readyFunction)
}
private onRunning () {
@@ -127,13 +137,19 @@ class LifeCycle {
}
})
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)
}
// click dock to open setting window
if (isMacOS) {
handleDockIcon()
if (db.get('settings.showDockIcon') !== false) {
windowManager.get(IWindowList.SETTING_WINDOW)?.show()
}
}
})
app.setLoginItemSettings({
openAtLogin: db.get('settings.autoStart') || false
@@ -180,6 +196,7 @@ class LifeCycle {
if (!gotTheLock) {
app.quit()
} else {
initStaticPath()
await this.beforeReady()
this.onReady()
this.onRunning()
+2 -1
View File
@@ -4,6 +4,7 @@ import path from 'path'
import fse from 'fs-extra'
import { PicGo as PicGoCore } from 'picgo'
import { T } from '~/main/i18n'
import { SHORTKEY_COMMAND_UPLOAD } from 'apis/core/bus/constants'
// from v2.1.2
const updateShortKeyFromVersion212 = (db: typeof ConfigStore, shortKeyConfig: IShortKeyConfigs | IOldShortKeyConfigs) => {
// #557 极端情况可能会出现配置不存在,需要重新写入
@@ -19,7 +20,7 @@ const updateShortKeyFromVersion212 = (db: typeof ConfigStore, shortKeyConfig: IS
}
if (shortKeyConfig.upload) {
// @ts-ignore
shortKeyConfig['picgo:upload'] = {
shortKeyConfig[SHORTKEY_COMMAND_UPLOAD] = {
enable: true,
key: shortKeyConfig.upload,
name: 'upload',
+3
View File
@@ -7,6 +7,7 @@ import {
import picgo from '@core/picgo'
import logger from '@core/picgo/logger'
import axios from 'axios'
import formUploader from './middlewares/formUploader'
class Server {
private httpServer: http.Server
@@ -56,6 +57,8 @@ class Server {
success: false
}
})
} else if (formUploader.isFileUpload(request)) {
formUploader.handleFileUpload(request, response)
} else {
let body: string = ''
let postObj: IObj
+125
View File
@@ -0,0 +1,125 @@
import http from 'http'
import multer from 'multer'
import path from 'path'
import fs from 'fs'
import { dbPathDir } from 'apis/core/datastore/dbChecker'
import logger from '@core/picgo/logger'
import { handleResponse } from '../utils'
import routers from '../routerManager'
import { FORM_IMAGE_FOLDER } from '~/universal/utils/static'
// Multer 错误类型定义
export interface MulterError extends Error {
code: string
field?: string
storageErrors?: any[]
}
// Multer 中间件适配器类型
export type MulterMiddleware = (
req: http.IncomingMessage,
res: http.ServerResponse,
callback: (error?: MulterError) => void
) => void;
// 扩展 multer 函数的返回类型
declare module 'multer' {
interface Multer {
array(fieldname: string, maxCount?: number): MulterMiddleware;
single(fieldname: string): MulterMiddleware;
fields(fields: { name: string; maxCount?: number }[]): MulterMiddleware;
none(): MulterMiddleware;
}
}
class FormUploader {
private upload!: multer.Multer
private formImagesPath!: string
constructor () {
this.initializeStorage()
this.setupMulter()
}
private initializeStorage () {
const STORE_PATH = dbPathDir()
this.formImagesPath = path.join(STORE_PATH, FORM_IMAGE_FOLDER)
if (!fs.existsSync(this.formImagesPath)) {
fs.mkdirSync(this.formImagesPath, { recursive: true })
}
}
private setupMulter () {
const storage = multer.diskStorage({
destination: (req: http.IncomingMessage, file: Express.Multer.File, cb: (error: Error | null, destination: string) => void) => {
cb(null, this.formImagesPath)
},
filename: (req: http.IncomingMessage, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => {
cb(null, file.originalname || Date.now() + path.extname(file.originalname))
}
})
this.upload = multer({
storage
})
}
/**
* 处理文件上传的中间件
*/
public handleFileUpload = (request: http.IncomingMessage, response: http.ServerResponse): void => {
logger.info('[PicGo Server] handling file upload')
this.upload.array('files')(request, response, async (err?: MulterError) => {
if (err) {
logger.error('[PicGo Server] file upload error', err)
return handleResponse({
response,
body: {
success: false,
message: 'File upload failed'
}
})
}
try {
const files = request.files
if (!files || files.length === 0) {
return handleResponse({
response,
body: {
success: false,
message: 'No files were uploaded'
}
})
}
const filePaths = files.map((file) => file.path)
logger.info('[PicGo Server] files uploaded: ' + filePaths.join(', '))
const handler = routers.getHandler(request.url!)
handler!({
list: filePaths,
response
})
} catch (err: any) {
logger.error('[PicGo Server] process upload files error', err)
handleResponse({
response,
body: {
success: false,
message: 'Failed to process uploaded files'
}
})
}
})
}
/**
* 检查请求是否为文件上传
*/
public isFileUpload (request: http.IncomingMessage): boolean {
return !!(request.headers['content-type'] && request.headers['content-type'].includes('multipart/form-data'))
}
}
export default new FormUploader()
+2 -2
View File
@@ -4,7 +4,7 @@ import {
} from './utils'
import logger from '@core/picgo/logger'
import windowManager from 'apis/app/window/windowManager'
import { uploadChoosedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis'
import { uploadSelectedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis'
import path from 'path'
import { dbPathDir } from 'apis/core/datastore/dbChecker'
const STORE_PATH = dbPathDir()
@@ -51,7 +51,7 @@ router.post('/upload', async ({
}
})
const win = windowManager.getAvailableWindow()
const res = await uploadChoosedFiles(win.webContents, pathList)
const res = await uploadSelectedFiles(win.webContents, pathList)
logger.info('[PicGo Server] upload result', res.join(' ; '))
if (res.length) {
handleResponse({
+32 -8
View File
@@ -5,6 +5,7 @@ import { dbPathChecker } from 'apis/core/datastore/dbChecker'
import yaml from 'js-yaml'
import { i18nManager } from '~/main/i18n'
// import { ILocales } from '~/universal/types/i18n'
import { getStaticPath } from '#/utils/staticPath'
const configPath = dbPathChecker()
const CONFIG_DIR = path.dirname(configPath)
@@ -16,6 +17,29 @@ function beforeOpen () {
resolveClipboardImageGenerator()
resolveOtherI18nFiles()
}
function copyFileOutsideOfElectronAsar (
sourceInAsarArchive: string,
destOutsideAsarArchive: string
) {
if (fs.existsSync(sourceInAsarArchive)) {
// file will be copied
if (fs.statSync(sourceInAsarArchive).isFile()) {
const file = destOutsideAsarArchive
const dir = path.dirname(file)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
fs.writeFileSync(file, fs.readFileSync(sourceInAsarArchive))
} else if (fs.statSync(sourceInAsarArchive).isDirectory()) {
fs.readdirSync(sourceInAsarArchive).forEach(function (fileOrFolderName) {
copyFileOutsideOfElectronAsar(
`${sourceInAsarArchive}/${fileOrFolderName}`,
`${destOutsideAsarArchive}/${fileOrFolderName}`
)
})
}
}
}
/**
* macOS 右键菜单
@@ -26,7 +50,7 @@ function resolveMacWorkFlow () {
return true
} else {
try {
fs.copySync(path.join(__static, 'Upload pictures with PicGo.workflow'), dest)
copyFileOutsideOfElectronAsar(getStaticPath('Upload pictures with PicGo.workflow'), dest)
} catch (e) {
console.log(e)
}
@@ -64,16 +88,16 @@ function resolveClipboardImageGenerator () {
function getClipboardFiles () {
const files = [
'/linux.sh',
'/mac.applescript',
'/windows.ps1',
'/windows10.ps1',
'/wsl.sh'
'linux.sh',
'mac.applescript',
'windows.ps1',
'windows10.ps1',
'wsl.sh'
]
return files.map(item => {
return {
origin: path.join(__static, item),
origin: getStaticPath(item),
dest: path.join(CONFIG_DIR, item)
}
})
@@ -88,7 +112,7 @@ function resolveOtherI18nFiles () {
if (!fs.pathExistsSync(i18nFolder)) {
fs.mkdirSync(i18nFolder)
}
i18nManager.setOutterI18nFolder(i18nFolder)
i18nManager.setOuterI18nFolder(i18nFolder)
const i18nFiles = fs.readdirSync(path.join(CONFIG_DIR, 'i18n'), {
withFileTypes: true
})
@@ -0,0 +1,28 @@
import path from 'path'
import fs from 'fs-extra'
import { getFormImageFolderPath } from '@core/datastore/dbChecker'
import logger from '@core/picgo/logger'
export const cleanupFormUploaderFiles = (fileInfoList?: string[] | ImgInfo[]): void => {
const formImageFolderPath = getFormImageFolderPath()
if (Array.isArray(fileInfoList)) {
fileInfoList.forEach(async fileInfo => {
const filePath = typeof fileInfo === 'string' ? fileInfo : fileInfo?.imgPath
if (!filePath) {
return
}
try {
// 检查文件路径是否在 formImageFolderPath 目录下
const relativePath = path.relative(formImageFolderPath, filePath)
const isWithinFolder = !relativePath.startsWith('..') && !path.isAbsolute(relativePath)
if (isWithinFolder && await fs.pathExists(filePath)) {
await fs.remove(filePath)
logger.info(`[PicGo] Deleted temp file: ${filePath}`)
}
} catch (error: any) {
logger.error('[PicGo] Failed to delete temp file', filePath, error)
}
})
}
}
+83 -20
View File
@@ -1,9 +1,12 @@
import fs from 'fs-extra'
import db from '~/main/apis/core/datastore'
import { clipboard, Notification, dialog } from 'electron'
import { handleUrlEncode } from '~/universal/utils/common'
import { readClipboardFilePaths } from 'clip-filepaths'
import crypto from 'node:crypto'
export const handleCopyUrl = (str: string): void => {
if (db.get('settings.autoCopy') !== false) {
if (db.get('settings.autoCopyUrl') !== false) {
clipboard.writeText(str)
}
}
@@ -52,7 +55,7 @@ export const showMessageBox = (options: any) => {
})
}
export const calcDurationRange = (duration: number) => {
export const calcUploadProcessDurationRange = (duration: number) => {
if (duration < 1000) {
return 500
} else if (duration < 1500) {
@@ -76,6 +79,44 @@ export const calcDurationRange = (duration: number) => {
return 100000
}
// 1 2 3 4 5 6 7 8 9 10 20 30 40 50 60 70 80 90 100 200 300 ...
export const calcUploadBigFileSizeRange = (fileSizeMB: number) => {
if (fileSizeMB < 10) {
// 3.2 -> 3, 3.6 -> 4
const result = Math.round(fileSizeMB);
return result === 0 && fileSizeMB > 0 ? 1 : result;
}
else if (fileSizeMB < 100) {
// 13 -> 1.3 -> 1 -> 10
// 17 -> 1.7 -> 2 -> 20
return Math.round(fileSizeMB / 10) * 10;
}
else {
// 135 -> 1.35 -> 1 -> 100
// 160 -> 1.60 -> 2 -> 200
return Math.round(fileSizeMB / 100) * 100;
}
}
// 1 2 3 4 5 6 7 8 9 10 20 30 40 50 60 70 80 90 100 200 300 ...
export const calcVideoDurationRange = (durationSec: number) => {
if (durationSec < 10) {
// 3.2 -> 3, 3.6 -> 4
const result = Math.round(durationSec);
return result === 0 && durationSec > 0 ? 1 : result;
}
else if (durationSec < 100) {
// 13 -> 1.3 -> 1 -> 10
// 17 -> 1.7 -> 2 -> 20
return Math.round(durationSec / 10) * 10;
}
else {
// 135 -> 1.35 -> 1 -> 100
// 160 -> 1.60 -> 2 -> 200
return Math.round(durationSec / 100) * 100;
}
}
/**
* macOS public.file-url will get encoded file path,
* so we need to decode it
@@ -96,24 +137,46 @@ export const ensureFilePath = (filePath: string, prefix = 'file://'): string =>
* for builtin clipboard to get image path from clipboard
* @returns
*/
export const getClipboardFilePath = (): string => {
// TODO: linux support
const img = clipboard.readImage()
if (img.isEmpty()) {
if (process.platform === 'win32') {
const imgPath = clipboard.readBuffer('FileNameW')?.toString('ucs2')?.replace(RegExp(String.fromCharCode(0), 'g'), '')
if (imgPath) {
return imgPath
}
export const getClipboardFilePathList = (): string[] => {
const { filePaths } = readClipboardFilePaths()
return filePaths
}
export const handleUrlEncodeWithSetting = (url: string) => {
if (db.get('settings.encodeOutputURL') === true) {
url = handleUrlEncode(url)
}
return url
}
export const replaceHost = (url: string, oldHost: string, newHost: string) => {
try {
const parsedUrl = new URL(url)
if (parsedUrl.host === oldHost) {
parsedUrl.host = newHost
}
} else {
if (process.platform === 'darwin') {
let imgPath = clipboard.read('public.file-url') // will get file://xxx/xxx
imgPath = ensureFilePath(imgPath)
if (imgPath) {
return imgPath.replace('file://', '')
}
}
return parsedUrl.toString()
} catch {
return url
}
}
export const getHost = (url: string = '') => {
try {
const parsedUrl = new URL(url)
return parsedUrl.host
} catch {
return ''
}
return ''
}
/**
* remove protocol and suffix
*/
export const removeProtocolAndSuffix = (url: string = '') => {
return url.replace(/^(https?:\/\/)?/, '').replace(/\/$/, '')
}
export const md5 = (str: string): string => {
return crypto.createHash('md5').update(str).digest('hex')
}
+2
View File
@@ -0,0 +1,2 @@
export const MB = 1024 * 1024
export const SECOND = 1000
+119
View File
@@ -0,0 +1,119 @@
import { ipcMain, IpcMainEvent, type WebContents } from "electron";
import { deviceIdManager } from "./deviceId";
import { REGISTER_DEVICE_ID, TALKING_DATA_EVENT } from "~/universal/events/constants";
import type { IImgInfo } from "picgo";
import { calcUploadBigFileSizeRange, calcUploadProcessDurationRange, calcVideoDurationRange } from "./common";
import { app } from "electron/main";
import db from "~/main/apis/core/datastore";
import { getVideoDuration } from "@picgo/video-duration";
import { MB, SECOND } from "./constants";
export interface IReportUploadDataOptions {
fromClipboard: boolean;
duration: number;
outputList: IImgInfo[];
}
class DataReportManager {
private deviceId: string | null = null;
private hasRegisterDeviceID: boolean = false;
constructor () {
this.init()
this.handleRegisterDeviceID()
}
private handleRegisterDeviceID() {
ipcMain.once(REGISTER_DEVICE_ID, (_evt: IpcMainEvent) => {
this.hasRegisterDeviceID = true;
console.log('Device ID registered');
});
}
private async init () {
if (this.deviceId) return;
this.deviceId = await deviceIdManager.getId()
}
public async reportUploadData(webContents: WebContents, options: IReportUploadDataOptions) {
await this.init();
await this.registerDeviceID(webContents);
const { fromClipboard, duration, outputList } = options;
const fileList = outputList.map(item => {
return {
fileName: item.fileName,
filePath: item.filePath,
mimeType: item.mimeType,
size: item.size || 0
}
})
const uploadEventData: ITalkingDataOptions = {
EventId: 'upload',
Label: '',
MapKv: {
by: fromClipboard ? 'clipboard' : 'files', // 上传剪贴板图片还是选择的文文件
count: fileList.length, // 上传的数量
duration: calcUploadProcessDurationRange(duration || 0), // 上传耗时
type: db.get('picBed.uploader') || db.get('picBed.current') || 'smms',
}
}
this.reportDataToWebContents(webContents, uploadEventData);
fileList.forEach(async file => {
if (file?.mimeType?.startsWith('video/') && file.filePath) {
const metadata = await getVideoDuration(file.filePath);
const sizeMB = metadata.size / MB;
const durationSec = (metadata.duration / SECOND) || 0;
const videoEventData: ITalkingDataOptions = {
EventId: 'upload_video',
Label: '',
MapKv: {
sizeRange: calcUploadBigFileSizeRange(sizeMB),
durationRange: calcVideoDurationRange(durationSec),
}
};
this.reportDataToWebContents(webContents, videoEventData);
} else if (file?.mimeType?.startsWith('image/') || fromClipboard) {
const imageEventData: ITalkingDataOptions = {
EventId: 'upload_image',
Label: '',
MapKv: {
type: db.get('picBed.uploader') || db.get('picBed.current') || 'smms',
mimeType: file.mimeType,
sizeRange: calcUploadBigFileSizeRange(file.size / MB)
}
};
this.reportDataToWebContents(webContents, imageEventData);
}
});
}
public async registerDeviceID(webContents: WebContents) {
if (this.hasRegisterDeviceID) return;
await this.init();
webContents.send(REGISTER_DEVICE_ID, this.deviceId);
}
private reportDataToWebContents(webContents: WebContents, data: ITalkingDataOptions) {
webContents.send(TALKING_DATA_EVENT, {
...data,
MapKv: {
...data.MapKv,
deviceId: this.deviceId,
version: app.getVersion(),
area: this.getAreaFromTimezone()
}
});
}
private getAreaFromTimezone() {
try {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (!timeZone) return 'UNKNOWN';
if (timeZone === 'Asia/Shanghai') return 'CN';
const region = timeZone.split('/')[0]; // Asia, America, Europe
return region;
} catch (e) {
return 'UNKNOWN';
}
}
}
export const dataReportManager = new DataReportManager()
+62
View File
@@ -0,0 +1,62 @@
import { networkInterfaceDefault } from 'systeminformation'
import { md5 } from './common'
import writeFile from 'write-file-atomic'
import { DEVICE_ID_PATH } from './env'
import fs from 'fs-extra'
class DeviceIdManager {
private deviceId: string | null = null
constructor() {
this.init()
}
private async init() {
await this.loadDeviceId()
}
private async getDeviceIdWithFallback(): Promise<string> {
try {
if (fs.existsSync(DEVICE_ID_PATH)) {
const deviceId = await fs.readFile(DEVICE_ID_PATH, 'utf-8')
if (deviceId && deviceId?.trim().length > 0) {
return deviceId.trim()
}
}
const netInterfaceMac = await networkInterfaceDefault()
if (netInterfaceMac) {
return md5(netInterfaceMac)
} else {
// random fallback
return md5(`${new Date().getTime()}-${Math.random().toString(36).substring(2, 15)}`)
}
} catch (error) {
// random fallback
return md5(`${new Date().getTime()}-${Math.random().toString(36).substring(2, 15)}`)
}
}
private async saveDeviceId(id: string): Promise<void> {
try {
await writeFile(DEVICE_ID_PATH, id, { encoding: 'utf-8' })
} catch (error) {
console.error('Failed to save device ID:', error)
}
}
private async loadDeviceId(): Promise<string> {
this.deviceId = await this.getDeviceIdWithFallback()
await this.saveDeviceId(this.deviceId)
return this.deviceId
}
public async getId(): Promise<string> {
if (this.deviceId) {
return this.deviceId
}
this.deviceId = await this.loadDeviceId()
return this.deviceId
}
}
export const deviceIdManager = new DeviceIdManager()
+35
View File
@@ -0,0 +1,35 @@
import path from 'path'
import { app } from 'electron'
import { pathToFileURL } from 'url'
export const isDev = !app.isPackaged
const defaultStaticPath = process.env.STATIC_PATH || (isDev
? path.join(process.cwd(), 'public')
: path.join(process.resourcesPath, 'public'))
process.env.STATIC_PATH = defaultStaticPath
const rendererHtmlPath = path.join(__dirname, '../renderer/index.html')
export const initStaticPath = () => defaultStaticPath
export const getRendererBaseUrl = () => {
if (isDev && process.env.ELECTRON_RENDERER_URL) {
return process.env.ELECTRON_RENDERER_URL
}
return pathToFileURL(rendererHtmlPath).toString()
}
export const buildRendererUrl = (hash?: string) => {
const base = getRendererBaseUrl()
if (!hash) return base
const cleanedHash = hash.startsWith('#') ? hash : `#${hash}`
return `${base}${cleanedHash}`
}
export const getStaticPath = () => process.env.STATIC_PATH || defaultStaticPath
// paths
export const STORE_PATH = app.getPath('userData')
export const DEVICE_ID_PATH = path.join(STORE_PATH, 'picgo-device-id')
@@ -1,11 +1,16 @@
// for referer policy, we can't use it in renderer
import axios from 'axios'
import { RELEASE_URL, RELEASE_URL_BACKUP } from './static'
import { RELEASE_URL, RELEASE_URL_BACKUP } from '../../universal/utils/static'
import yaml from 'js-yaml'
export const getLatestVersion = async (isCheckBetaUpdate: boolean = false) => {
let res: string = ''
try {
res = await axios.get(RELEASE_URL).then(r => {
res = await axios.get(RELEASE_URL, {
headers: {
Referer: 'https://github.com'
}
}).then(r => {
const list = r.data as IStringKeyMap[]
if (isCheckBetaUpdate) {
const betaList = list.filter(item => item.name.includes('beta'))
@@ -14,7 +19,11 @@ export const getLatestVersion = async (isCheckBetaUpdate: boolean = false) => {
const normalList = list.filter(item => !item.name.includes('beta'))
return normalList[0].name
}).catch(async () => {
const result = await axios.get(isCheckBetaUpdate ? `${RELEASE_URL_BACKUP}/latest.beta.yml` : `${RELEASE_URL_BACKUP}/latest.yml`)
const result = await axios.get(isCheckBetaUpdate ? `${RELEASE_URL_BACKUP}/latest.beta.yml` : `${RELEASE_URL_BACKUP}/latest.yml`, {
headers: {
Referer: 'https://github.com'
}
})
const r = yaml.load(result.data) as IStringKeyMap
return r.version
})
+1 -1
View File
@@ -1,6 +1,6 @@
import { uuid } from 'uuidv4'
import { trimValues } from '#/utils/common'
import picgo from '@core/picgo'
import { v4 as uuid } from 'uuid'
export const handleConfigWithFunction = (config: IPicGoPluginOriginConfig[]): IPicGoPluginConfig[] => {
for (const i in config) {
+6 -3
View File
@@ -1,5 +1,5 @@
import { IPasteStyle } from '#/types/enum'
import { handleUrlEncode } from '#/utils/common'
import { handleUrlEncodeWithSetting } from './common'
const formatCustomLink = (customLink: string, item: ImgInfo) => {
const fileName = item.fileName!.replace(new RegExp(`\\${item.extname}$`), '')
@@ -21,14 +21,17 @@ const formatCustomLink = (customLink: string, item: ImgInfo) => {
}
export default (style: IPasteStyle, item: ImgInfo, customLink: string | undefined) => {
const url = handleUrlEncode(item.url || item.imgUrl)
const url = handleUrlEncodeWithSetting(item.url || item.imgUrl)
const _customLink = customLink || '$url'
const tpl = {
markdown: `![](${url})`,
HTML: `<img src="${url}"/>`,
URL: url,
UBB: `[IMG]${url}[/IMG]`,
Custom: formatCustomLink(_customLink, item)
Custom: formatCustomLink(_customLink, {
...item,
url
})
}
return tpl[style]
}
+1 -1
View File
@@ -3,7 +3,7 @@ import db from '~/main/apis/core/datastore'
import pkg from 'root/package.json'
import { lt } from 'semver'
import { T } from '~/main/i18n'
import { getLatestVersion } from '#/utils/getLatestVersion'
import { getLatestVersion } from '~/main/utils/getLatestVersion'
const version = pkg.version
// const releaseUrl = 'https://api.github.com/repos/Molunerfinn/PicGo/releases'
// const releaseUrlBackup = 'https://picgo-1251750343.cos.ap-chengdu.myqcloud.com'
+13
View File
@@ -0,0 +1,13 @@
// temp no used
// will be refactor in future
import { contextBridge, webUtils } from 'electron'
const getFilePath = (file: File): string => webUtils.getPathForFile(file)
const electronApi = {
getFilePath
}
contextBridge.exposeInMainWorld('electronApi', electronApi)
export type ElectronApi = typeof electronApi
+34 -3
View File
@@ -1,12 +1,43 @@
<template>
<div id="app">
<router-view></router-view>
<router-view />
</div>
</template>
<script>
<script lang="ts" setup>
import { useStore } from '@/hooks/useStore'
import { onBeforeMount, onMounted, onUnmounted } from 'vue'
import { getConfig } from './utils/dataSender'
import type { IConfig } from 'picgo'
import bus from './utils/bus'
import { FORCE_UPDATE } from '~/universal/events/constants'
import { useATagClick } from './hooks/useATagClick'
useATagClick()
const store = useStore()
onBeforeMount(async () => {
const config = await getConfig<IConfig>()
if (config) {
store?.setDefaultPicBed(config?.picBed?.uploader || config?.picBed?.current || 'smms')
}
})
onMounted(() => {
bus.on(FORCE_UPDATE, () => {
store?.updateForceUpdateTime()
})
})
onUnmounted(() => {
bus.off(FORCE_UPDATE)
})
</script>
<script lang="ts">
export default {
name: 'picgo'
name: 'PicGoApp'
}
</script>
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
-37
View File
@@ -1,37 +0,0 @@
<template>
<div id="choose-pic-bed">
<span>{{ $T('CHOOSE_YOUR_DEFAULT_PICBED', { d: label }) }}</span>
<el-switch
v-model="value"
@change="choosePicBed"
>
</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) {
this.value = true
}
}
choosePicBed () {
this.saveConfig({
'picBed.current': this.type,
'picBed.uploader': this.type
})
this.$emit('update:choosed', this.type)
}
}
</script>
<style lang='stylus'>
</style>
+112 -151
View File
@@ -1,180 +1,141 @@
<template>
<div id="config-form">
<el-form
label-position="right"
label-width="120px"
:model="ruleForm"
ref="form"
size="mini"
<div
id="config-form"
:class="props.colorMode === 'white' ? 'white' : ''"
>
<BaseConfigForm
ref="$form"
:config="configList"
:form-model="formModel"
>
<el-form-item
:label="$T('UPLOADER_CONFIG_NAME')"
required
prop="_configName"
>
<el-input
type="input"
v-model="ruleForm._configName"
:placeholder="$T('UPLOADER_CONFIG_PLACEHOLDER')"
></el-input>
</el-form-item>
<!-- dynamic config -->
<el-form-item
v-for="(item, index) in configList"
:label="item.alias || item.name"
:required="item.required"
:prop="item.name"
:key="item.name + index"
>
<el-input
v-if="item.type === 'input' || item.type === 'password'"
:type="item.type === 'password' ? 'password' : 'input'"
v-model="ruleForm[item.name]"
:placeholder="item.message || item.name"
></el-input>
<el-select
v-else-if="item.type === 'list' && item.choices"
v-model="ruleForm[item.name]"
:placeholder="item.message || item.name"
<template #extra-form>
<el-form-item
v-if="isUploader"
:label="$T('UPLOADER_CONFIG_NAME')"
required
prop="_configName"
>
<el-option
v-for="choice in item.choices"
:label="choice.name || choice.value || choice"
:key="choice.name || choice.value || choice"
:value="choice.value || choice"
></el-option>
</el-select>
<el-select
v-else-if="item.type === 'checkbox' && item.choices"
v-model="ruleForm[item.name]"
:placeholder="item.message || item.name"
multiple
collapse-tags
>
<el-option
v-for="choice in item.choices"
:label="choice.name || choice.value || choice"
:key="choice.value || choice"
:value="choice.value || choice"
></el-option>
</el-select>
<el-switch
v-else-if="item.type === 'confirm'"
v-model="ruleForm[item.name]"
active-text="yes"
inactive-text="no"
>
</el-switch>
</el-form-item>
<slot></slot>
</el-form>
<el-input
v-model="formModel._configName"
type="input"
:placeholder="$T('UPLOADER_CONFIG_PLACEHOLDER')"
/>
</el-form-item>
</template>
<slot />
</BaseConfigForm>
</div>
</template>
<script lang="ts">
import {
Component,
Vue,
Prop,
Watch
} from 'vue-property-decorator'
import { cloneDeep, union } from 'lodash'
<script lang="ts" setup>
import { reactive, ref, watch, toRefs } from 'vue'
import { getConfig } from '@/utils/dataSender'
import { useRoute } from 'vue-router'
import BaseConfigForm from './form/BaseConfigForm.vue'
import { useConfigForm } from '@/hooks/useConfigForm'
@Component({
name: 'config-form'
})
export default class extends Vue {
@Prop() private config!: any[]
@Prop() readonly type!: 'uploader' | 'transformer' | 'plugin'
@Prop() readonly id!: string
configList: IPicGoPluginConfig[] = []
ruleForm: IStringKeyMap = {}
@Watch('config', {
deep: true,
immediate: true
})
handleConfigChange (val: any) {
this.handleConfig(val)
}
interface IProps {
config: any[]
type: 'uploader' | 'transformer' | 'plugin'
id: string
colorMode?: 'white' | 'dark'
}
async validate () {
return new Promise((resolve) => {
// @ts-ignore
this.$refs.form.validate((valid: boolean) => {
if (valid) {
resolve(this.ruleForm)
} else {
resolve(false)
return false
}
})
})
}
const props = defineProps<IProps>()
const $route = useRoute()
const $form = ref<IFormInstance>()
const configList = ref<IPicGoPluginConfig[]>([])
const formModel = reactive<IStringKeyMap>({})
const isUploader = props.type === 'uploader'
getConfigType () {
switch (this.type) {
case 'plugin': {
return this.id
}
case 'uploader': {
return `picBed.${this.id}`
}
case 'transformer': {
return `transformer.${this.id}`
}
default:
return 'unknown'
async function validate (): Promise<IStringKeyMap | false> {
const res = await $form.value?.validate() || false
return res
}
function getConfigType () {
switch (props.type) {
case 'plugin': {
return props.id
}
}
async handleConfig (val: IPicGoPluginConfig[]) {
const config = await this.getCurConfigFormData()
const configId = this.$route.params.configId
this.ruleForm = Object.assign({}, config)
if (val.length > 0) {
this.configList = cloneDeep(val).map((item) => {
if (!configId) return item
let defaultValue = item.default !== undefined
? item.default
: item.type === 'checkbox'
? []
: null
if (item.type === 'checkbox') {
const defaults = item.choices?.filter((i: any) => {
return i.checked
}).map((i: any) => i.value) || []
defaultValue = union(defaultValue, defaults)
}
if (config && config[item.name] !== undefined) {
defaultValue = config[item.name]
}
this.$set(this.ruleForm, item.name, defaultValue)
return item
})
case 'uploader': {
return `picBed.${props.id}`
}
}
async getCurConfigFormData () {
const configId = this.$route.params.configId
const curTypeConfigList = await this.getConfig<IStringKeyMap[]>(`uploader.${this.id}.configList`) || []
return curTypeConfigList.find(i => i._id === configId) || {}
case 'transformer': {
return `transformer.${props.id}`
}
default:
return 'unknown'
}
}
const handleConfigForm = useConfigForm()
async function handleConfig (inputConfig: IPicGoPluginConfig[]) {
const currentConfig = await getCurConfigFormData()
const resetConfig = isUploader && !$route.params.configId
Object.assign(formModel, currentConfig)
configList.value = handleConfigForm(inputConfig, formModel, currentConfig, resetConfig)
}
async function getCurConfigFormData () {
const configId = $route.params.configId
const configType = getConfigType()
if (isUploader) {
const curTypeConfigList = await getConfig<IStringKeyMap[]>(`uploader.${props.id}.configList`) || []
return curTypeConfigList.find(i => i._id === configId) || {}
} else {
const config = await getConfig<IStringKeyMap>(configType)
return config || {}
}
}
async function resetConfig () {
const config = await getCurConfigFormData()
Object.assign(formModel, config)
}
watch(toRefs(props.config), (val: IPicGoPluginConfig[]) => {
handleConfig(val)
}, {
deep: true,
immediate: true
})
defineExpose({
validate,
getConfigType,
resetConfig
})
</script>
<style lang='stylus'>
.config-form-common-tips
a
color #409EFF
text-decoration none
#config-form
.el-form
label
line-height 22px
padding-bottom 0
&-item
display: flex
justify-content space-between
border-bottom 1px solid darken(#eee, 50%)
padding-bottom 16px
&:last-child
border-bottom none
&__content
justify-content flex-end
.el-button-group
width 100%
.el-button
width 50%
.el-input__inner
border-radius 19px
.el-radio-group
margin-left 25px
.el-switch__label
&.is-active
color #409EFF
&.white
.el-form-item
border-bottom 1px solid #ddd
</style>
@@ -1,70 +0,0 @@
<template>
<el-dialog
:title="inputBoxOptions.title || $T('INPUT')"
: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>{{ $T('CANCEL') }}</el-button>
<el-button type="primary" @click="handleInputBoxConfirm" round>{{ $T('CONFIRM') }}</el-button>
</span>
</el-dialog>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { 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>
@@ -0,0 +1,29 @@
<template>
<div class="toolbox-handler">
<ElButton
type="text"
@click="() => props.handler(value)"
>
{{ props.handlerText }}
</ElButton>
</div>
</template>
<script lang="ts" setup>
import { IToolboxItemCheckStatus } from '~/universal/types/enum'
interface IProps {
status: IToolboxItemCheckStatus
value: any
handlerText: string
handler: (value: any) => void | Promise<void>
}
const props = defineProps<IProps>()
</script>
<script lang="ts">
export default {
name: 'ToolboxHandler'
}
</script>
<style lang='stylus'>
</style>
@@ -0,0 +1,48 @@
<template>
<el-icon
:color="color"
class="toolbox-status-icon"
>
<template v-if="props.status === IToolboxItemCheckStatus.SUCCESS">
<SuccessFilled />
</template>
<template v-if="props.status === IToolboxItemCheckStatus.ERROR">
<CircleCloseFilled />
</template>
<template v-if="props.status === IToolboxItemCheckStatus.LOADING">
<Loading />
</template>
</el-icon>
</template>
<script lang="ts" setup>
import { CircleCloseFilled, Loading, SuccessFilled } from '@element-plus/icons-vue'
import { computed } from 'vue'
import { IToolboxItemCheckStatus } from '~/universal/types/enum'
interface IProps {
status: IToolboxItemCheckStatus
}
const props = defineProps<IProps>()
const color = computed(() => {
switch (props.status) {
case IToolboxItemCheckStatus.SUCCESS:
return '#67C23A'
case IToolboxItemCheckStatus.ERROR:
return '#F56C6C'
default:
return '#909399'
}
})
</script>
<script lang="ts">
export default {
name: 'ToolboxStatusIcon'
}
</script>
<style lang='stylus'>
.toolbox-status-icon {
margin-left: 8px;
}
</style>
@@ -0,0 +1,63 @@
<template>
<div class="">
<ConfirmDialog
v-model="visible"
:title="title"
@confirm="handleConfirm"
@cancel="handleCancel"
@close="handleCancel"
>
<template #body>
<BaseConfigForm
ref="$form"
:config="configList"
:form-model="formModel"
theme="light"
/>
</template>
</ConfirmDialog>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import ConfirmDialog from '@/components/dialog/ConfirmDialog.vue'
import { useIPCOn } from '@/hooks/useIPC'
import { IRPCActionType } from '~/universal/types/enum'
import BaseConfigForm from '@/components/form/BaseConfigForm.vue'
import { useConfigForm } from '@/hooks/useConfigForm'
import { sendToMain } from '@/utils/dataSender'
const configList = ref<IPicGoPluginConfig[]>([])
const formModel = reactive<IStringKeyMap>({})
const $form = ref<IFormInstance>()
const title = ref('')
const handleConfigForm = useConfigForm()
const visible = ref(false)
useIPCOn(IRPCActionType.OPEN_CONFIG_DIALOG, (event, options: IPicGoPluginShowConfigDialogOption) => {
visible.value = true
configList.value = handleConfigForm(options.config, formModel)
title.value = options.title
})
const handleConfirm = async () => {
const res = await $form.value?.validate() || false
if (!res) {
return
}
sendToMain(IRPCActionType.OPEN_CONFIG_DIALOG, res)
visible.value = false
}
const handleCancel = () => {
visible.value = false
sendToMain(IRPCActionType.OPEN_CONFIG_DIALOG, false)
}
</script>
<script lang="ts">
export default {
name: 'ConfigFormDialog'
}
</script>
<style lang='stylus'>
</style>
@@ -0,0 +1,85 @@
<template>
<ElDialog
v-model="visible"
:title="title"
@close="handleClose"
>
<slot name="body" />
<template #footer>
<slot
v-if="footerSlots"
name="footer"
/>
<template v-else>
<ElButton
type="default"
round
@click="handleCancel"
>
{{ cancelButtonText }}
</ElButton>
<ElButton
type="primary"
round
@click="handleConfirm"
>
{{ confirmButtonText }}
</ElButton>
</template>
</template>
</ElDialog>
</template>
<script lang="ts" setup>
import { useVModel } from '@/hooks/useVModel'
import { T as $T } from '@/i18n/index'
import { useSlots, ref } from 'vue'
interface IProps {
modelValue: boolean
title: string
confirmButtonText?: string
cancelButtonText?: string
}
const props = withDefaults(defineProps<IProps>(), {
confirmButtonText: $T('CONFIRM'),
cancelButtonText: $T('CANCEL')
})
const $emit = defineEmits(['confirm', 'cancel', 'close'])
const isCancel = ref(false)
const isConfirm = ref(false)
const visible = useVModel(props, 'modelValue')
const handleClose = () => {
if (isConfirm.value) {
return
}
if (isCancel.value) {
$emit('cancel')
isCancel.value = false
} else {
$emit('close')
}
}
const handleCancel = () => {
isCancel.value = true
visible.value = false
}
const handleConfirm = () => {
isConfirm.value = true
$emit('confirm')
}
const footerSlots = !!useSlots().footer
</script>
<script lang="ts">
export default {
name: 'ConfirmDialog'
}
</script>
<style lang='stylus'>
</style>

Some files were not shown because too many files have changed in this diff Show More