Compare commits

..
Author SHA1 Message Date
PiEgg 9274b3f8be 📦 Chore(fuck): electron-builder publish option 2020-01-01 19:38:31 +08:00
PiEgg 7809b626d3 🐛 Fix: cI build error in linux 2020-01-01 19:29:37 +08:00
PiEgg a7995c037c 📦 Chore: fix CI script 2020-01-01 19:01:47 +08:00
PiEgg 85f4c67e0b 🎉 Release: v2.2.0 2020-01-01 18:18:51 +08:00
501 changed files with 18986 additions and 65726 deletions
+4
View File
@@ -0,0 +1,4 @@
test/unit/coverage/**
test/unit/*.js
test/e2e/*.js
dist/
+23
View File
@@ -0,0 +1,23 @@
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'
},
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
-1
View File
@@ -1 +0,0 @@
github: ["Molunerfinn"]
+30
View File
@@ -0,0 +1,30 @@
<!--
PicGo Issue 模板
请依照该模板来提交,否则将会被关闭。
**提问之前请注意你看过FAQ、配置手册以及那些被关闭的issues。否则同样的提问也会被关闭!**
-->
## 问题类型
<!-- 你要提交的是Bug Report还是Feature Request-->
## PicGo的相关信息
<!-- 版本、所在平台(Mac或者Windows -->
## 问题重现
<!-- 如果是Bug Report请填写本项 -->
<!-- 请附上相关截图 -->
## 功能请求
<!-- 如果是Feature Request请填写本项 -->
<!-- 详细描述你所预想的功能或者是现有功能的改进。 -->
---
<!--
最后,喜欢PicGo的话不妨给它点个star~
如果可以的话,请我喝杯咖啡?首页有赞助二维码,谢谢你的支持!
-->
-68
View File
@@ -1,68 +0,0 @@
name: "🐛 Bug Report"
description: 提交一个问题 / Report a bug
title: "[Bug]: "
labels: ["bug"]
assignees:
- molunerfinn
body:
- type: markdown
attributes:
value: |+
## PicGo Issue 模板
请依照该模板来提交,否则将会被关闭。
**提问之前请注意你看过 FAQ、文档以及那些被关闭的 issues。否则同样的提问也会被关闭!**
Please submit according to this template, otherwise it will be closed.
**Before asking a question, please note that you have read the FAQ, Doc, and those closed issues. Otherwise the same question will also be closed! **
- type: checkboxes
id: read
attributes:
label: 前置阅读 | Pre-reading
description: 我已经自行查找、阅读以下内容(阅读了请打勾) | I have searched and read the following on my own (Please tick after reading)
options:
- label: "[文档/Doc](https://docs.picgo.app/gui/)"
required: true
- label: "[Issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed)"
required: true
- label: "[FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md)"
required: true
- type: input
id: version
attributes:
label: PicGo的版本 | PicGo Version
placeholder: 例如 v2.3.0-beta.1
validations:
required: true
- type: dropdown
id: platform
attributes:
label: 系统信息 | System Information
options:
- Windows
- Mac
- Mac(arm64)
- Linux
- All
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: 问题重现 | Bug reproduce
description: 请复述Bug重现流程,必要时附图 | Please tell me the bug reproduction process, attach pictures if necessary
validations:
required: true
- type: textarea
id: log
attributes:
label: 相关日志 | Logs
description: 请附上 PicGo 的相关报错日志(用文本的形式)。报错日志可以在 PicGo 设置 -> 设置日志文件 -> 点击打开 后找到 | Please attach PicGo's relevant error log (in text form). The error log can be found in PicGo Settings -> Set Log File -> Click to Open
- type: markdown
attributes:
value: |
最后,喜欢 PicGo 的话不妨给它点个 star~
如果可以的话,请我喝杯咖啡?首页有赞助二维码,谢谢你的支持!
Finally, if you like PicGo, give it a star~
Buy me a cup of coffee if you can? There is a sponsorship QR code on the homepage, thank you for your support!
-1
View File
@@ -1 +0,0 @@
blank_issues_enabled: false
@@ -1,63 +0,0 @@
name: "✨ Feature Request"
description: 功能请求 / Feature request
title: "[Feature]: "
labels: ["feature request"]
assignees:
- molunerfinn
body:
- type: markdown
attributes:
value: |+
## PicGo Issue 模板
请依照该模板来提交,否则将会被关闭。
**提问之前请注意你看过 FAQ、文档以及那些被关闭的 issues。否则同样的提问也会被关闭!**
Please submit according to this template, otherwise it will be closed.
**Before asking a question, please note that you have read the FAQ, Doc, and those closed issues. Otherwise the same question will also be closed! **
- type: checkboxes
id: read
attributes:
label: 前置阅读 | Pre-reading
description: 我已经自行查找、阅读以下内容(阅读了请打勾) | I have searched and read the following on my own (Please tick after reading)
options:
- label: "[文档/Doc](https://docs.picgo.app/gui/)"
required: true
- label: "[Issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed)"
required: true
- label: "[FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md)"
required: true
- type: input
id: version
attributes:
label: PicGo的版本 | PicGo Version
placeholder: 例如 v2.3.0-beta.1
validations:
required: true
- type: dropdown
id: platform
attributes:
label: 系统信息 | System Information
options:
- Windows
- Mac
- Mac(arm64)
- Linux
- All
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: 功能请求 | Feature request
description: 详细描述你所预想的功能或者是现有功能的改进 | Describe in detail the features you envision or improvements to existing features
validations:
required: true
- type: markdown
attributes:
value: |
最后,喜欢 PicGo 的话不妨给它点个 star~
如果可以的话,请我喝杯咖啡?首页有赞助二维码,谢谢你的支持!
Finally, if you like PicGo, give it a star~
Buy me a cup of coffee if you can? There is a sponsorship QR code on the homepage, thank you for your support!
@@ -1,71 +0,0 @@
name: Issue Duplicate Detection
on:
issues:
types:
- opened
- edited
- reopened
permissions:
contents: read
issues: write
jobs:
detect-duplicate-issues:
runs-on: ubuntu-latest
steps:
- name: Run Warp Agent duplicate detector
uses: warpdotdev/warp-agent-action@v1
id: duplicate_detector
env:
GH_TOKEN: ${{ github.token }}
with:
warp_api_key: ${{ secrets.WARP_API_KEY }}
profile: ${{ vars.WARP_AGENT_PROFILE || '' }}
prompt: |
You are triaging duplicate GitHub issues for this repository.
Repository: ${{ github.repository }}
Target issue number: #${{ github.event.issue.number }}
Target issue URL: ${{ github.event.issue.html_url }}
Target issue title: ${{ github.event.issue.title }}
Target issue body:
${{ github.event.issue.body }}
Use the GitHub CLI with GH_TOKEN for all operations.
Workflow requirements:
1. Gather issue context from title/body/error messages/symptoms/components.
- Run:
gh issue view ${{ github.event.issue.number }} --repo ${{ github.repository }} --json number,title,body,url,state,labels
2. Search with multiple strategies:
- title keywords
- error message fragments
- symptom words
- component/module names
Example command pattern:
gh issue list --repo ${{ github.repository }} --state all --search "<query>"
3. Inspect every candidate in detail:
gh issue view <candidate_number> --repo ${{ github.repository }} --json number,title,body,url,state
4. Duplicate threshold:
- only mark duplicate when confidence >= 90%
- same root cause + very similar symptoms/errors/components
5. Exclusions:
- never include pull requests
- never include the current issue itself (#${{ github.event.issue.number }})
6. If confidence is insufficient or no duplicates exist, exit without commenting.
7. If duplicates exist, create or update exactly one comment on issue #${{ github.event.issue.number }}:
- first line must be: <!-- issue-duplicate-detector -->
- include markdown bullet list with title + link:
- [Issue title](${{ github.server_url }}/${{ github.repository }}/issues/123)
8. Before posting, check existing comments on the target issue:
- if marker comment exists, update that comment
- otherwise create a new comment
9. Do not comment on any other issue.
Expected comment format:
<!-- issue-duplicate-detector -->
Detected potentially duplicate issues:
- [Duplicate issue title](${{ github.server_url }}/${{ github.repository }}/issues/123)
- [Another duplicate issue](${{ github.server_url }}/${{ github.repository }}/issues/456)
-393
View File
@@ -1,393 +0,0 @@
name: Build & Release
on:
push:
tags:
- v*
workflow_dispatch:
inputs:
release_tag:
description: "GitHub release tag to publish to (optional, defaults to current branch like dev)"
required: false
default: ""
type: string
build_os:
description: "Build for specific OS: Windows, macOS, Linux, All"
required: true
default: "All"
type: choice
options:
- Windows
- macOS
- Linux
- All
test_upload_dist:
description: "Test upload-dist.js script"
required: true
default: false
type: boolean
test_upload_dist_to_dev:
description: "Test upload-dist.js script to dev folder"
required: true
default: false
type: boolean
skip_mac_notarize:
description: "Skip Mac Notarization (true/false)"
required: true
default: false
type: boolean
win_signing_mode:
description: "Windows Signing Mode: release-signing(default) or test-signing"
required: true
default: "release-signing"
type: choice
options:
- release-signing
- test-signing
env:
NODE_VERSION: 22.x
jobs:
# ============== macOS Builds ==============
build-macos:
name: Build macOS (${{ matrix.arch }})
if: github.event.inputs.build_os == 'macOS' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-intel
target: dmg
arch: x64
- runner: macos-latest
target: dmg
arch: arm64
steps:
- name: Check out git repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.29.2
- 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
run: rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build macOS App (${{ matrix.arch }})
run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --mac ${{ matrix.target }} --${{ matrix.arch }} --publish never
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SKIP_NOTARIZE: ${{ inputs.skip_mac_notarize }}
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: PicGo-macOS-${{ matrix.arch }}
path: dist/*.*
# ============== Windows Builds ==============
build-windows:
name: Build Windows (${{ matrix.arch }})
if: github.event.inputs.build_os == 'Windows' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- runner: windows-latest
arch: x64-ia32
build_arch: --x64 --ia32
target: nsis
- runner: windows-11-arm
arch: arm64
build_arch: --arm64
target: nsis
steps:
- name: Check out git repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.29.2
- 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
run: |
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 }
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: Install dependencies
run: pnpm install --frozen-lockfile
# 1. Build (Unsigned)
- name: Build Windows App (${{ matrix.arch }})
run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --win ${{matrix.target}} ${{ matrix.build_arch }} --publish never
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
# 2. Upload Unsigned Artifact to SignPath (Intermediate Step)
- name: Upload Unsigned Artifact for Signing
id: upload-unsigned
uses: actions/upload-artifact@v4
with:
name: unsigned-${{ matrix.arch }}
path: dist/*.exe
retention-days: 1
if-no-files-found: error
- name: Notification for Signing Start
shell: bash
env:
NOTIFY_URL: ${{ secrets.SIGN_NOTIFICATION }}
run: curl "$NOTIFY_URL"
# 3. Submit to SignPath and Wait
- name: Sign Artifact with SignPath
uses: signpath/github-action-submit-signing-request@v2
env:
SIGNPATH_SIGNING_POLICY_SLUG: |
${{ (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') || inputs.win_signing_mode == 'release-signing')
&& 'release-signing'
|| 'test-signing' }}
ARTIFACT_SLUG: |
${{ (matrix.arch == 'x64-ia32')
&& 'PicGo-Windows'
|| (matrix.arch == 'arm64')
&& 'PicGo-Windows-ARM64'
|| '' }}
with:
api-token: "${{ secrets.SIGNPATH_API_TOKEN }}"
organization-id: "${{ secrets.SIGNPATH_ORGANIZATION_ID }}"
project-slug: "${{ secrets.SIGNPATH_PROJECT_SLUG }}"
signing-policy-slug: "${{ env.SIGNPATH_SIGNING_POLICY_SLUG }}"
github-artifact-id: "${{ steps.upload-unsigned.outputs.artifact-id }}"
artifact-configuration-slug: "${{ env.ARTIFACT_SLUG }}"
wait-for-completion: true
output-artifact-directory: "signed-artifact"
# 4. Replace Unsigned with Signed & Fix Blockmap (Critical for Auto-Update)
- name: Replace Unsigned with Signed & Update latest.yml
shell: powershell
run: |
Write-Host "[Info] Contents of signed-artifact:"
Get-ChildItem -Path "signed-artifact" -Recurse | Format-Table FullName, Length
# If returned zip files, extract them first
$zips = Get-ChildItem -Path "signed-artifact\*.zip" -ErrorAction SilentlyContinue
if ($zips) {
Write-Host "[Info] Found zip file(s), extracting..."
foreach ($zip in $zips) {
Expand-Archive -Path $zip.FullName -DestinationPath "signed-artifact" -Force
Remove-Item -Path $zip.FullName -Force
Write-Host "[Done] Extracted: $($zip.Name)"
}
Write-Host "[Info] Contents after extraction:"
Get-ChildItem -Path "signed-artifact" -Recurse | Format-Table FullName, Length
}
$exeFiles = Get-ChildItem -Path "signed-artifact\*.exe" -ErrorAction SilentlyContinue
if ($exeFiles) {
foreach ($exe in $exeFiles) {
Move-Item -Path $exe.FullName -Destination "dist\" -Force
Write-Host "[Done] Moved signed artifact: $($exe.Name)"
}
} else {
Write-Host "[Error] No .exe files found in signed-artifact!"
Get-ChildItem -Path "signed-artifact" -Recurse
exit 1
}
# Run the Node.js script to update latest.yml
node scripts/update-win-yaml.js
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: PicGo-Windows-${{ matrix.arch }}
path: dist/*.*
# ============== Linux Builds ==============
build-linux:
name: Build Linux (${{ matrix.arch }})
if: github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
arch: x64
target: AppImage deb snap
- runner: ubuntu-24.04-arm
arch: arm64
target: AppImage deb
steps:
- name: Check out git repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.29.2
- 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
run: rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y libfuse2
- name: Build Linux App (${{ matrix.arch }})
run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --linux ${{matrix.target}} --${{ matrix.arch }} --publish never
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: PicGo-Linux-${{ matrix.arch }}
path: dist/*.*
# ============== Release ==============
release:
name: Merge & Release
needs: [build-macos, build-windows, build-linux]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check out git repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.29.2
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: PicGo-*
- name: List artifacts
run: ls -laR artifacts/
- name: Merge artifacts and yml files
run: node scripts/merge-artifacts.js
- name: List dist
run: ls -la dist/
- name: Upload update metadata to release.picgo.app
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.test_upload_dist || github.event.inputs.test_upload_dist_to_dev
run: |
ARGS="--all --version-files-only"
if [[ "${{ github.event.inputs.test_upload_dist_to_dev }}" == "true" ]]; then
ARGS="$ARGS --dev"
echo "🚧 Test Upload Mode: ON"
fi
node scripts/upload-dist.js $ARGS
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 }}
- name: Publish GitHub Workflow Release
if: github.event_name == 'workflow_dispatch'
uses: softprops/action-gh-release@v2
continue-on-error: true
with:
token: ${{ secrets.GH_TOKEN }}
tag_name: ${{ github.event.inputs.release_tag || github.ref_name }}
draft: true
prerelease: false
files: |
dist/*.exe
dist/*.dmg
dist/*.zip
dist/*.AppImage
dist/*.deb
dist/*.snap
dist/*.tar.gz
dist/*.yml
dist/*.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: |
dist/*.exe
dist/*.dmg
dist/*.zip
dist/*.AppImage
dist/*.deb
dist/*.snap
dist/*.tar.gz
dist/*.yml
dist/*.blockmap
@@ -1,64 +0,0 @@
name: Mirror Release to AtomGit
on:
push:
branches:
- atomgit-release-ci-test
release:
types:
- published
workflow_dispatch:
inputs:
release_tag:
description: Published GitHub release tag to mirror
required: true
type: string
permissions:
contents: read
concurrency:
group: mirror-atomgit-${{ github.event.release.tag_name || inputs.release_tag }}
cancel-in-progress: false
jobs:
mirror-release:
name: Mirror GitHub Release to AtomGit
runs-on: ubuntu-latest
env:
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag || 'v3.0.2' }}
ATOM_OWNER: ${{ vars.ATOM_OWNER || 'Molunerfinn' }}
ATOM_REPO: ${{ vars.ATOM_REPO || 'PicGo' }}
ATOM_TARGET_COMMITISH: ${{ vars.ATOM_TARGET_COMMITISH }}
ATOM_UPLOAD_TIMEOUT_MINUTES: ${{ vars.ATOM_UPLOAD_TIMEOUT_MINUTES || '70' }}
steps:
- name: Check out git repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
- name: Read GitHub release metadata
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release view "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--json tagName,name,body,isDraft,isPrerelease,targetCommitish \
> github-release.json
- name: Download GitHub release assets
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p dist
gh release download "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--dir dist
- name: Publish AtomGit release
env:
ATOM_PAT: ${{ secrets.ATOM_PAT }}
run: node scripts/publish-atomgit-release.js
-16
View File
@@ -4,7 +4,6 @@ dist/web/*
build/*
!build/icons
!build/installer.nsh
!build/entitlements.mac.plist
coverage
node_modules/
npm-debug.log
@@ -17,18 +16,3 @@ docs/dist/
.env.local
.env.*.local
dist_electron/
test.js
.env
scripts/*.yml
#Electron-builder output
/dist_electron
.serena/
.claude/
dist/*
test.js
specs/
.cache/
openspec/
bug*
Trace-*.json
-1
View File
@@ -1 +0,0 @@
pnpm commitlint ${1}
-1
View File
@@ -1 +0,0 @@
pnpm check
-1
View File
@@ -1 +0,0 @@
pnpm test
-1
View File
@@ -1 +0,0 @@
!openspec/
-1
View File
@@ -1 +0,0 @@
22
-1
View File
@@ -1 +0,0 @@
shamefully-hoist=true
+3 -3
View File
@@ -22,7 +22,9 @@ addons:
- icnsutils
#- xvfb
before_install:
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install git-lfs; fi
- mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v1.2.1/git-lfs-$([
"$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-1.2.1.tar.gz
| tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi
install:
#- export DISPLAY=':99.0'
@@ -37,8 +39,6 @@ script:
#- yarn run pack && xvfb-maybe node_modules/.bin/mocha test/e2e
- npm run release
# - yarn run build:docs
before_script:
- git lfs pull
branches:
only:
- master
+24 -30
View File
@@ -1,44 +1,38 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Electron: Main",
"name": "Launch",
"type": "node",
"request": "launch",
"protocol": "inspector",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
"program": "${workspaceRoot}/src/main/index.dev.js",
"env": {
"DEBUG_ENV": "debug"
},
"preLaunchTask": "electron-debug",
"args": [
"--remote-debugging-port=9223",
"./dist"
"stopOnEntry": false,
"args": [],
"cwd": "${workspaceRoot}",
// this points to the electron task runner
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"runtimeArgs": [
"--nolazy"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
]
"sourceMaps": true
},
{
"name": "Electron: Renderer",
"type": "chrome",
"name": "Attach",
"type": "node",
"request": "attach",
"port": 9223,
"urlFilter": "http://localhost:*",
"timeout": 30000,
"webRoot": "${workspaceFolder}/src",
"sourceMapPathOverrides": {
"webpack:///./src/*": "${webRoot}/*"
}
}
],
"compounds": [
{
"name": "Electron: All",
"configurations": [
"Electron: Main",
"Electron: Renderer"
]
"port": 5858,
"sourceMaps": true,
"restart": true,
"outFiles": [],
"localRoot": "${workspaceRoot}",
"protocol": "inspector",
"remoteRoot": null
}
]
}
+2 -8
View File
@@ -1,7 +1,6 @@
{
"eslint.enable": true,
"eslint.alwaysShowStatus": true,
"eslint.format.enable": true,
"eslint.validate": [
"javascript",
"javascriptreact",
@@ -21,11 +20,6 @@
"stylusSupremacy.sortProperties": "grouped",
"stylusSupremacy.quoteChar": "\"",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"prettier.enable": false,
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"source.fixAll.eslint": true
}
}
-27
View File
@@ -1,27 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "electron-debug",
"type": "process",
"command": "./node_modules/.bin/vue-cli-service",
"windows": {
"command": "./node_modules/.bin/vue-cli-service.cmd"
},
"isBackground": true,
"args": ["electron:serve", "--debug"],
"problemMatcher": {
"owner": "custom",
"pattern": {
"regexp": ""
},
"background": {
"beginsPattern": "Starting development server\\.\\.\\.",
"endsPattern": "Not launching electron as debug argument was passed\\."
}
}
}
]
}
-70
View File
@@ -1,70 +0,0 @@
# 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. Only run this when the user explicitly asks/coordinates it.
- 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 check` — run `tsc` + `lint` (run once before finishing a task).
- Before completing a task, always run `pnpm check` and resolve any issues it reports.
- i18n type files are auto-generated by the Vite `i18nTypesPlugin` when `public/i18n/*.yml` changes. Do not add or rely on a manual `gen-i18n` step.
## 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.
- Avoid `as any` in tests as well; build concrete typed stubs (e.g., `IpcMainInvokeEvent`) instead.
- Do not prefix method calls with `void` (e.g. use `store?.refreshPicBeds()` rather than `void store?.refreshPicBeds()`).
- Do not write `void someMethod()` or `void object.method()` anywhere in the codebase. If you need fire-and-forget behavior, use an `async` callback with `await`, or handle errors explicitly with `try/catch` and logging instead of swallowing them.
- Because the renderer uses React Compiler, do not add `useMemo` or `useCallback` by default. Prefer plain values and inline functions unless a specific API requires stable identity or there is a proven performance issue.
- For Zustand store state/actions, prefer reading them directly in the consuming component with `useAppStore` / `useStore` instead of passing them down through unnecessary prop layers. If a child can access the needed store value or action itself, do not thread it through parent props.
- If a child component only needs a store action (for example `providerStoreActions.toggleExpanded`, `settingsStoreActions.setSearchValue`, or `galleryStoreActions.setViewMode`), do not pass that store action through props. Import and use the action directly inside the child component.
- Renderer Zustand stores must follow the project store architecture:
- `src/renderer/store/app-store.ts` is for true global renderer state only.
- Feature/page-local UI state must live under `src/renderer/store/<feature>/` (for example `src/renderer/store/gallery/store.ts`, `src/renderer/store/gallery/actions.ts`), not inside `app-store.ts`.
- Do not add an extra nested `store/` directory like `src/renderer/store/gallery/store/store.ts`.
- Zustand state and actions must be separated:
- Do not define state-mutating actions inside `create()`.
- Keep store files focused on state shape and initial state.
- Put global actions in `src/renderer/store/app-actions.ts`.
- Put feature actions in `src/renderer/store/<feature>/actions.ts`.
- Actions must update state via `useXxxStore.setState(...)`.
- Follow strict IPC boundaries for Zustand actions:
- Pure IPC/service calls without Zustand state changes should call the adapter/service directly from the component or helper, not through a Zustand action.
- Flows that combine IPC/service work with Zustand state updates must live in actions files.
- Keep server state and client state separated:
- TanStack Query should own server state: remote API data, loading/error/stale status, refetching, cache, and request dedupe.
- Zustand should own client/UI state: selected source, selected ids, filters, view mode, panel open state, and other local user intent.
- Connect the two with ids, query params, and local UI state (for example `albumSource`, `typeFilter`, or `searchValue`), but do not mirror query response data back into Zustand.
- If server state invalidates a local UI choice (for example a user becomes non-paid while `albumSource` is cloud), use a small effect/action to correct the local UI state instead of storing the whole server response in Zustand.
- When updating nested Zustand state (especially config-like objects), use `zustand/middleware/immer`; do not reintroduce deep `...state` spread chains for nested updates.
- Components must consume Zustand state through auto-generated selectors (for example `useAppStore.use.appConfig()`), not by destructuring the whole store or writing ad-hoc hook selectors in components.
- Renderer-side shared constants (for example responsive breakpoints, UI timing values, fixed dimensions, thresholds, and repeated literal values used across components) should be centralized in `src/renderer/utils/consts.ts` instead of being hardcoded inline in components. When a new renderer constant may be reused or affects shared behavior, add it there first.
- Renderer-side date/time formatting should use `dayjs` and shared format constants from `src/renderer/utils/consts.ts` (for example `DEFAULT_DATE_TIME_FORMAT`) instead of `Intl.DateTimeFormat` or ad-hoc inline format strings.
- Enum-like object constants declared with `as const` (for example status maps, option maps, and value registries) must use PascalCase names, not camelCase. Prefer names like `PicGoCloudRequestStatusValues`, `SettingsAppearanceValues`, or `AppPlatformValues`.
- If a renderer → main request mutates persisted config/state without using `saveConfig`, call `notifyAppConfigUpdated()` in main to inform renderers.
- Prefer enums over union types for discrete value sets (e.g., encryption methods). Avoid introducing new string literal union types.
- Renderer page/component styles should prefer Tailwind utility classes; avoid adding new Vue `<style>` blocks unless there's no reasonable Tailwind equivalent.
- New renderer ↔ main request/response APIs should be implemented via RPC routes (see `src/main/events/rpc/routes/system.ts`) with `RPCRouter` + `IRPCActionType` rather than adding ad-hoc IPC modules (e.g. `picgoCloudIPC`).
- For request/response semantics in renderer, prefer `invokeRPC` (backed by `ipcMain.handle(RPC_ACTIONS, ...)` in `src/main/events/rpc/index.ts`).
- **AlertDialog async confirm buttons**: Do not use `AlertDialogAction` for confirm buttons that perform async operations (API calls, etc.), because `AlertDialogAction` auto-closes the dialog on click regardless of `event.preventDefault()`. Use a plain `<Button>` instead, manage `open` state manually, and close the dialog only after the async operation completes (success or error). See `src/renderer/components/main/gallery/gallery-delete-dialog.tsx` for reference.
- **Nullish coalescing for optional checks**: Prefer `(value?.field ?? fallback) > 0` over verbose `value !== null && value !== undefined && typeof value.field === 'number' && value.field > 0` chains. Use TypeScript's optional chaining (`?.`) and nullish coalescing (`??`) to keep boolean checks concise.
## 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`. Typed i18n declarations are generated automatically from `public/i18n/en.yml` into `src/universal/types/i18n.d.ts` and `src/renderer/i18n/i18next.d.ts`.
- Any user-facing copy (UI text, error messages, warnings, prompts, tips, notifications, etc.) MUST use i18n keys. Do not hardcode strings in code.
- Renderer: use `$T('KEY')` from `src/renderer/i18n/index.ts`.
- Main process: use `T('KEY')` from `src/main/i18n/index.ts`.
- Add new keys to all locales under `public/i18n/` (at least `en.yml`, `zh-CN.yml`, `zh-TW.yml`). The Vite i18n types plugin will regenerate the shared declarations automatically.
+3 -911
View File
@@ -1,914 +1,3 @@
## :tada: 3.0.2 (2026-08-14)
### :bug: Bug Fixes
* **gui:** protect persisted provider credentials ([77f1ed1](https://github.com/Molunerfinn/PicGo/commit/77f1ed1))
### :zap: Performance Improvements
* **gui:** stop idle GPU churn from uploader status dot ([#1436](https://github.com/Molunerfinn/PicGo/issues/1436)) ([33bbc9a](https://github.com/Molunerfinn/PicGo/commit/33bbc9a))
## :tada: 3.0.1 (2026-07-12)
### :sparkles: Features
* **i18n:** add Japanese (ja) translation ([#1422](https://github.com/Molunerfinn/PicGo/issues/1422)) ([33c9530](https://github.com/Molunerfinn/PicGo/commit/33c9530))
* **i18n:** add Korean (ko) translation ([#1420](https://github.com/Molunerfinn/PicGo/issues/1420)) ([622031f](https://github.com/Molunerfinn/PicGo/commit/622031f))
### :bug: Bug Fixes
* **gui:** mini window ignores startup HIDE mode on Linux ([#1417](https://github.com/Molunerfinn/PicGo/issues/1417)) ([4d92ca1](https://github.com/Molunerfinn/PicGo/commit/4d92ca1))
### :pencil: Documentation
* update docs ([4056447](https://github.com/Molunerfinn/PicGo/commit/4056447))
* update docs ([e26a21c](https://github.com/Molunerfinn/PicGo/commit/e26a21c))
# :tada: 3.0.0 (2026-07-01)
### :sparkles: Features
* v3 ([#1414](https://github.com/Molunerfinn/PicGo/issues/1414)) ([93bbb29](https://github.com/Molunerfinn/PicGo/commit/93bbb29))
### :bug: Bug Fixes
* **gui:** migrate renderer electron access to preload bridge ([#1405](https://github.com/Molunerfinn/PicGo/issues/1405)) ([e1b04b8](https://github.com/Molunerfinn/PicGo/commit/e1b04b8))
* picgo-core link bug ([810cfb1](https://github.com/Molunerfinn/PicGo/commit/810cfb1))
* signed path extract error ([#1407](https://github.com/Molunerfinn/PicGo/issues/1407)) ([d3dcd23](https://github.com/Molunerfinn/PicGo/commit/d3dcd23))
### :pencil: Documentation
* add 2.5.3 docs ([ff85ede](https://github.com/Molunerfinn/PicGo/commit/ff85ede))
* fix `https//` typo in Chinese FAQ [#1](https://github.com/Molunerfinn/PicGo/issues/1) ([#1410](https://github.com/Molunerfinn/PicGo/issues/1410)) ([7bd84a7](https://github.com/Molunerfinn/PicGo/commit/7bd84a7))
* update readme ([01130f2](https://github.com/Molunerfinn/PicGo/commit/01130f2))
## :tada: 2.5.3 (2026-03-06)
### :bug: Bug Fixes
* **plugin:** refresh plugin config dialog state ([#1395](https://github.com/Molunerfinn/PicGo/issues/1395)) ([0e452e1](https://github.com/Molunerfinn/PicGo/commit/0e452e1)), closes [#1394](https://github.com/Molunerfinn/PicGo/issues/1394)
* **update:** correct latest version lookup with beta channel ([#1396](https://github.com/Molunerfinn/PicGo/issues/1396)) ([#1397](https://github.com/Molunerfinn/PicGo/issues/1397)) ([c7ca0de](https://github.com/Molunerfinn/PicGo/commit/c7ca0de))
### :package: Chore
* add oz agent for issues ([#1392](https://github.com/Molunerfinn/PicGo/issues/1392)) ([f05ab63](https://github.com/Molunerfinn/PicGo/commit/f05ab63))
### :pencil: Documentation
* **custom:** update README ([#1390](https://github.com/Molunerfinn/PicGo/issues/1390)) ([5d0f016](https://github.com/Molunerfinn/PicGo/commit/5d0f016))
* update docs ([ac9a00b](https://github.com/Molunerfinn/PicGo/commit/ac9a00b))
* update sponsor ([87d92d5](https://github.com/Molunerfinn/PicGo/commit/87d92d5))
## :tada: 2.5.2 (2026-02-10)
### :bug: Bug Fixes
* s.ee upload error ([a1f76ca](https://github.com/Molunerfinn/PicGo/commit/a1f76ca)), closes [#1385](https://github.com/Molunerfinn/PicGo/issues/1385)
### :pencil: Documentation
* add 2.5.1 docs ([5008a92](https://github.com/Molunerfinn/PicGo/commit/5008a92))
* update readme for s.ee ([d0defee](https://github.com/Molunerfinn/PicGo/commit/d0defee))
## :tada: 2.5.1 (2026-02-10)
### :sparkles: Features
* windows signature ([#1386](https://github.com/Molunerfinn/PicGo/issues/1386)) ([781cf30](https://github.com/Molunerfinn/PicGo/commit/781cf30))
### :bug: Bug Fixes
* **plugin:** plugin search error ([ef07c15](https://github.com/Molunerfinn/PicGo/commit/ef07c15)), closes [#1383](https://github.com/Molunerfinn/PicGo/issues/1383)
### :package: Chore
* **notification:** add notification for windows sign ([#1387](https://github.com/Molunerfinn/PicGo/issues/1387)) ([592f985](https://github.com/Molunerfinn/PicGo/commit/592f985))
* update link ([27464dc](https://github.com/Molunerfinn/PicGo/commit/27464dc))
* update picgo version to support s.ee ([e2e05ae](https://github.com/Molunerfinn/PicGo/commit/e2e05ae)), closes [#1385](https://github.com/Molunerfinn/PicGo/issues/1385)
# :tada: 2.5.0 (2026-01-27)
### :sparkles: Features
* add picgo cloud and config sync ([#1382](https://github.com/Molunerfinn/PicGo/issues/1382)) ([4344772](https://github.com/Molunerfinn/PicGo/commit/4344772)), closes [#1381](https://github.com/Molunerfinn/PicGo/issues/1381)
### :bug: Bug Fixes
* review changes ([5cabbbd](https://github.com/Molunerfinn/PicGo/commit/5cabbbd))
### :package: Chore
* **multi-arch:** add multi-arch build support ([#1379](https://github.com/Molunerfinn/PicGo/issues/1379)) ([678cb71](https://github.com/Molunerfinn/PicGo/commit/678cb71))
### :pencil: Documentation
* **faq:** update faq ([4fb3f2d](https://github.com/Molunerfinn/PicGo/commit/4fb3f2d))
* **release:** update 2.4.3 docs ([5eb3755](https://github.com/Molunerfinn/PicGo/commit/5eb3755))
## :tada: 2.4.3 (2026-01-12)
### :sparkles: Features
* add global url rewrite support ([#1377](https://github.com/Molunerfinn/PicGo/issues/1377)) ([2ed1dd5](https://github.com/Molunerfinn/PicGo/commit/2ed1dd5)), closes [#1255](https://github.com/Molunerfinn/PicGo/issues/1255) [#1281](https://github.com/Molunerfinn/PicGo/issues/1281)
* **upload:** add batch url upload support ([#1376](https://github.com/Molunerfinn/PicGo/issues/1376)) ([c84d542](https://github.com/Molunerfinn/PicGo/commit/c84d542)), closes [#1302](https://github.com/Molunerfinn/PicGo/issues/1302)
### :bug: Bug Fixes
* **gallery:** fix picbed list visible status not sync with gallery ([#1373](https://github.com/Molunerfinn/PicGo/issues/1373)) ([dfe92d4](https://github.com/Molunerfinn/PicGo/commit/dfe92d4)), closes [#1372](https://github.com/Molunerfinn/PicGo/issues/1372)
* **ui:** some ui bugs ([089884b](https://github.com/Molunerfinn/PicGo/commit/089884b))
## :tada: 2.4.2 (2026-01-07)
### :sparkles: Features
* **notification:** refactor notification and add notificationSound settings ([#1370](https://github.com/Molunerfinn/PicGo/issues/1370)) ([2936b19](https://github.com/Molunerfinn/PicGo/commit/2936b19)), closes [#1229](https://github.com/Molunerfinn/PicGo/issues/1229)
### :bug: Bug Fixes
* **tray:** clamp image titles to two lines ([525492b](https://github.com/Molunerfinn/PicGo/commit/525492b))
### :pencil: Documentation
* **2.4.2:** update changelog ([2c9a188](https://github.com/Molunerfinn/PicGo/commit/2c9a188))
## :tada: 2.4.2-beta.0 (2025-12-31)
### :sparkles: Features
* **config:** add copy config \&\& add double confirm before copy \& delete config ([25f86a6](https://github.com/Molunerfinn/PicGo/commit/25f86a6))
### :bug: Bug Fixes
* **icon:** app icon too large in macOS 15.x ([840c33b](https://github.com/Molunerfinn/PicGo/commit/840c33b)), closes [#1367](https://github.com/Molunerfinn/PicGo/issues/1367)
### :package: Chore
* electron builder not publish with build command ([9269f96](https://github.com/Molunerfinn/PicGo/commit/9269f96))
* fix workflow build ([a1d22e5](https://github.com/Molunerfinn/PicGo/commit/a1d22e5))
* **signature:** add signature \& notarization process ([ee6ca02](https://github.com/Molunerfinn/PicGo/commit/ee6ca02))
* update FAQ \&\& plugin filter logic ([f7f5804](https://github.com/Molunerfinn/PicGo/commit/f7f5804))
## :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)
### :sparkles: Features
* add file-name in gallery & add unknown-file-type placholder image ([f0787d3](https://github.com/Molunerfinn/PicGo/commit/f0787d3)), closes [#1050](https://github.com/Molunerfinn/PicGo/issues/1050)
* add file-name in tray-page ([c011654](https://github.com/Molunerfinn/PicGo/commit/c011654)), closes [#1054](https://github.com/Molunerfinn/PicGo/issues/1054)
* support multiple config ([#1016](https://github.com/Molunerfinn/PicGo/issues/1016)) ([9555649](https://github.com/Molunerfinn/PicGo/commit/9555649))
### :bug: Bug Fixes
* handleUploaderConfig should be placed in main/utils ([fc897ce](https://github.com/Molunerfinn/PicGo/commit/fc897ce))
* nsis script ([44f5fbb](https://github.com/Molunerfinn/PicGo/commit/44f5fbb)), closes [#1019](https://github.com/Molunerfinn/PicGo/issues/1019)
### :pencil: Documentation
* update FAQ.md ([#1011](https://github.com/Molunerfinn/PicGo/issues/1011)) ([05998bb](https://github.com/Molunerfinn/PicGo/commit/05998bb))
## :tada: 2.3.1 (2022-11-13)
### :sparkles: Features
* add $extName for paste template ([64e54d0](https://github.com/Molunerfinn/PicGo/commit/64e54d0)), closes [#1000](https://github.com/Molunerfinn/PicGo/issues/1000)
### :bug: Bug Fixes
* upyun options is not required ([167e424](https://github.com/Molunerfinn/PicGo/commit/167e424)), closes [#1002](https://github.com/Molunerfinn/PicGo/issues/1002)
## :tada: 2.3.1-beta.8 (2022-10-30)
### :sparkles: Features
* add remoteNotice ([9317871](https://github.com/Molunerfinn/PicGo/commit/9317871))
* macOS tray icon more clearer ([ecd462f](https://github.com/Molunerfinn/PicGo/commit/ecd462f))
### :bug: Bug Fixes
* url encode bug && copy-paste url encode bug ([4de7a1d](https://github.com/Molunerfinn/PicGo/commit/4de7a1d)), closes [#996](https://github.com/Molunerfinn/PicGo/issues/996)
### :pencil: Documentation
* add PicHoro ([a355bc0](https://github.com/Molunerfinn/PicGo/commit/a355bc0))
## :tada: 2.3.1-beta.7 (2022-10-23)
### :sparkles: Features
* add zh-TW.yml ([#976](https://github.com/Molunerfinn/PicGo/issues/976)) ([72371de](https://github.com/Molunerfinn/PicGo/commit/72371de))
### :bug: Bug Fixes
* change decodeURI -> decodeURIComponent & encode also ([7677f1e](https://github.com/Molunerfinn/PicGo/commit/7677f1e))
* macOS tray icon background color bug ([9791ff2](https://github.com/Molunerfinn/PicGo/commit/9791ff2)), closes [#970](https://github.com/Molunerfinn/PicGo/issues/970)
* some bugs will case picgo-gui-local.log too large ([012a01d](https://github.com/Molunerfinn/PicGo/commit/012a01d)), closes [#995](https://github.com/Molunerfinn/PicGo/issues/995)
* some case will cause picgo-gui-local.log too large ([3c01861](https://github.com/Molunerfinn/PicGo/commit/3c01861))
* some texts in zh-TW ([#978](https://github.com/Molunerfinn/PicGo/issues/978)) ([531bc13](https://github.com/Molunerfinn/PicGo/commit/531bc13))
### :pencil: Documentation
* update readme & FAQ ([d438f82](https://github.com/Molunerfinn/PicGo/commit/d438f82))
## :tada: 2.3.1-beta.6 (2022-09-04)
### :sparkles: Features
* cli support uploading image with url ([e848918](https://github.com/Molunerfinn/PicGo/commit/e848918))
* finish i18n system ([428ffc7](https://github.com/Molunerfinn/PicGo/commit/428ffc7))
### :bug: Bug Fixes
* macos clipboard image can't show on tray page ([20e38f4](https://github.com/Molunerfinn/PicGo/commit/20e38f4)), closes [#961](https://github.com/Molunerfinn/PicGo/issues/961)
* showFileExplorer result bug ([b6b2eea](https://github.com/Molunerfinn/PicGo/commit/b6b2eea))
* windows upload clipboard file with builtin-clipboard not work ([7b50ba7](https://github.com/Molunerfinn/PicGo/commit/7b50ba7))
### :package: Chore
* up issue template ([5f1fb08](https://github.com/Molunerfinn/PicGo/commit/5f1fb08))
* update cos upload url ([86012c0](https://github.com/Molunerfinn/PicGo/commit/86012c0))
## :tada: 2.3.1-beta.5 (2022-08-14)
### :sparkles: Features
* add dist upload to cos & update checkupdate logic ([c926414](https://github.com/Molunerfinn/PicGo/commit/c926414))
* add logFileSizeLimit for log file ([219b367](https://github.com/Molunerfinn/PicGo/commit/219b367)), closes [#935](https://github.com/Molunerfinn/PicGo/issues/935) [#945](https://github.com/Molunerfinn/PicGo/issues/945)
* add server response headers for CORS ([#939](https://github.com/Molunerfinn/PicGo/issues/939)) ([fb69bad](https://github.com/Molunerfinn/PicGo/commit/fb69bad))
* tray-window add open-setting-window button ([83ab3c6](https://github.com/Molunerfinn/PicGo/commit/83ab3c6))
* update tray icon in macOS 11 or 12 ([4ebdc72](https://github.com/Molunerfinn/PicGo/commit/4ebdc72)), closes [#776](https://github.com/Molunerfinn/PicGo/issues/776)
### :bug: Bug Fixes
* build error ([1db84a2](https://github.com/Molunerfinn/PicGo/commit/1db84a2))
* plugin config can't save ([09e4e82](https://github.com/Molunerfinn/PicGo/commit/09e4e82)), closes [#943](https://github.com/Molunerfinn/PicGo/issues/943)
### :package: Chore
* fix github action build scripts ([e6b9d88](https://github.com/Molunerfinn/PicGo/commit/e6b9d88))
* fix version error ([0a9e169](https://github.com/Molunerfinn/PicGo/commit/0a9e169))
* update macOS tray icon ([87161b3](https://github.com/Molunerfinn/PicGo/commit/87161b3))
* update manually action ([351fbda](https://github.com/Molunerfinn/PicGo/commit/351fbda))
### :pencil: Documentation
* update cos links ([3102d7b](https://github.com/Molunerfinn/PicGo/commit/3102d7b))
## :tada: 2.3.1-beta.4 (2022-06-12)
### :bug: Bug Fixes
* **db:** fix some db bugs ([d3bb5ca](https://github.com/Molunerfinn/PicGo/commit/d3bb5ca)), closes [#873](https://github.com/Molunerfinn/PicGo/issues/873) [#806](https://github.com/Molunerfinn/PicGo/issues/806)
* **gallery:** can't copy gallery pics link ([8d861be](https://github.com/Molunerfinn/PicGo/commit/8d861be)), closes [#901](https://github.com/Molunerfinn/PicGo/issues/901)
### :pencil: Documentation
* fix electron_mirror link error ([5d06469](https://github.com/Molunerfinn/PicGo/commit/5d06469)), closes [#849](https://github.com/Molunerfinn/PicGo/issues/849)
* update FAQ ([a79efbf](https://github.com/Molunerfinn/PicGo/commit/a79efbf))
* update readme & FAQ ([746635e](https://github.com/Molunerfinn/PicGo/commit/746635e))
### :package: Chore
* add issue template ([db6c5b8](https://github.com/Molunerfinn/PicGo/commit/db6c5b8))
## :tada: 2.3.1-beta.3 (2022-04-04)
### :sparkles: Features
* add i18n for en ([1936ccf](https://github.com/Molunerfinn/PicGo/commit/1936ccf))
* add tencent-cos options for url ([af291e4](https://github.com/Molunerfinn/PicGo/commit/af291e4)), closes [#862](https://github.com/Molunerfinn/PicGo/issues/862) [#863](https://github.com/Molunerfinn/PicGo/issues/863) [#865](https://github.com/Molunerfinn/PicGo/issues/865) [#524](https://github.com/Molunerfinn/PicGo/issues/524) [#845](https://github.com/Molunerfinn/PicGo/issues/845) [#732](https://github.com/Molunerfinn/PicGo/issues/732)
* add upload-clipboard-image from electron' clipboard ([27628da](https://github.com/Molunerfinn/PicGo/commit/27628da)), closes [#822](https://github.com/Molunerfinn/PicGo/issues/822)
* add wayland support for linux ([f1c8507](https://github.com/Molunerfinn/PicGo/commit/f1c8507))
* click cancel in rename-window will use origin filename now ([04701d4](https://github.com/Molunerfinn/PicGo/commit/04701d4)), closes [#791](https://github.com/Molunerfinn/PicGo/issues/791)
### :bug: Bug Fixes
* fix mini-page can't upload image from dragging browser image ([6bcd019](https://github.com/Molunerfinn/PicGo/commit/6bcd019)), closes [#822](https://github.com/Molunerfinn/PicGo/issues/822)
* mini window not always on top after reopen ([c79a286](https://github.com/Molunerfinn/PicGo/commit/c79a286))
* notification freeze the main-process after uploading with clipboard ([3a50315](https://github.com/Molunerfinn/PicGo/commit/3a50315)), closes [#824](https://github.com/Molunerfinn/PicGo/issues/824)
* picgo.log path error ([6b6ae27](https://github.com/Molunerfinn/PicGo/commit/6b6ae27)), closes [#819](https://github.com/Molunerfinn/PicGo/issues/819)
### :pencil: Documentation
* update readme ([6bcda9b](https://github.com/Molunerfinn/PicGo/commit/6bcda9b)), closes [#849](https://github.com/Molunerfinn/PicGo/issues/849) [#850](https://github.com/Molunerfinn/PicGo/issues/850)
### :package: Chore
* types change ([43d2a8e](https://github.com/Molunerfinn/PicGo/commit/43d2a8e))
* update fix-path ([bcaf255](https://github.com/Molunerfinn/PicGo/commit/bcaf255)), closes [#774](https://github.com/Molunerfinn/PicGo/issues/774)
## :tada: 2.3.1-beta.2 (2022-01-06)
### :bug: Bug Fixes
* electron builder actions bug ([5dd6e72](https://github.com/Molunerfinn/PicGo/commit/5dd6e72))
* linux github actions workflow bug ([5cb8151](https://github.com/Molunerfinn/PicGo/commit/5cb8151))
## :tada: 2.3.1-beta.1 (2022-01-05)
### :package: Chore
* update ci build scripts ([56e814a](https://github.com/Molunerfinn/PicGo/commit/56e814a))
## :tada: 2.3.1-beta.0 (2022-01-05)
### :bug: Bug Fixes
* mini window drag bug ([34b3656](https://github.com/Molunerfinn/PicGo/commit/34b3656))
### :package: Chore
* add mac-arm64 build support ([f2a4197](https://github.com/Molunerfinn/PicGo/commit/f2a4197))
* update electron from v6 -> v16 ([ea20d3b](https://github.com/Molunerfinn/PicGo/commit/ea20d3b))
# :tada: 2.3.0 (2021-09-11)
### :sparkles: Features
* add open devtool option ([75e3edc](https://github.com/Molunerfinn/PicGo/commit/75e3edc))
### :bug: Bug Fixes
* shift key function in gallery page ([5895889](https://github.com/Molunerfinn/PicGo/commit/5895889))
* some bugs ([a676c08](https://github.com/Molunerfinn/PicGo/commit/a676c08)), closes [#722](https://github.com/Molunerfinn/PicGo/issues/722)
* urlEncode bug when copy ([6c6f847](https://github.com/Molunerfinn/PicGo/commit/6c6f847)), closes [#731](https://github.com/Molunerfinn/PicGo/issues/731)
### :pencil: Documentation
* update FAQ ([58420c8](https://github.com/Molunerfinn/PicGo/commit/58420c8))
# :tada: 2.3.0-beta.8 (2021-08-13)
### :bug: Bug Fixes
* settings bug ([20d3cf9](https://github.com/Molunerfinn/PicGo/commit/20d3cf9)), closes [#710](https://github.com/Molunerfinn/PicGo/issues/710)
* upload clipboard images via http should return list ([ae69263](https://github.com/Molunerfinn/PicGo/commit/ae69263)), closes [#721](https://github.com/Molunerfinn/PicGo/issues/721)
# :tada: 2.3.0-beta.7 (2021-08-01)
### :sparkles: Features
* add gallery db ([6ddd660](https://github.com/Molunerfinn/PicGo/commit/6ddd660))
* add win32 support ([1657542](https://github.com/Molunerfinn/PicGo/commit/1657542)), closes [#632](https://github.com/Molunerfinn/PicGo/issues/632)
* finish custom config path ([7030f7a](https://github.com/Molunerfinn/PicGo/commit/7030f7a)), closes [#255](https://github.com/Molunerfinn/PicGo/issues/255)
### :bug: Bug Fixes
* bug of gallery db for plugin ([96a63ea](https://github.com/Molunerfinn/PicGo/commit/96a63ea))
* enable plugin should reload ([49e5f34](https://github.com/Molunerfinn/PicGo/commit/49e5f34)), closes [#659](https://github.com/Molunerfinn/PicGo/issues/659)
* gallery db bug ([f1eb7f4](https://github.com/Molunerfinn/PicGo/commit/f1eb7f4))
* multiple uploading in the same time will cause output conflict ([06b67e5](https://github.com/Molunerfinn/PicGo/commit/06b67e5)), closes [#666](https://github.com/Molunerfinn/PicGo/issues/666)
* multiple uploading in the same time will cause rename failed ([12cecc2](https://github.com/Molunerfinn/PicGo/commit/12cecc2))
* uploader error in linux ([ab762ef](https://github.com/Molunerfinn/PicGo/commit/ab762ef)), closes [#627](https://github.com/Molunerfinn/PicGo/issues/627)
* use uploader first ([92022a6](https://github.com/Molunerfinn/PicGo/commit/92022a6))
* windows ia32 && x64 build options ([bdf523a](https://github.com/Molunerfinn/PicGo/commit/bdf523a))
# :tada: 2.3.0-beta.6 (2021-04-24)
### :sparkles: Features
* add baidu tongji for analytics ([f536391](https://github.com/Molunerfinn/PicGo/commit/f536391))
* add logs for picgo-server ([2d9e9c0](https://github.com/Molunerfinn/PicGo/commit/2d9e9c0)), closes [#627](https://github.com/Molunerfinn/PicGo/issues/627)
* add privacy policy ([992ff35](https://github.com/Molunerfinn/PicGo/commit/992ff35))
### :bug: Bug Fixes
* add plugin install failed notice ([b05139f](https://github.com/Molunerfinn/PicGo/commit/b05139f))
* default picBed using picBed.uploader instead of picBed.current ([0a986c8](https://github.com/Molunerfinn/PicGo/commit/0a986c8))
* dev error with install vue-devtools ([a657c51](https://github.com/Molunerfinn/PicGo/commit/a657c51)), closes [#653](https://github.com/Molunerfinn/PicGo/issues/653) [#658](https://github.com/Molunerfinn/PicGo/issues/658)
* disable plugin need reload app ([a1b70b4](https://github.com/Molunerfinn/PicGo/commit/a1b70b4))
* fix analytics value ([06d40ef](https://github.com/Molunerfinn/PicGo/commit/06d40ef))
* windows cli uploading bug ([321e339](https://github.com/Molunerfinn/PicGo/commit/321e339)), closes [#657](https://github.com/Molunerfinn/PicGo/issues/657)
### :package: Chore
* add main process hot reload ([3fd6e4e](https://github.com/Molunerfinn/PicGo/commit/3fd6e4e))
* fix eslint error notice in indent ([69e1dc8](https://github.com/Molunerfinn/PicGo/commit/69e1dc8))
# :tada: 2.3.0-beta.5 (2021-04-04)
### :sparkles: Features
* add local plugin support && npm registry/proxy support ([f0e1fa1](https://github.com/Molunerfinn/PicGo/commit/f0e1fa1))
* 为Linux系统适配桌面图标栏(Tray) ([#603](https://github.com/Molunerfinn/PicGo/issues/603)) ([0fe3ade](https://github.com/Molunerfinn/PicGo/commit/0fe3ade))
### :bug: Bug Fixes
* default github placeholder ([51d80a6](https://github.com/Molunerfinn/PicGo/commit/51d80a6))
### :package: Chore
* change travis-ci -> GitHub Actions ([064f37d](https://github.com/Molunerfinn/PicGo/commit/064f37d))
# :tada: 2.3.0-beta.4 (2020-12-19)
### :sparkles: Features
* add global value for PicGo get GUI_VERSION & CORE_VERSION ([eab014d](https://github.com/Molunerfinn/PicGo/commit/eab014d))
* **config:** auto configuration backup & fallback to avoid main process crash ([32b8b97](https://github.com/Molunerfinn/PicGo/commit/32b8b97)), closes [#568](https://github.com/Molunerfinn/PicGo/issues/568)
### :bug: Bug Fixes
* disabled plugin won't be shown in plugin page ([33fdb16](https://github.com/Molunerfinn/PicGo/commit/33fdb16))
* shortKeyConfig maybe undefined ([7b5e5ef](https://github.com/Molunerfinn/PicGo/commit/7b5e5ef)), closes [#557](https://github.com/Molunerfinn/PicGo/issues/557)
* url encode before uploading by url ([ce2b5cd](https://github.com/Molunerfinn/PicGo/commit/ce2b5cd)), closes [#581](https://github.com/Molunerfinn/PicGo/issues/581)
### :pencil: Documentation
* add flutter-picgo ([92ff282](https://github.com/Molunerfinn/PicGo/commit/92ff282))
* add gitads ([2d42381](https://github.com/Molunerfinn/PicGo/commit/2d42381))
* rm gitads ([bb90e17](https://github.com/Molunerfinn/PicGo/commit/bb90e17))
* update discussions in doc ([e793599](https://github.com/Molunerfinn/PicGo/commit/e793599))
* update install command by Homebrew ([#599](https://github.com/Molunerfinn/PicGo/issues/599)) ([bd2311b](https://github.com/Molunerfinn/PicGo/commit/bd2311b))
* update README.md ([#555](https://github.com/Molunerfinn/PicGo/issues/555)) ([2151857](https://github.com/Molunerfinn/PicGo/commit/2151857))
# :tada: 2.3.0-beta.3 (2020-07-12)
### :bug: Bug Fixes
* choose default picBed failure ([21d3942](https://github.com/Molunerfinn/PicGo/commit/21d3942)), closes [#537](https://github.com/Molunerfinn/PicGo/issues/537)
* shortkey disabled failure ([4f0809e](https://github.com/Molunerfinn/PicGo/commit/4f0809e)), closes [#534](https://github.com/Molunerfinn/PicGo/issues/534)
# :tada: 2.3.0-beta.2 (2020-07-12)
### :sparkles: Features
* add qrcode for picbeds' config ([7fabc47](https://github.com/Molunerfinn/PicGo/commit/7fabc47))
### :bug: Bug Fixes
* encoding the result of picgo-server ([db71139](https://github.com/Molunerfinn/PicGo/commit/db71139))
* initialize db bugs ([5f87018](https://github.com/Molunerfinn/PicGo/commit/5f87018))
### :pencil: Documentation
* update readme ([1c5880a](https://github.com/Molunerfinn/PicGo/commit/1c5880a))
# :tada: 2.3.0-beta.1 (2020-06-28)
### :bug: Bug Fixes
* auto-copy option && copy style ([b6e3adb](https://github.com/Molunerfinn/PicGo/commit/b6e3adb))
* beta version update bug ([18ad542](https://github.com/Molunerfinn/PicGo/commit/18ad542))
* paste url encoding bug ([59d3eba](https://github.com/Molunerfinn/PicGo/commit/59d3eba)), closes [#454](https://github.com/Molunerfinn/PicGo/issues/454)
### :pencil: Documentation
* update FAQ && ISSUE_TEMPLATE ([2c57a27](https://github.com/Molunerfinn/PicGo/commit/2c57a27))
* update readme ([2adff1e](https://github.com/Molunerfinn/PicGo/commit/2adff1e))
# :tada: 2.3.0-beta.0 (2020-04-30)
### :sparkles: Features
* add autoCopy option for users to use or not ([67e526f](https://github.com/Molunerfinn/PicGo/commit/67e526f))
* add beta-version update support ([ad6acd8](https://github.com/Molunerfinn/PicGo/commit/ad6acd8))
* add smms-v2 support ([3f3ea69](https://github.com/Molunerfinn/PicGo/commit/3f3ea69))
* add uploading image from URL support ([a28c678](https://github.com/Molunerfinn/PicGo/commit/a28c678))
* finish all-select && shift multi-select ([2aeca50](https://github.com/Molunerfinn/PicGo/commit/2aeca50)), closes [#342](https://github.com/Molunerfinn/PicGo/issues/342)
### :bug: Bug Fixes
* confused port number auto increasing when opening a new PicGo app ([cd70a1a](https://github.com/Molunerfinn/PicGo/commit/cd70a1a))
* correct inputbox value && remove listener ([32334e9](https://github.com/Molunerfinn/PicGo/commit/32334e9))
* give a hint when node.js is not installed ([7e86618](https://github.com/Molunerfinn/PicGo/commit/7e86618))
* right-click menu upload fails with PicGo open ([96cdfea](https://github.com/Molunerfinn/PicGo/commit/96cdfea)), closes [#415](https://github.com/Molunerfinn/PicGo/issues/415)
* some win10 upload clipboard image crash ([cc182b0](https://github.com/Molunerfinn/PicGo/commit/cc182b0))
* travis-ci bug ([b357dfb](https://github.com/Molunerfinn/PicGo/commit/b357dfb))
* url uploader bug ([96544f5](https://github.com/Molunerfinn/PicGo/commit/96544f5))
### :pencil: Documentation
* update choco install picgo ([f357557](https://github.com/Molunerfinn/PicGo/commit/f357557))
* update docs ([fd7673e](https://github.com/Molunerfinn/PicGo/commit/fd7673e))
* update readme ([fb014dd](https://github.com/Molunerfinn/PicGo/commit/fb014dd))
### :package: Chore
* update funding url ([024d9cf](https://github.com/Molunerfinn/PicGo/commit/024d9cf))
## :tada: 2.2.2 (2020-01-20)
### :bug: Bug Fixes
* picgo-server upload clipboard file's result bug ([4ebd76f](https://github.com/Molunerfinn/PicGo/commit/4ebd76f))
* releaseUrl may can't get latest version ([ee46ab1](https://github.com/Molunerfinn/PicGo/commit/ee46ab1))
## :tada: 2.2.1 (2020-01-09)
### :sparkles: Features
* add alias for plugin config name ([5a06483](https://github.com/Molunerfinn/PicGo/commit/5a06483))
* add aliyun oss options ([a33f1ad](https://github.com/Molunerfinn/PicGo/commit/a33f1ad)), closes [#347](https://github.com/Molunerfinn/PicGo/issues/347)
* **server:** add http server for uploading images by a http request ([c56d4ef](https://github.com/Molunerfinn/PicGo/commit/c56d4ef))
* add server config settings ([6b57cf7](https://github.com/Molunerfinn/PicGo/commit/6b57cf7))
* only shows visible pic-beds ([9d4d605](https://github.com/Molunerfinn/PicGo/commit/9d4d605)), closes [#310](https://github.com/Molunerfinn/PicGo/issues/310)
### :bug: Bug Fixes
* decrease title-bar z-index when config-form dialog shows ([f2750e1](https://github.com/Molunerfinn/PicGo/commit/f2750e1))
* **website:** website pictures error ([a5b6526](https://github.com/Molunerfinn/PicGo/commit/a5b6526))
* add new tray icon for macOS dark-mode ([c5adf3b](https://github.com/Molunerfinn/PicGo/commit/c5adf3b)), closes [#267](https://github.com/Molunerfinn/PicGo/issues/267)
* beforeOpen handler in windows ([cd30a6c](https://github.com/Molunerfinn/PicGo/commit/cd30a6c))
* busApi event register first && emit later ([e1a0cbb](https://github.com/Molunerfinn/PicGo/commit/e1a0cbb))
* enum type error ([4e3fa28](https://github.com/Molunerfinn/PicGo/commit/4e3fa28))
* handle empty request-body ([81e6acb](https://github.com/Molunerfinn/PicGo/commit/81e6acb))
* launch error in new structrue ([bc8e641](https://github.com/Molunerfinn/PicGo/commit/bc8e641))
* miniWindow minimize bug ([5f2b7c7](https://github.com/Molunerfinn/PicGo/commit/5f2b7c7))
* plugin config-form && default plugin logo ([514fc40](https://github.com/Molunerfinn/PicGo/commit/514fc40))
* release script ([b4f10c6](https://github.com/Molunerfinn/PicGo/commit/b4f10c6))
* removeById handler error ([c4f0a30](https://github.com/Molunerfinn/PicGo/commit/c4f0a30)), closes [#382](https://github.com/Molunerfinn/PicGo/issues/382)
* rename page not work ([29a55ed](https://github.com/Molunerfinn/PicGo/commit/29a55ed))
* save debug mode && PICGO_ENV into config file ([c6ead5b](https://github.com/Molunerfinn/PicGo/commit/c6ead5b))
* server may never start ([73870a5](https://github.com/Molunerfinn/PicGo/commit/73870a5))
* settingPage && miniPage style in windows ([3fd9572](https://github.com/Molunerfinn/PicGo/commit/3fd9572))
### :pencil: Documentation
* add note for windows electron mirror ([46a49ed](https://github.com/Molunerfinn/PicGo/commit/46a49ed))
* remove weibo picbed ([e81b8f4](https://github.com/Molunerfinn/PicGo/commit/e81b8f4))
* update installation by scoop ([91b397d](https://github.com/Molunerfinn/PicGo/commit/91b397d)), closes [#295](https://github.com/Molunerfinn/PicGo/issues/295)
* update readme ([1b3522e](https://github.com/Molunerfinn/PicGo/commit/1b3522e))
* update README ([f491209](https://github.com/Molunerfinn/PicGo/commit/f491209))
* update site ([fe9e19a](https://github.com/Molunerfinn/PicGo/commit/fe9e19a))
# :tada: 2.2.0 (2020-01-01)
@@ -999,3 +88,6 @@
### :package: Chore
* add picgo bump-version ([37f1d34](https://github.com/Molunerfinn/PicGo/commit/37f1d34))
-37
View File
@@ -1,37 +0,0 @@
## 贡献指南
### 安装与启动
1. 使用 [yarn](https://yarnpkg.com/) 安装依赖
```bash
yarn install
```
然后通过
```bash
yarn dev
```
启动项目。
2. 只跟 Electron 主进程相关的代码请在 `src/main` 目录下添加。只跟渲染进程相关的代码请在 `src/renderer` 目录下添加。两个进程都能使用的代码请在 `src/universal` 目录下添加。 **注意**:渲染进程没有 `Node.js` 能力,所有渲染进程需要用到 `Node.js` 模块相关的代码请在 `src/main/events/picgoCoreIPC.ts` 下添加事件进行处理。
3. 所有的跨进程事件名请在 `src/universal/events/constants.ts` 里添加。
4. 所有的全局类型定义请在 `src/universal/types/` 里添加,如果是 `enum`,请在 `src/universal/types/enum.ts` 里添加。
### i18n
1.`public/i18n/` 下面创建一种语言的 `yml` 文件,例如 `zh-Hans.yml`。然后参考 `zh-CN.yml` 或者 `en.yml` 编写语言文件。并注意,PicGo 会通过语言文件中的 `LANG_DISPLAY_LABEL` 向用户展示该语言的名称。
2.`src/universal/i18n/index.ts` 里添加一种默认语言。其中 `label` 就是语言文件中 `LANG_DISPLAY_LABEL` 的值,`value` 是语言文件名。
3. 如果是对已有语言文件进行更新,请在更新完,务必运行一遍 `yarn gen-i18n`,确保能生成正确的语言定义文件。
### 提交代码
1. 请检查代码没有多余的注释、`console.log` 等调试代码。
2. 提交代码前,请执行命令 `git add . && yarn cz`,唤起 PicGo 的[代码提交规范工具](https://github.com/PicGo/bump-version)。通过该工具提交代码。
-37
View File
@@ -1,37 +0,0 @@
## Contribution Guidelines
### Installation and startup
1. Use [yarn](https://yarnpkg.com/) to install dependencies
```bash
yarn install
```
then pass
```bash
yarn dev
```
Startup project.
2. Please add code only related to the main process of Electron in the `src/main` directory. Code only related to the rendering process should be added in the `src/renderer` directory. Add code that can be used by both processes in the `src/universal` directory. **Note**: The rendering process does not have the `Node.js` capability. All rendering processes need to use `Node.js modules` related code, please add events under `src/main/events/picgoCoreIPC.ts` for processing.
3. Please add all cross-process event names in `src/universal/events/constants.ts`.
4. Please add all global type definitions in `src/universal/types/`, if it is `enum`, please add it in `src/universal/types/enum.ts`.
### i18n
1. Create a language `yml` file under `public/i18n/`, for example `zh-Hans.yml`. Then refer to `zh-CN.yml` or `en.yml` to write language files. Also note that PicGo will display the name of the language to the user via `LANG_DISPLAY_LABEL` in the language file.
2. Add a default language to `src/universal/i18n/index.ts`. where `label` is the value of `LANG_DISPLAY_LABEL` in the language file, and `value` is the name of the language file.
3. If you are updating an existing language file, be sure to run `yarn gen-i18n` after the update to ensure that the correct language definition file can be generated.
### Submit code
1. Please check that the code has no extra comments, `console.log` and other debugging code.
2. Before submitting the code, please execute the command `git add . && yarn cz` to invoke PicGo's [Code Submission Specification Tool](https://github.com/PicGo/bump-version). Submit code through this tool.
+25 -151
View File
@@ -1,184 +1,58 @@
## Frequently Asked Questions / 常见问题
## 常见问题
> While using PicGo you may run into various issues. Many of them have already been asked and resolved, so please check the [documentation](https://docs.picgo.app/gui/guide/getting-started), this FAQ, and closed [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed) first — you will likely find the answer there.
>
> 在使用 PicGo 期间你会遇到很多问题,不过很多问题其实之前就有人提问过,也被解决,所以你可以先看看 [使用文档](https://docs.picgo.app/gui/guide/getting-started),这份 FAQ,以及那些被关闭的 [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed),应该能找到答案。
> 在使用PicGo期间你会遇到很多问题,不过很多问题其实之前就有人提问过,也被解决,所以你可以先看看[使用文档](https://github.com/Molunerfinn/PicGo/wiki/%E8%AF%A6%E7%BB%86%E7%AA%97%E5%8F%A3%E7%9A%84%E4%BD%BF%E7%94%A8#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%BF%AB%E6%8D%B7%E9%94%AE),这份FAQ,以及那些被关闭的[issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed),应该能找到答案。
## 1. Qiniu image host: upload succeeds but images dont show in Album, or the URL has no `http://` prefix / 七牛图床上传图片成功后,相册里无法显示或图片无`http://`前缀
## 1.七牛图床上传图片成功后无法显示或图片无`http://`前缀
This is usually because the `Set URL` (access URL) in your Qiniu image host configuration does not include the `http://` or `https://` scheme.
Reference: [issue#79](https://github.com/Molunerfinn/PicGo/issues/79)
通常是你的七牛图床配置里的`设定访问网址`没有加上`http://`或者`https://`头。
通常是你的七牛图床配置里的`设定访问网址`没有加上`http://`或者`https//`头。
参考:[issue#79](https://github.com/Molunerfinn/PicGo/issues/79)
## 2. Can PicGo delete images on the remote image host after upload? / 能否支持图床远端同步删除
## 2.能否支持图床远端同步删除
Not at the moment. Some image hosts (e.g. Weibo image host, SM.MS, Imgur, etc.) dont provide a backend management API, so PicGo does not support remote deletion for the sake of a consistent architecture.
不能。有些图床(比如微博图床、SM.MSImgur等)不支持后台管理,为了架构统一不支持远端删除。
暂时不支持。有些图床(比如微博图床、SM.MS、Imgur 等)不支持后台管理,为了架构统一不支持远端删除。
## 3.能否支持上传视频文件
## 3. Can PicGo upload video files? / 能否支持上传视频文件
目前不能。2.0版本出来后将会支持插件系统,届时如果有人开发了相应的插件理论可以支持任意文件上传。
Some image hosts support uploading video files, but not all. Please follow the capabilities of the image host (and/or the plugin) you are actually using.
目前部分图床支持上传视频文件,但并非所有图床都支持,请以实际使用的图床以及插件为准。
## 4. Weibo image host: uploaded images dont preview / 微博图床上传之后无法显示预览图
This is usually caused by having a global proxy enabled.
Reference: [issue36](https://github.com/Molunerfinn/PicGo/issues/36)
## 4.微博图床上传之后无法显示预览图
通常是挂了全局代理导致的。
参考:[issue36](https://github.com/Molunerfinn/PicGo/issues/36)
## 5. Can you add support for an image host? / 能否支持某某某图床
## 5.能否支持某某某图床
As of v1.6, PicGo supports the following built-in image hosts:
- `Weibo image host` v1.0
- `Qiniu image host` v1.0
- `Tencent Cloud COS v4/v5` v1.1 & v1.5.0
- `Upyun` v1.2.0
- `GitHub` v1.5.0
- `SM.MS` v1.5.1
- `Alibaba Cloud OSS` v1.6.0
- `Imgur` v1.6.0
PicGo itself will not add support for additional third-party image hosts as built-in features. If you need other image hosts, please refer to existing third-party [plugins](https://github.com/PicGo/Awesome-PicGo). If the one you need doesnt exist yet, youre welcome to develop a plugin and share it with the community.
截止 v1.6,PicGo 支持了如下图床:
截止v1.6PicGo支持了如下图床:
- `微博图床` v1.0
- `七牛图床` v1.0
- `腾讯云 COS v4\v5 版本` v1.1 & v1.5.0
- `腾讯云COS v4\v5版本` v1.1 & v1.5.0
- `又拍云` v1.2.0
- `GitHub` v1.5.0
- `SM.MS` v1.5.1
- `阿里云 OSS` v1.6.0
- `阿里云OSS` v1.6.0
- `Imgur` v1.6.0
所以本体内将不会再支持其他第三方图床。需要其他图床支持可以参考目前已有的三方 [插件](https://github.com/PicGo/Awesome-PicGo),如果还是没有你所需要的图床欢迎开发一个插件供大家使用
所以本体内将不会再支持其他图床。待2.0版本出来支持插件系统后可以由其他开发者开发相应的图床插件用以支持其他图床
## 6. GitHub image host uploads sometimes succeed and sometimes fail / GitHub 图床有时能上传,有时上传失败
## 6.一个图床设置多个信息
1. The GitHub image host does not allow uploading files with the same name. If you upload a duplicate filename, you will get an error. Enable `Timestamp Rename` to avoid name collisions.
2. Due to GitHub network conditions (and the Great Firewall in mainland China), uploads may sometimes succeed and sometimes fail — there is no universal fix. For stability, consider using a paid cloud storage service such as Alibaba Cloud or Tencent Cloud; they are usually inexpensive.
不能。因为目前的架构只支持一个图床一份信息。
1. GitHub 图床不支持上传同名文件,如果有同名文件上传,会报错。建议开启 `时间戳重命名` 避免同名文件。
2. GitHub 服务器和国内 GFW 的问题会导致有时上传成功,有时上传失败,无解。想要稳定请使用付费云存储,如阿里云、腾讯云等,价格也不会贵。
## 7.v2.0版本有什么新功能
## 7. Cant open PicGos main window on macOS / Mac 上无法打开 PicGo 的主窗口界面
在不改变PicGo现有使用界面和使用习惯的基础上,将会重构整个底层`传输->转换->上传`的架构,使其能够支持插件系统,方便其他开发者基于PicGo的底层进行二次开发。
On macOS, PicGo is a menu bar app, so it wont show an icon in the Dock by default. To open the main window, right-click (or two-finger click) the PicGo menu bar icon and choose “Open Main Window”.
理论上有了插件系统之后,其他开发者能够支持如下功能:
Starting from v2.4.1, PicGo lets you hide the Dock icon (`showDockIcon`) and the menu bar icon (`showMenubarIcon`) separately. If you turn both off (set both to `false`), you wont be able to find the UI via either the Dock or the menu bar.
1. 能够实现上传前图片压缩
2. 能够通过图片url实现上传图片 -> 更深入地可以批量地实现图床的迁移
3. 能够支持其他图床甚至是自己的图床
How to recover manually:
等等。
1. Locate and edit PicGos config file `data.json`.
- If you can still open the settings page: PicGo Settings -> “Open Config File”.
- If you cant find the UI: the default location is usually `~/Library/Application Support/PicGo/data.json` (if you configured a custom path, follow `configPath`).
2. Set either field below to `true` (its recommended to keep at least one of them `true`). Do not modify other fields.
3. Save and restart PicGo.
届时底层架构将会独立出来成为一个独立的npm包,能够直接在命令行里调用。
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。
## 8. Upload failed, or server returned an error / 上传失败,或者是服务器出错
1. PicGos built-in image hosts are tested; upload errors are usually not caused by PicGo itself. If you are using the GitHub image host, see FAQ #6.
2. Check PicGo logs (PicGo Settings -> Log File -> Open) and look for key information in `[PicGo Error]`.
1. Search the error message first — you can often find the root cause via search engines without opening an issue.
2. If you see `401`, `403`, or other `40X` status codes, it almost certainly means your configuration is wrong. Double-check for typos, trailing spaces, etc.
3. If you see `HttpError`, `RequestError`, `socket hang up`, etc., that indicates a network issue. Please check your network, proxy, and DNS settings.
3. Upload failures caused by network issues are often due to incorrect proxy settings. If you enabled a system proxy, its recommended to also configure the corresponding HTTP proxy in PicGo. See [#912](https://github.com/Molunerfinn/PicGo/issues/912)
1. PicGo 自带的图床都经过测试,上传出错一般都不是 PicGo 自身的原因。如果你用的是 GitHub 图床请参考上面的第 6 点。
2. 检查 PicGo 的日志(报错日志可以在 PicGo 设置 -> 设置日志文件 -> 点击打开 后找到),看看 `[PicGo Error]` 的报错信息里有什么关键信息
1. 先自行搜索 error 里的报错信息,往往你能百度或者谷歌出问题原因,不必开 issue。
2. 如果有带有 `401``403``40X` 状态码字样的,不用怀疑,就是你配置写错了,仔细检查配置,看看是否多了空格之类的。
3. 如果带有 `HttpError``RequestError``socket hang up` 等字样的说明这是网络问题,我无法帮你解决网络问题,请检查你自己的网络,是否有代理,DNS 设置是否正常等。
3. 通常网络问题引起的上传失败都是因为代理设置不当导致的。如果开启了系统代理,建议同时也在 PicGo 的代理设置中设置对应的HTTP代理。参考 [#912](https://github.com/Molunerfinn/PicGo/issues/912)
## 9. Installed on macOS but there is no main UI window / macOS版本安装完之后没有主界面
Find the PicGo icon in the macOS menu bar, then right-click (two-finger click on trackpad) to open the menu and choose “Open Main Window”.
请找到PicGo在顶部栏的图标,然后右键(触摸板双指点按,或者鼠标右键),即可找到「打开详细窗口」的菜单。
## 10. Album suddenly cant show images, or doesnt refresh after upload, or Typora + PicGo upload succeeds but doesnt write back / 相册突然无法显示图片 或者 上传后相册不更新 或者 使用Typora+PicGo上传图片成功但是没有写回Typora
This may be caused by a corrupted album database. Locate `picgo.db` under your PicGo config directory, delete it (backup first if needed), then restart PicGo.
Also check the log file for errors and open an issue if necessary. Versions >= 2.3.0 have addressed issues caused by a corrupted `picgo.db`, so upgrading is recommended.
这个原因可能是相册存储文件损坏导致的。可以找到 PicGo 配置文件所在路径下的 `picgo.db` ,将其删掉(删掉前建议备份一遍),再重启 PicGo 试试。
注意同时看看日志文件里有没有什么error,必要时可以提issue。2.3.0以上的版本已经解决因为 `picgo.db` 损坏导致的上述问题,建议更新版本。
## 11. Gitee-related issues / Gitee相关问题
If you run into upload issues with the Gitee image host, PicGo cannot help because PicGo does not provide an official Gitee uploader. Please open an issue in the repository of the Gitee plugin you are using.
如果在使用 Gitee 图床的时候遇到上传的问题,由于 PicGo 并没有官方提供 Gitee 上传服务,无法帮你解决,请去你所使用的 Gitee 插件仓库发相关的issue。
## 12. On macOS, PicGo shows “App is damaged”, or it doesnt respond after installation / macOS系统安装完PicGo显示「文件已损坏」或者安装完打开没有反应
Please try the version >= v2.4.2, which has fixed this issue.
请尝试使用 v2.4.2 及以上版本,已经修复该问题。
## 13. Are third-party plugins claiming to be “PicGo Official image host” trustworthy? / 所谓「PicGo 官方图床」的第三方插件是否可信
No. Any third-party plugin that claims to be a “PicGo Official image host” (including, but not limited to, www.picgo.net) is not an official PicGo image host or service. Please do not trust such claims.
An official PicGo image host (if any) would be built into PicGo out of the box — it would not require you to download and install a “third-party plugin”, and it would not direct you to an unknown website to purchase or configure a so-called “official image host”. If you choose to use third-party image hosts, please prefer community plugins from reputable sources and assess their safety yourself.
不可信。所有打着「PicGo 官方图床」旗号的第三方插件(包括不限于 www.picgo.net 等)都不是 PicGo 官方提供的图床或服务,请勿轻信。
PicGo 不会以“第三方插件”的形式要求你另外下载安装所谓的 PicGo 官方图床。如果 PicGo 真的做了官方图床,一定是开箱即用的内置在本体里的。如果你需要使用第三方图床,请优先参考 PicGo 官方维护的插件集合与社区仓库,并自行甄别来源与安全性。
## 14. SM.MS migrated to S.EE: how should I update my config? / SM.MS 迁移到 S.EE 后,配置应该怎么改?
SM.MS uploader has migrated to **S.EE** and changed from the original free plan to a paid service.
To continue uploading normally:
1. Get your API token from [https://s.ee/user/dashboard/](https://s.ee/user/dashboard/).
2. Check your `picBed.smms.backupDomain`:
- if it is an old domain such as `sm.ms` or `smms.app`, remove this field, or
- change it to `s.ee`.
SM.MS 上传器已迁移到 **S.EE**,并且服务已从原本免费改为收费。
如需继续正常上传:
1. 到 [https://s.ee/user/dashboard/](https://s.ee/user/dashboard/) 获取 API Token。
2. 检查你的 `picBed.smms.backupDomain`
- 如果是旧域名(如 `sm.ms``smms.app`),请删除该字段,或
- 改为 `s.ee`
目前开发进度可以参考[PicGo-Core](https://github.com/Molunerfinn/PicGo-Core)
+69 -132
View File
@@ -1,187 +1,124 @@
<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>
<div align="center" markdown="1">
<sup>Sponsored by:</sup>
<br>
<a href="https://console.neon.tech/app/?promo=PicGo">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://neon.com/brand/neon-logo-dark-color.svg">
<source media="(prefers-color-scheme: light)" srcset="https://neon.com/brand/neon-logo-light-color.svg">
<img alt="Neon sponsorship" width="400" src="https://neon.com/brand/neon-logo-dark-color.svg">
</picture>
</a>
### [Fast Postgres Databases for Teams and Agents](https://console.neon.tech/app/?promo=PicGo)
</div>
---
[中文](./README_zh-CN.md) | **English**
<div align="center">
<img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/New%20LOGO-150.png" alt="PicGo Logo">
<img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/New%20LOGO-150.png" alt="">
<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>
<a href="https://atomgit.com/Molunerfinn/PicGo">
<img src="https://atomgit.com/Molunerfinn/PicGo/star/badge.svg" alt="">
</a>
</p>
<blockquote>图片上传+管理新体验 </blockquote>
<a href="https://github.com/feross/standard">
<img src="https://img.shields.io/badge/code%20style-standard-green.svg?style=flat-square" alt="">
</a>
<a href="https://travis-ci.org/Molunerfinn/PicGo/builds">
<img src="https://img.shields.io/travis/Molunerfinn/PicGo.svg?style=flat-square" alt="">
</a>
<a href="https://github.com/Molunerfinn/PicGo/releases">
<img src="https://img.shields.io/github/downloads/Molunerfinn/PicGo/total.svg?style=flat-square" alt="">
</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>
</div>
## 📖 Overview
## 应用说明
**PicGo aims to make image uploading a seamless part of your creative workflow.**
**PicGo 在上传图片之后自动会将图片链接复制到你的剪贴板里,可选 5 种复制的链接格式。**
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.
PicGo 目前支持了
### Supported Image hosts
- ~~`微博图床` v1.0~~ **微博图床从 2019 年 4 月开始进行防盗链,不建议继续使用**
- `七牛图床` v1.0
- `腾讯云 COS v4\v5版本` v1.1 & v1.5.0
- `又拍云` v1.2.0
- `GitHub` v1.5.0
- `SM.MS` v1.5.1
- `阿里云 OSS` v1.6.0
- `Imgur` v1.6.0
PicGo supports mainstream Image hosts out of the box, and can be extended indefinitely through its plugin system:
**本体不再增加默认的图床支持。你可以自行开发第三方图床插件。详见 [PicGo-Core](https://picgo.github.io/PicGo-Core-Doc/)**
- **China cloud vendors**: Qiniu, Tencent Cloud COS, UPYUN, Alibaba Cloud OSS
- **International / open platforms**: GitHub, SM.MS(S.EE), Imgur
- **More options via plugins**: AWS S3, Cloudflare R2, MinIO, and more
第三方插件以及使用了 PicGo 底层的应用可以在 [Awesome-PicGo](https://github.com/PicGo/Awesome-PicGo) 找到。欢迎贡献!
> **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://docs.picgo.app/core/).
PicGo 支持 macOS、Windows 64位(>= v1.3.1),Linux>= v1.6.0)。
## ✨ Key Features
支持快捷键`command+shift+p`macOS)或者`control+shift+p`Windows\Linux)用以支持快捷上传剪贴板里的图片(第一张)。
PicGo 支持自定义快捷键,使用方法见[配置手册](https://picgo.github.io/PicGo-Doc/zh/guide/config.html)。
PicGo is built around a fast, low-friction image upload experience:
开发进度可以查看 [Projects](https://github.com/Molunerfinn/PicGo/projects),会同步更新开发进度。
### ⚡ 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...
**如果第一次使用,请参考应用使用[快速上手](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)。**
### 🚀 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+U` (macOS) / `Ctrl+Shift+U` (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)
点击此处下载[应用](https://github.com/Molunerfinn/PicGo/releases)。
### 🛠 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://docs.picgo.app/core/).
macOS 用户请下载最新版本的 `dmg` 文件,Windows 用户请下载最新版本的 `exe` 文件,Linux用户请下载 `AppImage` 文件。
> Theres more to discover—development progress is tracked in [Projects](https://github.com/Molunerfinn/PicGo/projects).
**如果你是 Arch 类的 Linux 用户,可以直接通过 `aurman -S picgo-appimage` 来安装 PicGo。感谢 @houbaron 的贡献!**
If youre new to PicGo, start with the [User Guide](https://docs.picgo.app/gui/guide/getting-started). 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).
**如果你是 macOS 用户,可以使用 `brew cask` 来安装 PicGo: `brew cask install picgo`。感谢 @womeimingzi11 的贡献!**
## Download & Install
**如果你是 Windows 用户,还可以使用 [Scoop](https://scoop.sh/) 来安装 PicGo: `scoop bucket add helbing https://github.com/helbing/scoop-bucket` & `scoop install picgo`。 感谢 @helbing 的贡献!**
| 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 |
| [Nix](https://search.nixos.org/packages?channel=unstable&query=picgo&show=picgo) | `nix-shell -p picgo` | Nix/NixOS | Thanks to @qrzbing |
## 应用截图
## Screenshots
![](https://raw.githubusercontent.com/Molunerfinn/PicGo/dev/assets/picgo-v3.gif)
![](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
## 开发说明
> Currently tested on macOS and Windows only. Linux has not been fully tested.
> 目前仅针对 Mac、Windows。Linux 平台并未测试。
If you want to learn, contribute, modify, or build PicGo yourself:
如果你想要学习、开发、修改或自行构建 PicGo,可以依照下面的指示:
> For an Electron-vue learning series, see: [Electron-vue development](https://molunerfinn.com/tags/Electron-vue/)
> 如果想学习 Electron-vue 的开发,可以查看我写的系列教程——[Electron-vue 开发实战](https://molunerfinn.com/tags/Electron-vue/)
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).
1. 你需要有 Node、Git环境,了解 npm 的相关知识。
2. `git clone https://github.com/Molunerfinn/PicGo.git` 并进入项目
3. `npm install` 下载依赖
4. Mac 需要有 Xcode 环境,Windows 需要有 VS 环境。
### Development mode
### 开发模式
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:
输入 `npm run electron:serve` 进入开发模式,开发模式具有热重载特性。不过需要注意的是,开发模式不稳定,会有进程崩溃的情况。此时需要:
```bash
ctrl+c # stop dev mode
pnpm run dev # restart
ctrl+c # 退出开发模式
npm run dev # 重新进入开发模式
```
> 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`.
**Note**: If your network is unstable, `electron-builder` may fail to download Electron binaries. You can set an alternative mirror before building:
**注意**:如果你的网络环境不太好,可能会出现 `electron-builder` 下载 `electron` 二进制文件失败的情况。这个时候需要在 `npm run build` 之前指定一下 `electron` 的源为国内源:
```bash
export ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
# On Windows: set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ (no quotes)
pnpm run build
export ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/"
# Windows 上,则可以使用 set ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/ (无需引号)
npm run build
```
Electron binaries are stored under `~/.electron/`. If you need to refresh them, delete that directory and rebuild.
只需第一次构建的时候指定一下国内源即可。后续构建不需要特地指定。二进制文件下载在 `~/.electron/` 目录下。如果想要更新 `electron` 构建版本,可以删除 `~/.electron/` 目录,然后重新运行上一步,让 `electron-builder `去下载最新的 `electron` 二进制文件。
## Related Projects
## 其他相关
- [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).
- [vs-picgo](https://github.com/Spades-S/vs-picgo)PicGo VS Code 版。
## Sponsorship
## 赞助
If you like PicGo and it helps your workflow, feel free to buy me a coffee.
如果你喜欢 PicGo 并且它对你确实有帮助,欢迎给我打赏一杯咖啡哈~
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)
Copyright (c) 2017 - Now Molunerfinn
Copyright (c) 2017 - 2019 Molunerfinn
-187
View File
@@ -1,187 +0,0 @@
<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>
<div align="center" markdown="1">
<sup>Sponsored by:</sup>
<br>
<a href="https://console.neon.tech/app/?promo=PicGo">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://neon.com/brand/neon-logo-dark-color.svg">
<source media="(prefers-color-scheme: light)" srcset="https://neon.com/brand/neon-logo-light-color.svg">
<img alt="Neon sponsorship" width="400" src="https://neon.com/brand/neon-logo-dark-color.svg">
</picture>
</a>
### [Fast Postgres Databases for Teams and Agents](https://console.neon.tech/app/?promo=PicGo)
</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>
<a href="https://atomgit.com/Molunerfinn/PicGo">
<img src="https://atomgit.com/Molunerfinn/PicGo/star/badge.svg" alt="">
</a>
</p>
</div>
## 📖 应用概述
**PicGo 致力于将图片上传无缝集成到你的创作工作流中。**
无论你是写博客、做笔记还是编写开发文档,PicGo 都能帮你一键上传图片并自动复制链接,让你专注于内容创作本身,而不是繁琐的上传步骤。
### 核心支持
PicGo 原生支持主流图床平台,并可通过插件系统无限扩展:
- **国内云厂商**:七牛云、腾讯云 COS、又拍云、阿里云 OSS
- **国际/开源平台**GitHub、SM.MS(S.EE)、Imgur
- **更多支持**:通过插件支持 AWS S3、Cloudflare R2、MinIO 等第三方图床
> **注意**:PicGo 本体不再增加默认的第三方图床支持。你可以自行开发第三方图床插件。详见 [PicGo-Core](https://docs.picgo.app/core/)。
## ✨ 特色功能
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+U` (macOS) / `Ctrl+Shift+U` (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://docs.picgo.app/core/)。
> 更多功能等你自己去发现,开发进度可以查看 [Projects](https://github.com/Molunerfinn/PicGo/projects)。
**如果第一次使用,请参考应用 [使用文档](https://docs.picgo.app/gui/guide/getting-started)。遇到问题了还可以看看 [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 的贡献 |
| [Nix](https://search.nixos.org/packages?channel=unstable&query=picgo&show=picgo) | `nix-shell -p picgo` | Nix/NixOS | 感谢 @qrzbing 的贡献 |
## 应用截图
![](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
+31
View File
@@ -0,0 +1,31 @@
# 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 12 x64
- git reset --hard HEAD
- yarn
- node --version
build_script:
#- yarn test
- npm run release
test: off
Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 KiB

+1 -2
View File
@@ -1,6 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: ['@babel/plugin-proposal-optional-chaining']
]
}
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- 允许 JIT (Electron 必须) -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!-- 允许加载未签名的动态库 (插件、原生模块必须) -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- 允许执行内存中可写的页 (部分 Electron 版本需要) -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.
-129
View File
@@ -1,129 +0,0 @@
# 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
@@ -1,31 +0,0 @@
# 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` 构建产物
-31
View File
@@ -1,31 +0,0 @@
# PicGo 2.4.2 Changelog
## Features
- New: macOS version with signature && notarization. No more manual handling
<img width="260" height="260" alt="image" src="https://github.com/user-attachments/assets/dc1b7114-1f10-40e3-b45e-e3b330712399" />
- Add duplicate button for image host config item. And add a double confirm dialog for `duplicate` and `delete` config item action
<img width="800" height="450" alt="image" src="https://github.com/user-attachments/assets/238881e3-e993-4cb7-9699-fd2f7441ce02" />
- Add: Refactor notifications and add `notificationSound` setting (#1370)
## Bug Fixes
- Fix: An issue that the app icon will be too big under macOS 26. See #1367 for more details
- Fix: Clamp tray image titles to two lines or the style will be broken
## Other
- Refactor: Remove config store (#1371)
----------
## Features
- 新增:macOS 版本签名+公证,现在不再需要手动放行,可以直接下载安装使用
<img width="260" height="260" alt="image" src="https://github.com/user-attachments/assets/dc1b7114-1f10-40e3-b45e-e3b330712399" />
- 新增:图床配置界面新增复制按钮,可以一键复制已有配置,方便进行多样化操作。同时复制和删除均增加二次确认弹窗
<img width="800" height="450" alt="image" src="https://github.com/user-attachments/assets/238881e3-e993-4cb7-9699-fd2f7441ce02" />
- 新增:重构通知并新增 `notificationSound` 设置(#1370
## Bug Fixes
- 修复:在 macOS 26 版本下 app 图标过大的问题。参考 #1367
- 修复:托盘图片标题最多显示两行否则样式会被破坏的问题
## Other
- 重构:移除 config store#1371
-17
View File
@@ -1,17 +0,0 @@
# PicGo 2.4.3 Changelog
## Features
- Add: Batch URL upload support (#1376). See #1302 for details
- Add: [Global URL rewrite](https://docs.picgo.app/core/guide/config.html#settings) support (#1377). See #1281 for details
- Refactor: Gallery image URL can not only rewrite the host but also the whole link. See #1255 for details
----------
## Features
- 新增:支持批量 URL 上传(#1376),参考 #1302
- 新增:支持[全局 URL 重写](https://docs.picgo.app/zh/core/guide/config.html#settings)#1377),参考 #1281
- 重构:相册页的图片 URL 可以批量修改,不仅仅只是修改 HOST,参考 #1255
## Bug Fixes
- 修复:相册页图床列表显示状态与相册不同步的问题(#1373),参考 #1372
- 修复:一些 UI 问题
-21
View File
@@ -1,21 +0,0 @@
# PicGo 2.5.1 Changelog
## Features
- Update: Bump `picgo` dependency to `^2.0.1` to support legacy `sm.ms` migrate to `s.ee` (#1385)
## Bug Fixes
- Fix: Plugin search no longer throws when an npm package description is empty (#1383)
## Other
- Update: Refresh the documentation link in the GitHub bug report template
----------
## Features
- 更新:将 `picgo` 依赖升级到 `^2.0.1` 以支持 `s.ee`。参考 #1385
## Bug Fixes
- 修复:插件搜索在 npm 包描述为空时会报错的问题。参考 #1383
## Other
- 更新:更新 GitHub 问题模板中的文档链接
-9
View File
@@ -1,9 +0,0 @@
# PicGo 2.5.2 Changelog
## Bug Fixes
- Fix: Resolve `s.ee` compatibility issues that could report upload failure even when the upload actually succeeded (#1385)
----------
## Bug Fixes
- 修复:解决 `s.ee` 兼容性问题,上传实际成功时仍可能提示上传失败(#1385
-17
View File
@@ -1,17 +0,0 @@
# PicGo 2.5.3 Changelog
## Bug Fixes
- Fix: Plugin configuration dialogs now refresh correctly when switching between plugins, avoiding stale form data from the previous plugin (#1394)
- Fix: Latest version checking now compares stable and beta releases correctly when beta updates are enabled, avoiding incorrect update prompts (#1396)
## Other
- Update: README now includes a Nix/NixOS setup note, thanks @qrzbing for the contribution! (#1390)
----------
## Bug Fixes
- 修复:在不同插件之间切换时,插件配置弹窗会正确刷新,避免沿用上一个插件的旧表单数据(#1394
- 修复:开启 beta 更新通道后,最新版本检查会正确比较正式版与 beta 版,避免更新提示不准确(#1396
## Other
- 更新:README 补充了 Nix/NixOS 相关说明,感谢 @qrzbing 的贡献!(#1390
-53
View File
@@ -1,53 +0,0 @@
# 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
- Exception: Remove holiday greetings (e.g., "Happy New Year!", "新年快乐!") from the generated changelog. These can stay in GitHub release notes but should not appear in `changelog/X.Y.Z.md`
## Structure
- Target file: `changelog/X.Y.Z.md` (replace with the series version)
- Use these top-level sections when they contain items: `## Features`, `## Bug Fixes`, `## Other`
- Omit any empty section title entirely (do not output a heading with no bullets)
- 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
- If you need to add items from git commit logs (for the same release series), do not paste raw commit messages. Normalize them to the existing changelog style (e.g., use `Add:`, `Fix:`, `Refactor:` prefixes without emoji)
- 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
- If one section has no items after merging, skip that section heading in both EN and ZH halves
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, reusing only the section headings that appeared in English (same heading set/order; no “(Chinese)” suffix)
8) Save the result to `changelog/X.Y.Z.md`
## Quick checklist
- [ ] All non-empty Features items present with images kept
- [ ] All non-empty Bug Fixes items present
- [ ] All non-empty Other notes present
- [ ] No beta headers
- [ ] No download links
- [ ] No empty section headings
- [ ] Chronological ordering preserved
- [ ] Chinese translation present with matching bullets/images after `----------`
-24
View File
@@ -1,24 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-vega",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/renderer/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
+216
View File
@@ -0,0 +1,216 @@
<template lang='pug'>
#app(v-cloak)
#header
.mask
img.logo(src="~icons/256x256.png", alt="PicGo")
h1.title PicGo
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/')") 查看文档
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更强大
#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")
.row.ex-width.display-list
.display-list__item(v-for="(item, index) in itemList" :key="index" :class="{ 'o-item': index % 2 !== 0 }")
.col-xs-10.col-xs-offset-1.col-md-7.col-md-offset-0
img(:src="item.url")
.col-xs-10.col-xs-offset-1.col-md-5.col-md-offset-0.display-list__content
.display-list__title {{ item.title }}
.display-list__desc {{ item.desc }}
.row.ex-width.info
.col-xs-10.col-xs-offset-1
| &copy;2017 - {{ year }} #[a(href="https://github.com/Molunerfinn" target="_blank") Molunerfinn]
</template>
<script>
export default {
name: '',
data () {
return {
version: '',
year: new Date().getFullYear(),
itemList: [
{
url: 'https://cdn.jsdelivr.net/gh/Molunerfinn/test/picgo-site/second.png',
title: '精致设计',
desc: 'macOS系统下,支持拖拽至menubar图标实现上传。menubar app 窗口显示最新上传的5张图片以及剪贴板里的图片。点击图片自动将上传的链接复制到剪贴板。(Windows平台不支持)'
},
{
url: 'https://cdn.jsdelivr.net/gh/Molunerfinn/test/picgo-site/third.png',
title: 'Mini小窗',
desc: 'Windows以及Linux系统下提供一个mini悬浮窗用于用户拖拽上传,节约你宝贵的桌面空间。'
},
{
url: 'https://cdn.jsdelivr.net/gh/Molunerfinn/test/picgo-site/forth.png',
title: '便捷管理',
desc: '查看你的上传记录,重复使用更方便。支持点击图片大图查看。支持删除图片(仅本地记录),让界面更加干净。'
},
{
url: 'https://cdn.jsdelivr.net/gh/Molunerfinn/test/picgo-site/fifth.png',
title: '可选图床',
desc: '默认支持微博图床、七牛图床、腾讯云COS、又拍云、GitHub、SM.MS、阿里云OSS、Imgur。方便不同图床的上传需求。2.0版本开始更可以自己开发插件实现其他图床的上传需求。'
},
{
url: 'https://cdn.jsdelivr.net/gh/Molunerfinn/test/picgo-site/sixth.png',
title: '多样链接',
desc: '支持5种默认剪贴板链接格式,包括一种自定义格式,让你的文本编辑游刃有余。'
},
{
url: 'https://cdn.jsdelivr.net/gh/Molunerfinn/test/picgo-site/seventh.png',
title: '插件系统',
desc: '2.0版本开始支持插件系统,让PicGo发挥无限潜能,成为一个极致的效率工具。'
}
]
}
},
created () {
this.getVersion()
},
methods: {
goLink (link) {
window.open(link, '_blank')
},
async getVersion () {
const release = 'https://api.github.com/repos/Molunerfinn/PicGo/releases/latest'
const res = await this.$http.get(release)
this.version = res.data.name
}
}
}
</script>
<style lang='stylus'>
[v-cloak]
display none
*
box-sizing border-box
body,
html,
h1
margin 0
padding 0
font-family "Source Sans Pro","Helvetica Neue","PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif
#app
position relative
.mask
position absolute
width 100%
height 100vh
top 0
left 0
background rgba(0,0,0, 0.7)
z-index -1
#header
height 100vh
width 100%
background-image url("https://cdn.jsdelivr.net/gh/Molunerfinn/test/picgo-site/bg.jpeg")
background-attachment fixed
background-size cover
background-position center
text-align center
padding 15vh
position relative
z-index 2
.logo
width 120px
.title
color #4BA2E2
font-size 36px
font-weight 300
margin 10px auto
text-align center
small
margin-left 10px
font-size 14px
.desc
font-weight 400
margin 20px auto 10px
color #ddd
a
text-decoration none
color #4BA2E2
.download
display inline-block
line-height 1
white-space nowrap
cursor pointer
background transparent
border 1px solid #d8dce5
color #ddd
-webkit-appearance none
text-align center
box-sizing border-box
outline none
margin 20px 12px
transition .1s
font-weight 500
user-select none
padding 12px 20px
font-size 14px
border-radius 20px
padding 12px 23px
transition .2s all ease-in-out
&:hover
background #ddd
color rgba(0,0,0, 0.7)
#container
position relative
text-align center
margin-top -10vh
z-index 3
.gallery
margin-bottom 60px
cursor pointer
transition all .2s ease-in-out
&:hover
transform scale(1.05)
.display-list
&__item
padding 48px
text-align left
background #2E2E2E
overflow hidden
&.o-item
background #fff
.display-list__desc
color #2E2E2E
img
width 100%
cursor pointer
transition all .2s ease-in-out
&:hover
transform scale(1.05)
&__content
padding-top 120px
&__title
color #4BA2E2
font-size 50px
&__desc
color #fff
margin-top 20px
.info
padding 48px 0
background #2E2E2E
color #fff
a
text-decoration none
color #fff
@media (max-width: 768px)
#header
padding 10vh
#container
.display-list
&__item
padding 24px 12px
&__content
padding-top 30px
&__title
font-size 25px
&__desc
margin-top 12px
</style>
+10
View File
@@ -0,0 +1,10 @@
import Vue from 'vue'
import App from './APP.vue'
import 'melody.css'
import axios from 'axios'
Vue.prototype.$http = axios
new Vue({
render: h => h(App)
}).$mount('#app')
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>PicGo</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
-119
View File
@@ -1,119 +0,0 @@
import type { Configuration } from 'electron-builder'
import dotenv from 'dotenv'
dotenv.config()
const shouldNotarize = process.env.SKIP_NOTARIZE !== 'true'
const config: Configuration = {
appId: 'com.molunerfinn.picgo',
productName: 'PicGo',
afterSign: shouldNotarize ? 'scripts/notarize.js' : undefined,
// 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/icon.icns',
extendInfo: {
LSUIElement: 0
},
target: [
{
target: 'dmg',
arch: ['arm64', 'x64']
}
],
artifactName: 'PicGo-${version}-${arch}.${ext}',
hardenedRuntime: true,
entitlements: 'build/entitlements.mac.plist',
entitlementsInherit: 'build/entitlements.mac.plist',
notarize: false
},
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: []
},
snap: {
publish: []
}
}
export default config
-71
View File
@@ -1,71 +0,0 @@
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
import { resolve } from 'path'
import { i18nTypesPlugin } from './scripts/vite-plugin-i18n-types'
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({
exclude: ['@picgo/i18n']
})],
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: [
tanstackRouter({
target: 'react',
autoCodeSplitting: true,
routesDirectory: './routes',
generatedRouteTree: './routeTree.gen.ts'
}),
react(),
i18nTypesPlugin(),
tailwindcss()
],
build: {
outDir: 'dist_electron/renderer',
rollupOptions: {
input: {
index: resolve(__dirname, 'src/renderer/index.html')
}
}
},
server: {
port: 5173
}
}
})
-179
View File
@@ -1,179 +0,0 @@
const globals = require('globals')
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 reactHooksPlugin = require('eslint-plugin-react-hooks')
const reactRefreshModule = require('eslint-plugin-react-refresh')
const vuePlugin = require('eslint-plugin-vue')
const stylistic = require('@stylistic/eslint-plugin')
const reactRefreshPlugin = reactRefreshModule.default || reactRefreshModule.reactRefresh || reactRefreshModule
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/**',
'vitest.config.ts',
'src/renderer/temp-vue/**',
'src/renderer/routeTree.gen.ts'
]
},
{
name: 'eslint/base',
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
__static: 'readonly'
}
},
plugins: {
import: importPlugin,
promise: promisePlugin,
'react-hooks': reactHooksPlugin,
'react-refresh': reactRefreshPlugin,
'@stylistic': stylistic
},
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',
'@stylistic/indent': ['error', 2],
'@stylistic/semi': ['error', 'never'],
'no-unexpected-multiline': 'error',
...reactHooksPlugin.configs.recommended.rules
}
},
...vueConfigs,
{
files: ['*.vue', '**/*.vue'],
rules: {
'vue/no-v-html': 'off',
'vue/attribute-hyphenation': 'off'
}
},
...tsConfigs,
{
files: ['**/*.{ts,tsx,vue}'],
rules: {
'no-undef': 'off'
}
},
{
files: ['src/renderer/**/*.{ts,tsx,vue}'],
rules: {
'no-restricted-imports': ['error', {
paths: [
{
name: 'electron',
allowTypeImports: true,
message: 'Use the preload bridge from @/utils/bridge in renderer runtime code.'
},
{
name: 'electron/renderer',
allowTypeImports: true,
message: 'Use the preload bridge from @/utils/bridge in renderer runtime code.'
}
]
}]
}
},
{
files: ['**/*.{tsx,jsx}'],
ignores: ['src/renderer/routes/**/*.tsx'],
rules: {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }]
}
},
{
files: ['**/*.d.ts'],
rules: {
'no-var': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-explicit-any': 'off'
}
},
{
// 1. 针对所有 JS 文件(或者特定目录)启用 Node 全局变量
files: ["**/*.js", "scripts/*.js"],
languageOptions: {
globals: {
...globals.node, // 注入 process, require, module, __dirname 等
...globals.browser // 如果你的项目是前端项目,可能还需要 browser
},
sourceType: "commonjs" // 如果你的项目代码主要是 CJS,加上这个;如果是 ESM 则设为 "module"
}
},
]
+65 -161
View File
@@ -1,167 +1,24 @@
{
"name": "picgo",
"version": "3.0.2",
"version": "2.2.0",
"private": true,
"main": "dist_electron/main/index.js",
"description": "A powerful & simple image uploader for creators.",
"author": {
"name": "Molunerfinn",
"email": "marksz@teamsz.xyz"
},
"scripts": {
"build": "electron-vite build",
"build:win": "npm run build && electron-builder --config electron-builder.config.ts --win --publish never",
"build:mac": "npm run build && electron-builder --config electron-builder.config.ts --mac --publish never",
"build:linux": "npm run build && electron-builder --config electron-builder.config.ts --linux --publish never",
"build:local": "dotenv -e .env -- electron-vite build && electron-builder --config electron-builder.config.ts --publish never",
"lint": "pnpm lint:dpdm && pnpm lint:dpdm-fe && eslint --ext .js,.jsx,.ts,.tsx,.vue src/",
"tsc": "tsc --noEmit",
"vue-tsc": "vue-tsc --noEmit",
"bump": "bump-version",
"cz": "git-cz",
"dev": "electron-vite dev",
"preview": "electron-vite preview",
"lint:fix": "eslint --fix --ext .js,.jsx,.ts,.tsx,.vue src/",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps",
"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",
"lint:dpdm-fe": "dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/renderer/main.tsx",
"check": "pnpm run tsc && pnpm run lint --fix",
"test": "vitest run",
"prepare": "husky",
"commitlint": "commitlint --edit"
"cz": "git-cz",
"bump": "bump-version",
"release": "vue-cli-service electron:build --publish always"
},
"dependencies": {
"@base-ui/react": "^1.2.0",
"@element-plus/icons-vue": "^2.3.2",
"@fontsource-variable/inter": "^5.2.8",
"@picgo/i18n": "^1.0.0",
"@picgo/store": "^2.2.2",
"@picgo/video-duration": "^1.0.1",
"@tanstack/history": "^1.161.4",
"@tanstack/react-query": "^5.100.9",
"@tanstack/react-router": "^1.166.2",
"@tanstack/router-plugin": "^1.166.2",
"@virtuoso.dev/masonry": "^1.4.2",
"ahooks": "^3.9.7",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clip-filepaths": "^0.3.0",
"clsx": "^2.1.1",
"comment-json": "^4.5.1",
"compare-versions": "^4.1.3",
"core-js": "^3.27.1",
"dayjs": "^1.11.19",
"dompurify": "^3.3.2",
"element-plus": "^2.3.7",
"epipebomb": "^1.0.0",
"fs-extra": "^10.0.0",
"i18next": "^25.8.14",
"immer": "^11.1.4",
"js-yaml": "^4.1.1",
"keycode": "^2.2.0",
"lodash": "^4.17.21",
"lodash-id": "^0.14.0",
"lowdb": "^1.0.0",
"lucide-react": "^0.577.0",
"marked": "^7.0.4",
"mime": "^4.1.0",
"mime-types": "^3.0.2",
"mitt": "^3.0.1",
"motion": "^12.35.1",
"multer": "^1.4.5-lts.1",
"next-themes": "^0.4.6",
"picgo": "^3.0.1",
"prismjs": "^1.30.0",
"qrcode.react": "^4.2.0",
"qrcode.vue": "^3.3.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-i18next": "^16.5.6",
"react-virtuoso": "^4.18.3",
"semver": "^7.7.3",
"shell-path": "2.1.0",
"sonner": "^2.0.7",
"systeminformation": "^5.27.14",
"tailwind-merge": "^3.5.0",
"tunnel": "^0.0.6",
"tw-animate-css": "^1.4.0",
"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",
"yaml": "^2.8.2",
"zustand": "^5.0.11"
},
"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",
"@stylistic/eslint-plugin": "^5.6.1",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@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/mime-types": "^3.0.1",
"@types/multer": "^1.4.12",
"@types/node": "^20",
"@types/prismjs": "^1.26.6",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@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-react": "^5.1.4",
"@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",
"dotenv-cli": "^11.0.0",
"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-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-vue": "^10.6.2",
"globals": "^16.5.0",
"husky": "^9.1.7",
"jsdom": "^28.1.0",
"postcss": "^8.4.23",
"shadcn": "^3.8.5",
"stylus": "^0.54.7",
"stylus-loader": "^3.0.2",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"vite": "^7.2.6",
"vitest": "^4.0.16",
"vue-tsc": "^3.2.3"
},
"commitlint": {
"extends": [
"./node_modules/@picgo/bump-version/commitlint-picgo"
]
"main": "background.js",
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"config": {
"commitizen": {
@@ -171,9 +28,56 @@
"config": "./node_modules/@picgo/bump-version/.cz-config.js"
}
},
"husky": {
"hooks": {
"commit-msg": "npm run lint:dpdm && commitlint -E HUSKY_GIT_PARAMS"
}
"commitlint": {
"extends": [
"./node_modules/@picgo/bump-version/commitlint-picgo"
]
},
"dependencies": {
"axios": "^0.19.0",
"core-js": "^3.3.2",
"element-ui": "^2.13.0",
"fix-path": "^2.1.0",
"fs-extra": "^8.1.0",
"keycode": "^2.2.0",
"lodash-id": "^0.14.0",
"lowdb": "^1.0.0",
"picgo": "^1.4.4",
"vue": "^2.6.10",
"vue-gallery": "^2.0.1",
"vue-lazyload": "^1.2.6",
"vue-router": "^3.1.3"
},
"devDependencies": {
"@commitlint/cli": "^8.2.0",
"@picgo/bump-version": "^1.0.3",
"@types/fs-extra": "^8.0.1",
"@types/inquirer": "^6.5.0",
"@types/lowdb": "^1.0.9",
"@types/node": "10.17.6",
"@types/request-promise-native": "^1.0.17",
"@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.0.0",
"@vue/cli-service": "^4.0.0",
"@vue/eslint-config-standard": "^4.0.0",
"@vue/eslint-config-typescript": "^4.0.0",
"commitizen": "^4.0.3",
"conventional-changelog": "^3.1.18",
"cz-customizable": "^6.2.0",
"electron": "^6.0.0",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"husky": "^3.1.0",
"stylus": "^0.54.7",
"stylus-loader": "^3.0.2",
"typescript": "~3.7.3",
"vue-cli-plugin-electron-builder": "^1.4.2",
"vue-property-decorator": "^8.3.0",
"vue-template-compiler": "^2.6.10"
},
"resolutions": {
"@types/node": "12.0.2"
}
}
-15183
View File
File diff suppressed because it is too large Load Diff
-12
View File
@@ -1,12 +0,0 @@
onlyBuiltDependencies:
- core-js
- ejs
- electron
- electron-winstaller
- esbuild
- husky
- vue-demi
supportedArchitectures:
cpu: [x64, arm64]
os: [current]
-593
View File
@@ -1,593 +0,0 @@
LANG_DISPLAY_LABEL: "English"
ABOUT: About
OPEN_MAIN_WINDOW: Open Main Window
CHOOSE_DEFAULT_PICBED: Choose Default Picbed
OPEN_UPDATE_HELPER: Open Update Helper
PRIVACY_TERMS_AGREEMENT: Privacy & Terms Agreement
RELOAD_APP: Reload App
UPLOAD_FAILED: Upload Failed
UPLOAD_SUCCEED: Upload Succeed
UPLOAD_PROGRESS: Upload Progress
OPERATION_FAILED: Operation Failed
OPERATION_SUCCEED: Operation Succeed
UPLOADING: Uploading
QUICK_UPLOAD: Quick Upload
UPLOAD_BY_CLIPBOARD: Upload by Clipboard
HIDE_WINDOW: Hide Window
SPONSOR_PICGO: Sponsor PicGo
SHOW_PICBED_QRCODE: Show Picbed Qrcode
PICBED_QRCODE: Picbed Qrcode
ENABLE: Enable
DISABLE: Disable
CONFIG_THING: Config ${c}
FIND_NEW_VERSION: Find New Version
NO_MORE_NOTICE: No More Notice
MORE: More
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:"
SIDEBAR_DASHBOARD: Dashboard
UPLOAD_AREA: Upload Area
ALBUM: Album
ALBUM_PROVIDERS: Providers
PICBEDS_SETTINGS: Picbeds Settings
PICGO_SETTINGS: PicGo Settings
PLUGIN_SETTINGS: Plugins Settings
DASHBOARD_HISTORY_PANEL_TITLE: History Panel
PICGO_CLOUD_TITLE: PicGo Cloud
PICGO_CLOUD_DESCRIPTION: A cloud service built by PicGo, connecting all your devices.
PICGO_CLOUD_BRAND_NAME: PicGo Cloud
PICGO_CLOUD_ERROR_TITLE: PicGo Cloud Error
PICGO_CLOUD_LOADING: Loading PicGo Cloud status...
PICGO_CLOUD_NOT_LOGGED_IN: Not logged in to PicGo Cloud.
PICGO_CLOUD_LOGIN: Log In
PICGO_CLOUD_LOGOUT: Log Out
PICGO_CLOUD_CANCEL_LOGIN: Cancel Login
PICGO_CLOUD_RETRY: Retry
PICGO_CLOUD_CONFIG_SYNC: Config Sync
PICGO_CLOUD_LOGIN_IN_PROGRESS: Login in progress. Please finish login in your browser.
PICGO_CLOUD_LOGGED_IN_AS: "Logged in as: ${user}"
PICGO_CLOUD_OPEN: Open PicGo Cloud
PICGO_CLOUD_LOGIN_TIMEOUT: Login timed out. Please try again.
PICGO_CLOUD_LOGIN_FAILED: Login failed. Please try again.
PICGO_CLOUD_AGREE_PREFIX: "I have read and agree to PicGo's "
PICGO_CLOUD_TERMS_OF_SERVICE: Terms of Service
PICGO_CLOUD_AGREE_AND: " and "
PICGO_CLOUD_PRIVACY_POLICY: Privacy Policy
PICGO_CLOUD_LOGIN_EXPIRED: Login expired. Please log in again.
PICGO_CLOUD_ENCRYPTION_MODE_LABEL: Encryption mode
PICGO_CLOUD_ENCRYPTION_MODE_AUTO: Auto
PICGO_CLOUD_ENCRYPTION_MODE_SERVER: Server-side encryption
PICGO_CLOUD_ENCRYPTION_MODE_E2E: End-to-end encryption
PICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO: "Auto: follow the cloud's last used encryption method (defaults to server-side)."
PICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER: "Server-side encryption: your config is encrypted on the server before being stored. No PIN required."
PICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E: "End-to-end encryption: use your PIN to encrypt/decrypt data. PicGo does not store your PIN; losing it means data cannot be recovered."
PICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC: Read full docs
PICGO_CLOUD_E2E_CHECKBOX_LABEL: Enable E2E encryption
PICGO_CLOUD_E2E_ENABLE_WARNING_TITLE: Enable E2E Encryption
PICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE: "E2E encryption requires you to keep your PIN safe. If you lose your PIN, the encrypted data cannot be recovered. PicGo does not store your PIN or any recovery data."
PICGO_CLOUD_REMOTE_E2E_AUTO_ENABLED: Remote config is E2E-encrypted. E2E has been enabled locally.
PICGO_CLOUD_E2E_PIN_SETUP_TITLE: Set up E2E PIN
PICGO_CLOUD_E2E_PIN_DECRYPT_TITLE: Enter E2E PIN
PICGO_CLOUD_E2E_PIN_RETRY_TITLE: "Incorrect PIN. Please try again (attempt ${retryCount})."
PICGO_CLOUD_E2E_PIN_PLACEHOLDER: PIN
PICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER: Confirm PIN
PICGO_CLOUD_CONFIG_SYNC_SUCCESS: Config sync succeeded.
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED: Conflict detected. Please resolve the conflicts.
PICGO_CLOUD_CONFIG_SYNC_FAILED: Config sync failed.
PICGO_CLOUD_CONFIG_SYNC_ABORTED: Config sync cancelled.
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE: Confirm switch encryption method?
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY: "You are switching from \"${from}\" to \"${to}\".\n\nNote: Switching encryption modes will clear all your cloud history versions.\nThis is because older history versions cannot be decrypted or verified under the new mode.\nAfter switching, the system will immediately create a new backup as the starting point."
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM: Confirm switch and clear history
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL: Cancel
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED: Encryption switch cancelled by user
PICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON: "Config sync failed: ${reason}"
PICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY: Too many incorrect PIN attempts. Please try again.
PICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID: Local config is invalid. Please check your config file.
PICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS: Config sync is in progress.
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING: There is a pending conflict session. Please resolve conflicts first.
PICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION: No conflict session found.
PICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE: Please choose Local or Cloud for all conflict items.
PICGO_CLOUD_CONFIG_SYNC_STARTING: Config sync started...
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE: "Conflict Detected (${count})"
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL: Choose All Local
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD: Choose All Cloud
PICGO_CLOUD_CONFIG_SYNC_RESET_ALL: Reset All
PICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION: Local Version
PICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION: Cloud Version
PICGO_CLOUD_CONFIG_SYNC_ABORT: Abort
PICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC: Confirm & Sync
PICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED: (empty)
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED: Conflicts resolved.
PICGO_CLOUD_LAST_SYNC_TIME: "Last local sync time: ${time}"
PICGO_CLOUD_LAST_SYNC_TIME_NONE: None
PICGO_CLOUD_LAST_SYNC_LABEL: Last sync
PICGO_CLOUD_PLAN_USAGE_TITLE: Plan & Usage
PICGO_CLOUD_PLAN_USAGE_DESC: View your current plan and usage overview.
PICGO_CLOUD_PLAN_PERIOD_LABEL: Plan Period
PICGO_CLOUD_STORAGE_LABEL: Storage
PICGO_CLOUD_FILES_LABEL: Files
PICGO_CLOUD_USAGE_PROGRESS: "${used} of ${total}"
PICGO_CLOUD_USAGE_UNLIMITED: Unlimited
PICGO_CLOUD_PLAN_PERIOD_RENEWS: "Renews on ${date}"
PICGO_CLOUD_PLAN_PERIOD_CANCELS: "Cancels on ${date}"
PICGO_CLOUD_PLAN_PERIOD_UNTIL: "Valid until ${date}"
PICGO_CLOUD_PLAN_PERIOD_LIFETIME: Lifetime
PICGO_CLOUD_PLAN_PERIOD_GRACE_LABEL: Grace Until
PICGO_CLOUD_PLAN_PERIOD_GRACE_TOOLTIP: "Your paid plan has expired and is now in the grace period. PicGo Cloud images remain accessible until this date, but quota is temporarily downgraded to free. Most paid features are temporarily unavailable. After this date, the account enters the frozen state."
PICGO_CLOUD_PLAN_PERIOD_FROZEN_LABEL: Frozen Until
PICGO_CLOUD_PLAN_PERIOD_FROZEN_TOOLTIP: "Your account is frozen. PicGo Cloud images return errors. After this date, data may be cleaned up. Renew to restore access."
PICGO_CLOUD_QUOTA_DOWNGRADED: "Quota temporarily downgraded to ${plan} during grace period. Renew to restore."
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_TITLE: Plan in grace period
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_DESC: "Your paid plan has expired and entered a ${days}-day grace period. Quota is temporarily downgraded to free, and most paid features are temporarily unavailable. Renew to restore full access."
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_TITLE: Account frozen
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_DESC: "Your account is frozen — cloud images are temporarily inaccessible and data will be cleaned up in ${days} days. Renew now to restore access."
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_TITLE: Data cleanup pending
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_DESC: Your account is pending cleanup and cloud data will be permanently deleted soon. Renew immediately if you want to keep your data.
PICGO_CLOUD_LIFECYCLE_BANNER_CTA: Renew Now
PICGO_CLOUD_LIFECYCLE_BANNER_DISMISS: Dismiss
PICGO_CLOUD_AUTO_IMPORT_DISABLED_BY_LIFECYCLE: Auto-import is paused while your plan is in grace or frozen period. It will resume after renewal.
PICGO_CLOUD_IMAGE_UNAVAILABLE: Image unavailable
PICGO_CLOUD_ERROR_GRACE_RESTRICTED: This action is unavailable during the grace period. Please renew your plan and try again.
PICGO_CLOUD_ERROR_ACCOUNT_FROZEN: Your account is frozen. Please renew your plan to restore access.
PICGO_CLOUD_ERROR_IMPORT_DISABLED: Auto-import is disabled. Enable it in PicGo Cloud settings first.
PICGO_CLOUD_ERROR_PLAN_INELIGIBLE: Your current plan does not support this feature. Please upgrade and try again.
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_ACTIVE: You have reached your plan quota. Upgrade to a higher plan to get more.
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_GRACE: Quota is downgraded to free tier during grace period. Renew to restore.
PICGO_CLOUD_ERROR_PLAN_REQUIRED: This feature requires a paid plan.
PICGO_CLOUD_USAGE_LOAD_FAILED: Failed to load usage data.
PICGO_CLOUD_CONFIG_SYNC_LOAD_FAILED: Failed to load sync state.
PICGO_CLOUD_FREE_PLAN_BANNER: You're on a free plan. Upgrade to unlock more quota and advanced cloud features.
PICGO_CLOUD_VIEW_PLANS: View Plans
PICGO_CLOUD_CONFIG_SYNC_TITLE: Configuration Sync
PICGO_CLOUD_CONFIG_SYNC_CARD_DESC: Sync your settings across devices securely.
PICGO_CLOUD_SYNC_NOW: Sync Now
PICGO_CLOUD_SYNC_QUOTA_LABEL: Sync quota
PICGO_CLOUD_SYNC_QUOTA_TIP: "Your plan retains the most recent ${limit} configuration snapshots. Even at ${limit} of ${limit}, syncing remains available — older snapshots beyond the limit are pruned automatically after each sync."
PICGO_CLOUD_LOGIN_PANEL_TITLE: Sign in to PicGo Cloud
PICGO_CLOUD_LOGIN_PANEL_DESC: Sign in to use PicGo Cloud as PicGo's official image host and enable cloud features.
PICGO_CLOUD_LOGIN_FEATURES_TITLE: Cloud features
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_TITLE: Official Image Host
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_DESC: PicGo's official image host with built-in album cloud sync.
PICGO_CLOUD_PAID_PLAN_BADGE: Paid plan
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE: Restart Required?
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE: Some settings may require a restart to take effect. Restart now?
PICGO_CLOUD_CONFIG_SYNC_RESTART_NOW: Restart Now
PICGO_CLOUD_CONFIG_SYNC_RESTART_LATER: Later
INPUT_BOX_CONFIRM_MISMATCH: The two inputs do not match.
PICGO_SPONSOR_TEXT: PicGo is a free software, if you like it, please don't forget to buy me a cup of coffee.
ALIPAY: Alipay
WECHATPAY: Wechat Pay
CHOOSE_PICBED: Choose Picbed
COPY_PICBED_CONFIG: Copy Picbed Config
COPY_PICBED_CONFIG_SUCCEED: Copy Picbed Config Succeed
INPUT: Input
CANCEL: Cancel
CONFIRM: Confirm
CHOOSE_SHOWED_PICBED: Choose Showed Picbed
CHOOSE_PASTE_FORMAT: Choose Paste Format
SEARCH: Search
COPY: Copy
DELETE: Delete
SELECT_ALL: Select All
CHANGE_IMAGE_URL: Change Image URL
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
SETTINGS: Settings
SETTINGS_OPEN_CONFIG_FILE: Open Config File
SETTINGS_CLICK_TO_OPEN: Click to Open
SETTINGS_SET_LOG_FILE: Set Log File
SETTINGS_CLICK_TO_SET: Click to Set
SETTINGS_CLICK_TO_CHECK: Click to Check
SETTINGS_SET_SHORTCUT: Set Shortcut
SETTINGS_URL_REWRITE: URL Rewrite
SETTINGS_CUSTOM_LINK_FORMAT: Custom Link Format
SETTINGS_SET_PROXY_AND_MIRROR: Set Proxy and Mirror
SETTINGS_SET_SERVER: Set Server
SETTINGS_CHECK_UPDATE: Check Update
SETTINGS_UPDATE_CHECK_RESULT: Latest version is ${version}, ${action}
SETTINGS_UPDATE_CHECK_NO_UPDATE: no update needed
SETTINGS_UPDATE_CHECK_CAN_UPDATE: update available
SETTINGS_OPEN_UPDATE_HELPER: Open Update Helper
SETTINGS_OPEN: Open
SETTINGS_CLOSE: Close
SETTINGS_ACCEPT_BETA_UPDATE: Accept Beta Update
SETTINGS_LAUNCH_ON_BOOT: Launch On Boot
SETTINGS_RENAME_BEFORE_UPLOAD: Rename Before Upload
SETTINGS_TIMESTAMP_RENAME: Timestamp Rename
SETTINGS_OPEN_UPLOAD_TIPS: Open Upload Tips
SETTINGS_NOTIFICATION_SOUND: Play Notification Sound
SETTINGS_MINI_WINDOW_ON_TOP: Mini Window On Top
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_UPLOAD_PROXY: Upload Proxy
SETTINGS_PLUGIN_INSTALL_PROXY: Proxy for Plugin Install
SETTINGS_PLUGIN_INSTALL_MIRROR: Mirror for Plugin Install
SETTINGS_CURRENT_VERSION: Current Version
SETTINGS_NEWEST_VERSION: Newest Version
SETTINGS_GETING: Getting...
SETTINGS_TIPS_HAS_NEW_VERSION: PicGo has a new version, please click confirm to open download page
SETTINGS_LOG_FILE: Log File
SETTINGS_LOG_LEVEL: Log Level
SETTINGS_LOG_FILE_SIZE: Log File Size
SETTINGS_SET_PICGO_SERVER: Set PicGo Server
SETTINGS_TIPS_SERVER_NOTICE: If you don't know what is the server's function, please read the document, or don't modify the configuration.
SETTINGS_ENABLE_SERVER: Enable Server
SETTINGS_SET_SERVER_HOST: Set Server Host
SETTINGS_SET_SERVER_PORT: Set Server Port
SETTINGS_TIP_PLACEHOLDER_HOST: Default:127.0.0.1
SETTINGS_TIP_PLACEHOLDER_PORT: Default:36677
SETTINGS_LOG_LEVEL_ALL: All
SETTINGS_LOG_LEVEL_SUCCESS: Success
SETTINGS_LOG_LEVEL_ERROR: Error
SETTINGS_LOG_LEVEL_INFO: Info
SETTINGS_LOG_LEVEL_WARN: Warn
SETTINGS_LOG_LEVEL_NONE: None
SETTINGS_RESULT: Result
SETTINGS_DEFAULT_PICBED: Default Picbed
SETTINGS_SET_DEFAULT_PICBED: Set Default Uploader
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
# url rewrite
URL_REWRITE_HELP: Rewrites uploaded image URLs. Rules are evaluated in order; the first matched rule wins.
URL_REWRITE_ADD_RULE: Add Rule
URL_REWRITE_EDIT_RULE: Edit Rule
URL_REWRITE_EMPTY: No rules
URL_REWRITE_ORDER: Order
URL_REWRITE_MATCH: Match
URL_REWRITE_REPLACE: Replace
URL_REWRITE_FLAGS: Flags
URL_REWRITE_ENABLED: Enabled
URL_REWRITE_ACTIONS: Actions
URL_REWRITE_MOVE_UP: Up
URL_REWRITE_MOVE_DOWN: Down
URL_REWRITE_EDIT: Edit
URL_REWRITE_DELETE: Delete
URL_REWRITE_DELETE_CONFIRM: Delete this rule?
URL_REWRITE_MATCH_TIPS: Supports regex (JavaScript RegExp)
URL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path
URL_REWRITE_REPLACE_TIPS: Replacement string (supports $1, $2, ...)
URL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath
URL_REWRITE_OPTIONS: Options
URL_REWRITE_RULE_ENABLED: Enable this rule
URL_REWRITE_FLAG_GLOBAL_LABEL: Global (g)
URL_REWRITE_FLAG_GLOBAL_DESC: Replace all occurrences, not just the first one
URL_REWRITE_FLAG_IGNORE_CASE_LABEL: Ignore case (i)
URL_REWRITE_FLAG_IGNORE_CASE_DESC: Case-insensitive matching (e.g. JPG equals jpg)
URL_REWRITE_MATCH_REQUIRED: Match is required
URL_REWRITE_REPLACE_REQUIRED: Replace is required
URL_REWRITE_INVALID_REGEX: Invalid regex
URL_REWRITE_PREVIEW_TITLE: Preview
URL_REWRITE_PREVIEW_TIPS: Enter a URL to see how the current rules rewrite it (matched in order; only the first match is applied)
URL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png
URL_REWRITE_PREVIEW_RUN: Preview
URL_REWRITE_PREVIEW_OUTPUT: Output URL
URL_REWRITE_PREVIEW_INPUT_REQUIRED: Please enter a URL to preview
URL_REWRITE_PREVIEW_RULE_INVALID: Invalid rule
URL_REWRITE_PREVIEW_MATCHED_RULE: Matched rule
URL_REWRITE_PREVIEW_NO_MATCH: No rules matched
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
SHORTCUT_NAME: Shortcut Name
SHORTCUT_BIND: Shortcut Binding
SHORTCUT_STATUS: Status
SHORTCUT_ENABLED: Enabled
SHORTCUT_DISABLED: Disabled
SHORTCUT_SOURCE: Source
SHORTCUT_HANDLE: Handle
SHORTCUT_ENABLE: Enable
SHORTCUT_DISABLE: Disable
SHORTCUT_EDIT: Edit
SHORTCUT_CHANGE_UPLOAD: Change Upload Shortcut
# album-page
ALBUM_URL_REWRITE_TITLE: Rewrite Selected Image URLs
ALBUM_URL_REWRITE_RESULT_TITLE: Rewrite Image URL Result
ALBUM_URL_REWRITE_WARN_NO_SELECTION: You must select at least one picture first
ALBUM_URL_REWRITE_APPLY_GLOBAL_RULES: Apply global URL rewrite rules
ALBUM_URL_REWRITE_GLOBAL_RULES_COUNT: Global rules
ALBUM_URL_REWRITE_TEMP_RULE_TIPS: Optional. Leave empty to skip the temporary rule. Temporary rule has higher priority than global rules.
ALBUM_URL_REWRITE_TEMP_RULE_REQUIRED: Match and Replace are required for the temporary rule
ALBUM_URL_REWRITE_NO_RULES_TO_APPLY: No global rules enabled and no temporary rule provided
ALBUM_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: Save the temporary rule to global URL rewrite rules?
ALBUM_URL_REWRITE_APPLY_AND_SAVE: Apply and Save
ALBUM_URL_REWRITE_APPLY_ONLY: Apply Only
ALBUM_URL_REWRITE_NO_CHANGES: No URLs were changed
ALBUM_URL_REWRITE_EMPTY_RESULT_WARN: The rewrite result is empty; skipped
# tray-page
WAIT_TO_UPLOAD: Wait to Upload
ALREADY_UPLOAD: Already Upload
# upload-page
PICTURE_UPLOAD: Picture Upload
DRAG_FILE_TO_HERE: Drag file to here, or
CLICK_TO_UPLOAD: click to upload
LINK_FORMAT: Link Format
CUSTOM: Custom
CLIPBOARD_PICTURE: Clipboard
TIPS_DRAG_VALID_PICTURE_OR_URL: Drag valid picture or url to here
TIPS_INPUT_URL: Input URL
TIPS_HTTP_PREFIX: Starts with http:// or https://. Multiple URLs supported (one per line)
TIPS_INPUT_VALID_URL: Input valid URL
# plugins
PLUGIN_SEARCH_PLACEHOLDER: Search picgo plugins on npm, or click the button to view the awesome plugins list
PLUGIN_SEARCH_EXACT_MATCH: Exact plugin name match
PLUGIN_INSTALL: Install
PLUGIN_INSTALLING: Installing...
PLUGIN_INSTALLED: Installed
PLUGIN_DEPRECATED_BADGE: Deprecated
PLUGIN_DEPRECATED_TITLE: This plugin has been deprecated by the author
PLUGIN_DOING_SOMETHING: Doing...
PLUGIN_LIST: Plugin List
PLUGIN_IMPORT_LOCAL: Import Local Plugins
# tips
TIPS_REMOVE_LINK: This operation will remove the picture from the album, continue?
TIPS_WILL_REMOVE_CHOOSED_IMAGES: This operation will remove the picture from the album, continue?
TIPS_MUST_CONTAINS_URL: Must contains $url or $fileName or $extName
TIPS_NETWORK_ERROR: Network Error
TIPS_NEED_RELOAD: Need Reload App
TIPS_PLEASE_CHOOSE_LOG_LEVEL: Please choose log level
TIPS_SET_SUCCEED: Set successfully
TIPS_PLUGIN_NOT_GUI_IMPLEMENT: This plugin is not optimized for the GUI, continue?
TIPS_CLICK_NOTIFICATION_TO_RELOAD: Click notification to reload app
TIPS_GET_PLUGIN_LIST_FAILED: Get plugin list failed
# ---renderer i18n end---
# plugins
PLUGIN_INSTALL_SUCCEED: Plugin install succeed
PLUGIN_INSTALL_FAILED: Plugin install failed
PLUGIN_UNINSTALL_SUCCEED: Plugin uninstall succeed
PLUGIN_UNINSTALL_FAILED: Plugin uninstall failed
PLUGIN_UPDATE_SUCCEED: Plugin update succeed
PLUGIN_UPDATE_FAILED: Plugin update failed
PLUGIN_IMPORT_SUCCEED: Plugin import succeed
PLUGIN_IMPORT_FAILED: Plugin import failed
ENABLE_PLUGIN: Enable Plugin
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_ALBUM_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_ALBUM_FILE_PATH_TIPS: "The album file path is: ${path}"
TOOLBOX_CHECK_ALBUM_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
TIPS_ERROR: Error
TIPS_SKIPPED_INVALID_URLS: Skipped ${n} invalid URL(s), see logs for details
TIPS_TOO_MANY_URLS_CONFIRM: You are about to upload ${n} URLs at once. This may cause lag. It's recommended to upload in batches. Continue?
TIPS_NO_VALID_URLS: No valid URL found
TIPS_INSTALL_NODE_AND_RELOAD_PICGO: Please install Node.js and restart PicGo to continue
TIPS_PLUGIN_REMOVE_ALBUM_ITEM: Plugin is trying to remove some images from your album, continue?
TIPS_PLUGIN_OVERWRITE_ALBUM: Plugin is trying to overwrite your album, continue?
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_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
TIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED: Custom link style modified successfully
TIPS_FIND_NEW_VERSION: Find new version ${v}update many new features, do you want to download the latest version?
TIPS_DELETE_UPLOADER_CONFIG: Are you sure you want to delete this config?
TIPS_COPY_UPLOADER_CONFIG: Are you sure you want to copy this config?
TIPS_UPLOADER_CONFIG_NAME_EMPTY: Config name cannot be empty
TIPS_UPLOADER_CONFIG_NOT_FOUND: Config not found
TIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST: Cannot delete the last config
# privacy
PRIVACY: "Please read and agree to the Privacy Policy ${privacyUrl} and Terms of Service ${termsUrl} before using."
PRIVACY_TIPS: Please agree the privacy policy to upload
QUIT: Quit
ALBUM_ALL_PHOTOS: All Photos
ALBUM_COLLECTIONS: Collections
ALBUM_TAGS: Tags
ALBUM_MENU: Menu
ALBUM_OPEN_INSPECTOR: Open Inspector
ALBUM_INSPECTOR_TITLE: Album Inspector
ALBUM_INSPECTOR_DESCRIPTION: Inspect and edit selected images.
ALBUM_CLEAR_SELECTION: Clear Selection
ALBUM_SELECTED_COUNT: ${count} selected
ALBUM_GRID_VIEW: Grid View
ALBUM_LIST_VIEW: List View
ALBUM_PREVIEW: Fullscreen Preview
ALBUM_PREVIEW_EMPTY: No images to preview
ALBUM_PREVIEW_PREV: Previous
ALBUM_PREVIEW_NEXT: Next
ALBUM_PREVIEW_CLOSE: Close preview
ALBUM_PREVIEW_COUNT: ${current} / ${total}
ALBUM_QUICK_ACTIONS: Quick Actions
ALBUM_EXPORT: Download
ALBUM_URL: URL
ALBUM_BATCH_REWRITE: Batch Rewrite
ALBUM_COLLECTION: Collection
ALBUM_ADD_TAG: Add tag
ALBUM_TAG_SUGGESTIONS: Suggestions
ALBUM_COLUMN_NAME: Name
ALBUM_COLUMN_PROVIDER: Provider
ALBUM_COLUMN_SIZE: Size
ALBUM_COLUMN_DATE: Date
ALBUM_ADD: Add
ALBUM_SOURCE_LOCAL: Local
ALBUM_SOURCE_CLOUD: Cloud
ALBUM_CLOUD_LOAD_FAILED: Failed to load cloud album
ALBUM_CLOUD_LOGIN_REQUIRED_TITLE: Login Required
ALBUM_CLOUD_LOGIN_REQUIRED_DESC: Sign in to a paid PicGo Cloud account to use cloud album
ALBUM_CLOUD_LOGIN_BUTTON: Log In
ALBUM_CLOUD_UPGRADE_TITLE: Upgrade Required
ALBUM_CLOUD_UPGRADE_DESC: Cloud album requires a paid plan
ALBUM_CLOUD_UPGRADE_BUTTON: Upgrade
ALBUM_CLOUD_FEATURES_TITLE: Why Cloud Album?
ALBUM_CLOUD_FEATURE_SYNC: Access your upload history across all devices
ALBUM_CLOUD_FEATURE_AUTO_IMPORT: Auto-import new uploads info (requires opt-in)
ALBUM_CLOUD_FEATURE_SECURE: One-click import local history to cloud
ALBUM_CLOUD_IMPORT_GUIDE_TITLE: Import Local Records
ALBUM_CLOUD_IMPORT_GUIDE_DESC: Import your local upload history to cloud album. Only records are imported, not the actual image files.
ALBUM_CLOUD_IMPORT_GUIDE_BUTTON: Start Import
ALBUM_CLOUD_EMPTY_TITLE: No Items
ALBUM_CLOUD_EMPTY_DESC: Your cloud album is empty. Upload an image to get started.
ALBUM_CLOUD_IMPORTING: Importing to cloud album...
ALBUM_CLOUD_IMPORT_SUCCESS: Successfully imported ${num} items to cloud album
ALBUM_CLOUD_IMPORT_FAILED: Failed to import to cloud album
ALBUM_CLOUD_REFRESH: Refresh
ALBUM_CLOUD_IMPORT_CONFIRM_TITLE: Enable Auto-Import
ALBUM_CLOUD_IMPORT_CONFIRM_DESC: This will enable auto-import so that future uploads are automatically recorded in your cloud album. All existing local records will also be imported now.
ALBUM_CLOUD_DELETE_FAILED: Failed to delete from cloud album
ALBUM_INSPECTOR_DETAILS_TITLE: Details
ALBUM_INSPECTOR_FILE_NAME: File name
ALBUM_INSPECTOR_UPLOADER: Uploader
ALBUM_INSPECTOR_CONTENT_TYPE: Content type
ALBUM_INSPECTOR_CREATED_AT: Created
ALBUM_INSPECTOR_UPDATED_AT: Updated
ALBUM_INSPECTOR_FILE_SIZE: Size
ALBUM_CLOUD_IMPORT_STATUS_TITLE: Cloud Album
ALBUM_CLOUD_IMPORTED: Imported to cloud
ALBUM_CLOUD_IMPORTED_TOOLTIP: This is a local marker. If the record was deleted from cloud, this status won't update. You can re-import if needed.
ALBUM_CLOUD_REIMPORT: Re-import
ALBUM_CLOUD_NATIVE: Uploaded via PicGo Cloud
ALBUM_CLOUD_IMPORT_BUTTON: Import to Cloud Album
ALBUM_CLOUD_IMPORT_BUTTON_COUNT: Import ${num} item(s) to Cloud Album
ALBUM_CLOUD_ALL_IMPORTED: All in cloud album
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_TITLE: Enable Auto Import
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_DESC: Auto import needs to be enabled before importing items to cloud album. Enable it now and continue?
ALBUM_CLOUD_AUTO_IMPORT_ENABLE_AND_IMPORT: Enable & Import
ALBUM_CLOUD_IMPORT_SINGLE_SUCCESS: Successfully imported to cloud album
PICGO_CLOUD_AUTO_IMPORT_LABEL: Auto-import to Cloud Album
PICGO_CLOUD_AUTO_IMPORT_DESC: Auto-import upload records from other uploaders to cloud album (PicGo Cloud is saved by default)
SIDEBAR_PLUGINS: Plugins
SIDEBAR_SYSTEM: System
SIDEBAR_NOTIFICATIONS: Notifications
SIDEBAR_EXPAND: Click to Expand
SIDEBAR_COLLAPSE: Collapse Sidebar
HISTORY_PANEL_TITLE: History
HISTORY_PANEL_FILTER_PLACEHOLDER: Filter...
HISTORY_PANEL_TODAY: Today
HISTORY_PANEL_YESTERDAY: Yesterday
DASHBOARD_NO_VISIBLE_PROVIDERS: No visible providers
DASHBOARD_NO_VISIBLE_PROVIDERS_DESCRIPTION: All providers are hidden. Open settings to change provider visibility.
DASHBOARD_OPEN_SETTINGS: Open Settings
DASHBOARD_DROP_IMAGES_HERE: Drop files here
DASHBOARD_OR: or
DASHBOARD_CLICK_TO_UPLOAD: Click to Upload
DASHBOARD_PASTE_FROM_CLIPBOARD: Paste from Clipboard
DASHBOARD_PASTE_FROM_URL: Paste from URL
DASHBOARD_CLIPBOARD: Clipboard
FIELD_IS_REQUIRED: ${field} is required
CONFIG_RENAME: Config Rename
SETTINGS_SECTION_GENERAL: General
SETTINGS_SECTION_APPEARANCE: Appearance
SETTINGS_SECTION_UPLOAD_WORKFLOW: Upload Workflow
SETTINGS_SECTION_NETWORK: Network
SETTINGS_SECTION_ADVANCED: Advanced
SETTINGS_SECTION_ABOUT: About
SETTINGS_NO_RESULTS_TITLE: No settings found
SETTINGS_NO_RESULTS_DESCRIPTION: Try another keyword or clear the search input.
SETTINGS_OPEN_SHORTCUTS: Shortcuts
SETTINGS_LINK_WEBSITE: Website
SETTINGS_LINK_GITHUB: GitHub
SETTINGS_LINK_DOCS: Documentation
SETTINGS_LINK_PRIVACY: Privacy Policy
SETTINGS_LINK_TERMS: Terms of Service
SETTINGS_APPEARANCE_MODE: Appearance
SETTINGS_APPEARANCE_MODE_LIGHT: Light
SETTINGS_APPEARANCE_MODE_DARK: Dark
SETTINGS_APPEARANCE_MODE_AUTO: Auto
COMMON_FOR_EXAMPLE: "Ex.: ${value}"
SETTINGS_SET_DEFAULT_CONFIG: Set as default
PROVIDER_DRAFT_CONFIG: Draft
PROVIDER_ACTIVE_UPLOADER_LABEL: Active
PROVIDER_ACTIVE_UPLOADER_TOOLTIP: ${uploaderName} is the active uploader
PROVIDER_DEFAULT_CONFIG_LABEL: Default
PROVIDER_DEFAULT_CONFIG_TOOLTIP: Used when ${uploaderName} is active.
PROVIDER_SIDEBAR_EMPTY: No uploaders or configs found.
PROVIDER_UPLOADER_ACTIONS: ${uploaderName} actions
PROVIDER_SIDEBAR_COLLAPSE: Collapse
PROVIDER_SIDEBAR_EXPAND: Expand
PROVIDER_CONFIG_ACTIONS: Config actions
PROVIDER_CREATE_CONFIG: Create config
PROVIDER_CREATE_CONFIG_DISABLED_EMPTY_SCHEMA: This uploader has no configuration options, creating additional configs is unnecessary
PROVIDER_INSTALL_MORE_UPLOADERS: Install more uploaders
PROVIDER_NO_UPLOADER_SELECTED: No uploader selected.
PROVIDER_NO_CONFIG_YET: No config yet
PROVIDER_NO_CONFIG_DESCRIPTION: Create a new config for ${uploaderName} to start editing schema-based options.
PROVIDER_CONFIGURATION: Configuration
PROVIDER_UPDATED_AT_LABEL: Updated
PROVIDER_DELETE_CONFIG_HINT: Permanently remove this configuration.
UPLOADER_SWITCHER_SELECT_PROVIDER: Select provider
UPLOADER_SWITCHER_CONFIG: Config
UPLOADER_SWITCHER_NO_CONFIG: No config
ALBUM_URL_REWRITE_DESCRIPTION: Batch rewrite the URLs for selected images.
ALBUM_URL_REWRITE_CHANGED: Changed
ALBUM_URL_REWRITE_UNCHANGED: Unchanged
TRAY_ALREADY_UPLOAD_EMPTY: No uploaded images yet
PLUGIN_EMPTY: No plugins found.
PLUGIN_EMPTY_DESCRIPTION: Search npm and install plugins to get started.
PLUGIN_DETAIL: Detail
PLUGIN_NO_CONFIG_SCHEMA: This plugin does not expose config schema.
PLUGIN_NO_TRANSFORMER_SCHEMA: This plugin does not expose transformer schema.
PLUGIN_NO_README: No README available for this plugin.
NO_OPTIONS_FOUND: No options found.
-593
View File
@@ -1,593 +0,0 @@
LANG_DISPLAY_LABEL: "日本語"
ABOUT: 情報
OPEN_MAIN_WINDOW: メインウィンドウを開く
CHOOSE_DEFAULT_PICBED: デフォルトの画像ホスティングを選択
OPEN_UPDATE_HELPER: アップデートヘルパーを開く
PRIVACY_TERMS_AGREEMENT: プライバシーポリシーと利用規約への同意
RELOAD_APP: アプリを再読み込み
UPLOAD_FAILED: アップロード失敗
UPLOAD_SUCCEED: アップロード成功
UPLOAD_PROGRESS: アップロード進行状況
OPERATION_FAILED: 操作失敗
OPERATION_SUCCEED: 操作成功
UPLOADING: アップロード中
QUICK_UPLOAD: クイックアップロード
UPLOAD_BY_CLIPBOARD: クリップボードからアップロード
HIDE_WINDOW: ウィンドウを隠す
SPONSOR_PICGO: PicGoを支援する
SHOW_PICBED_QRCODE: 画像ホスティングQRコードを表示
PICBED_QRCODE: 画像ホスティングQRコード
ENABLE: 有効
DISABLE: 無効
CONFIG_THING: "${c}を設定"
FIND_NEW_VERSION: 新しいバージョンを発見
NO_MORE_NOTICE: 今後通知しない
MORE: その他
SHOW_DEVTOOLS: 開発者ツールを表示
CURRENT_PICBED: 現在の画像ホスティング
OPEN_TOOLBOX: ツールボックスを開く
# ---renderer i18n begin---
CHOOSE_YOUR_DEFAULT_PICBED: "${d}をデフォルトの画像ホスティングに設定:"
SIDEBAR_DASHBOARD: ダッシュボード
UPLOAD_AREA: アップロードエリア
ALBUM: アルバム
ALBUM_PROVIDERS: プロバイダー
PICBEDS_SETTINGS: 画像ホスティング設定
PICGO_SETTINGS: PicGo設定
PLUGIN_SETTINGS: プラグイン設定
DASHBOARD_HISTORY_PANEL_TITLE: 履歴パネル
PICGO_CLOUD_TITLE: PicGo Cloud
PICGO_CLOUD_DESCRIPTION: すべてのデバイスをつなぐPicGoのクラウドサービスです。
PICGO_CLOUD_BRAND_NAME: PicGo Cloud
PICGO_CLOUD_ERROR_TITLE: PicGo Cloudエラー
PICGO_CLOUD_LOADING: PicGo Cloudの状態を読み込み中...
PICGO_CLOUD_NOT_LOGGED_IN: PicGo Cloudにログインしていません。
PICGO_CLOUD_LOGIN: ログイン
PICGO_CLOUD_LOGOUT: ログアウト
PICGO_CLOUD_CANCEL_LOGIN: ログインをキャンセル
PICGO_CLOUD_RETRY: リトライ
PICGO_CLOUD_CONFIG_SYNC: 設定同期
PICGO_CLOUD_LOGIN_IN_PROGRESS: ログイン処理中です。ブラウザでログインを完了してください。
PICGO_CLOUD_LOGGED_IN_AS: "ログイン中: ${user}"
PICGO_CLOUD_OPEN: PicGo Cloudを開く
PICGO_CLOUD_LOGIN_TIMEOUT: ログインがタイムアウトしました。もう一度お試しください。
PICGO_CLOUD_LOGIN_FAILED: ログインに失敗しました。もう一度お試しください。
PICGO_CLOUD_AGREE_PREFIX: "PicGoの以下に同意します: "
PICGO_CLOUD_TERMS_OF_SERVICE: 利用規約
PICGO_CLOUD_AGREE_AND: "および"
PICGO_CLOUD_PRIVACY_POLICY: プライバシーポリシー
PICGO_CLOUD_LOGIN_EXPIRED: ログインが期限切れです。再度ログインしてください。
PICGO_CLOUD_ENCRYPTION_MODE_LABEL: 暗号化モード
PICGO_CLOUD_ENCRYPTION_MODE_AUTO: 自動
PICGO_CLOUD_ENCRYPTION_MODE_SERVER: サーバー側暗号化
PICGO_CLOUD_ENCRYPTION_MODE_E2E: エンドツーエンド暗号化
PICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO: "自動: クラウドで最後に使用した暗号化方式に従います(デフォルトはサーバー側暗号化)。"
PICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER: "サーバー側暗号化: 設定はサーバーに保存される前に暗号化されます。PINは不要です。"
PICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E: "エンドツーエンド暗号化: PINを使用してデータを暗号化/復号化します。PicGoはPINを保存しません。紛失した場合、データは復元できません。"
PICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC: ドキュメントを読む
PICGO_CLOUD_E2E_CHECKBOX_LABEL: E2E暗号化を有効にする
PICGO_CLOUD_E2E_ENABLE_WARNING_TITLE: E2E暗号化を有効にする
PICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE: "E2E暗号化を使用するにはPINを安全に保管する必要があります。PINを紛失すると、暗号化されたデータを復元できません。PicGoはPINや復旧データを保存しません。"
PICGO_CLOUD_REMOTE_E2E_AUTO_ENABLED: リモート設定がE2E暗号化されています。ローカルでもE2Eが有効になりました。
PICGO_CLOUD_E2E_PIN_SETUP_TITLE: E2E暗号化PINの設定
PICGO_CLOUD_E2E_PIN_DECRYPT_TITLE: E2E暗号化PINを入力
PICGO_CLOUD_E2E_PIN_RETRY_TITLE: "PINが正しくありません。もう一度お試しください(${retryCount}回目)。"
PICGO_CLOUD_E2E_PIN_PLACEHOLDER: PIN
PICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER: PINを確認
PICGO_CLOUD_CONFIG_SYNC_SUCCESS: 設定の同期に成功しました。
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED: 競合が検出されました。競合を解決してください。
PICGO_CLOUD_CONFIG_SYNC_FAILED: 設定の同期に失敗しました。
PICGO_CLOUD_CONFIG_SYNC_ABORTED: 設定の同期がキャンセルされました。
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE: 暗号化方式を切り替えますか?
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY: "「${from}」から「${to}」に切り替えます。\n\n注意: 暗号化モードを切り替えると、クラウドの履歴バージョンがすべて削除されます。\nこれは古い履歴バージョンが新しいモードで復号化または検証できないためです。\n切り替え後、システムは新しいバックアップを開始点として即座に作成します。"
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM: 切り替えを確認して履歴を削除
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL: キャンセル
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED: ユーザーにより暗号化の切り替えがキャンセルされました
PICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON: "設定同期失敗: ${reason}"
PICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY: PINの入力回数が上限に達しました。もう一度お試しください。
PICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID: ローカル設定が無効です。設定ファイルを確認してください。
PICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS: 設定の同期が進行中です。
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING: 保留中の競合セッションがあります。先に競合を解決してください。
PICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION: 競合セッションが見つかりません。
PICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE: すべての競合項目についてローカルまたはクラウドを選択してください。
PICGO_CLOUD_CONFIG_SYNC_STARTING: 設定の同期を開始しています...
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE: "競合が検出されました(${count}件)"
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL: すべてローカルを選択
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD: すべてクラウドを選択
PICGO_CLOUD_CONFIG_SYNC_RESET_ALL: すべてリセット
PICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION: ローカルバージョン
PICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION: クラウドバージョン
PICGO_CLOUD_CONFIG_SYNC_ABORT: 中止
PICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC: 確認して同期
PICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED: (空)
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED: 競合が解決されました。
PICGO_CLOUD_LAST_SYNC_TIME: "最終ローカル同期日時: ${time}"
PICGO_CLOUD_LAST_SYNC_TIME_NONE: なし
PICGO_CLOUD_LAST_SYNC_LABEL: 最終同期
PICGO_CLOUD_PLAN_USAGE_TITLE: プランと使用量
PICGO_CLOUD_PLAN_USAGE_DESC: 現在のプランと使用量の概要を確認します。
PICGO_CLOUD_PLAN_PERIOD_LABEL: プラン期間
PICGO_CLOUD_STORAGE_LABEL: ストレージ
PICGO_CLOUD_FILES_LABEL: ファイル
PICGO_CLOUD_USAGE_PROGRESS: "${total}中${used}使用"
PICGO_CLOUD_USAGE_UNLIMITED: 無制限
PICGO_CLOUD_PLAN_PERIOD_RENEWS: "${date}に更新"
PICGO_CLOUD_PLAN_PERIOD_CANCELS: "${date}に解約"
PICGO_CLOUD_PLAN_PERIOD_UNTIL: "${date}まで有効"
PICGO_CLOUD_PLAN_PERIOD_LIFETIME: 永久ライセンス
PICGO_CLOUD_PLAN_PERIOD_GRACE_LABEL: 猶予期限
PICGO_CLOUD_PLAN_PERIOD_GRACE_TOOLTIP: "有料プランが期限切れとなり、現在猶予期間中です。この日付まではPicGo Cloud画像にアクセスできますが、使用量は一時的に無料枠に引き下げられます。ほとんどの有料機能は一時的に利用できません。この日付以降、アカウントは凍結状態になります。"
PICGO_CLOUD_PLAN_PERIOD_FROZEN_LABEL: 凍結期限
PICGO_CLOUD_PLAN_PERIOD_FROZEN_TOOLTIP: "アカウントが凍結されています。PicGo Cloud画像はエラーを返します。この日付以降、データが削除される場合があります。アクセスを復元するには更新してください。"
PICGO_CLOUD_QUOTA_DOWNGRADED: "猶予期間中、使用量は一時的に${plan}に引き下げられています。復元するには更新してください。"
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_TITLE: プランが猶予期間中です
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_DESC: "有料プランが期限切れとなり、${days}日間の猶予期間に入りました。使用量は一時的に無料枠に引き下げられ、ほとんどの有料機能が一時的に利用できません。全機能を復元するには更新してください。"
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_TITLE: アカウントが凍結されました
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_DESC: "アカウントが凍結されています。クラウド画像は一時的にアクセスできず、${days}日後にデータが削除されます。今すぐ更新してアクセスを復元してください。"
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_TITLE: データ削除予定
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_DESC: アカウントは削除待ち状態であり、クラウドデータがまもなく完全に削除されます。データを保持するにはすぐに更新してください。
PICGO_CLOUD_LIFECYCLE_BANNER_CTA: 今すぐ更新
PICGO_CLOUD_LIFECYCLE_BANNER_DISMISS: 閉じる
PICGO_CLOUD_AUTO_IMPORT_DISABLED_BY_LIFECYCLE: プランが猶予期間または凍結期間中のため、自動インポートは一時停止されています。更新後に再開されます。
PICGO_CLOUD_IMAGE_UNAVAILABLE: 画像を利用できません
PICGO_CLOUD_ERROR_GRACE_RESTRICTED: 猶予期間中はこの操作を利用できません。プランを更新してからもう一度お試しください。
PICGO_CLOUD_ERROR_ACCOUNT_FROZEN: アカウントが凍結されています。アクセスを復元するにはプランを更新してください。
PICGO_CLOUD_ERROR_IMPORT_DISABLED: 自動インポートが無効です。PicGo Cloud設定で先に有効にしてください。
PICGO_CLOUD_ERROR_PLAN_INELIGIBLE: 現在のプランではこの機能をサポートしていません。アップグレードしてからもう一度お試しください。
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_ACTIVE: プランの使用量上限に達しました。上位プランにアップグレードしてください。
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_GRACE: 猶予期間中は使用量が無料枠に引き下げられます。復元するには更新してください。
PICGO_CLOUD_ERROR_PLAN_REQUIRED: この機能には有料プランが必要です。
PICGO_CLOUD_USAGE_LOAD_FAILED: 使用量データの読み込みに失敗しました。
PICGO_CLOUD_CONFIG_SYNC_LOAD_FAILED: 同期状態の読み込みに失敗しました。
PICGO_CLOUD_FREE_PLAN_BANNER: 無料プランをご利用中です。アップグレードすると、より多くの容量と高度なクラウド機能を利用できます。
PICGO_CLOUD_VIEW_PLANS: プランを見る
PICGO_CLOUD_CONFIG_SYNC_TITLE: 設定同期
PICGO_CLOUD_CONFIG_SYNC_CARD_DESC: デバイス間で設定を安全に同期します。
PICGO_CLOUD_SYNC_NOW: 今すぐ同期
PICGO_CLOUD_SYNC_QUOTA_LABEL: 同期クォータ
PICGO_CLOUD_SYNC_QUOTA_TIP: "プランでは最新の${limit}個の設定スナップショットが保持されます。${limit}個中${limit}個を使用していても同期は引き続き可能です。上限を超えた古いスナップショットは同期のたびに自動的に削除されます。"
PICGO_CLOUD_LOGIN_PANEL_TITLE: PicGo Cloudにログイン
PICGO_CLOUD_LOGIN_PANEL_DESC: PicGo Cloudを公式画像ホスティングとして使用し、クラウド機能を有効にするにはログインしてください。
PICGO_CLOUD_LOGIN_FEATURES_TITLE: クラウド機能
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_TITLE: 公式画像ホスティング
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_DESC: アルバムクラウド同期が組み込まれたPicGoの公式画像ホスティングです。
PICGO_CLOUD_PAID_PLAN_BADGE: 有料プラン
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE: 再起動が必要ですか?
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE: 一部の設定は再起動が必要な場合があります。今すぐ再起動しますか?
PICGO_CLOUD_CONFIG_SYNC_RESTART_NOW: 今すぐ再起動
PICGO_CLOUD_CONFIG_SYNC_RESTART_LATER: あとで
INPUT_BOX_CONFIRM_MISMATCH: 2つの入力値が一致しません。
PICGO_SPONSOR_TEXT: PicGoはフリーソフトウェアです。気に入っていただけたら、コーヒー一杯の支援をお忘れなく。
ALIPAY: Alipay
WECHATPAY: WeChat Pay
CHOOSE_PICBED: 画像ホスティングを選択
COPY_PICBED_CONFIG: 画像ホスティング設定をコピー
COPY_PICBED_CONFIG_SUCCEED: 画像ホスティング設定のコピーに成功
INPUT: 入力
CANCEL: キャンセル
CONFIRM: 確認
CHOOSE_SHOWED_PICBED: 表示する画像ホスティングを選択
CHOOSE_PASTE_FORMAT: 貼り付け形式を選択
SEARCH: 検索
COPY: コピー
DELETE: 削除
SELECT_ALL: すべて選択
CHANGE_IMAGE_URL: 画像URLを変更
CHANGE_IMAGE_URL_SUCCEED: 画像URLの変更に成功
COPY_LINK_SUCCEED: リンクのコピーに成功
BATCH_COPY_LINK_SUCCEED: リンクの一括コピーに成功
FILE_RENAME: ファイル名を変更
COPY_FILE_PATH: ファイルパスをコピー
OPEN_FILE_PATH: ファイルパスを開く
SUCCESS: 成功
FAILED: 失敗
# settings
SETTINGS: 設定
SETTINGS_OPEN_CONFIG_FILE: 設定ファイルを開く
SETTINGS_CLICK_TO_OPEN: クリックして開く
SETTINGS_SET_LOG_FILE: ログファイルの設定
SETTINGS_CLICK_TO_SET: クリックして設定
SETTINGS_CLICK_TO_CHECK: クリックして確認
SETTINGS_SET_SHORTCUT: ショートカットの設定
SETTINGS_URL_REWRITE: URLリライト
SETTINGS_CUSTOM_LINK_FORMAT: カスタムリンク形式
SETTINGS_SET_PROXY_AND_MIRROR: プロキシとミラーの設定
SETTINGS_SET_SERVER: サーバーの設定
SETTINGS_CHECK_UPDATE: アップデートを確認
SETTINGS_UPDATE_CHECK_RESULT: "最新バージョンは${version}です。${action}"
SETTINGS_UPDATE_CHECK_NO_UPDATE: 更新の必要はありません
SETTINGS_UPDATE_CHECK_CAN_UPDATE: アップデート可能
SETTINGS_OPEN_UPDATE_HELPER: アップデートヘルパーを開く
SETTINGS_OPEN: 開く
SETTINGS_CLOSE: 閉じる
SETTINGS_ACCEPT_BETA_UPDATE: ベータ版アップデートを許可
SETTINGS_LAUNCH_ON_BOOT: 起動時に自動実行
SETTINGS_RENAME_BEFORE_UPLOAD: アップロード前にリネーム
SETTINGS_TIMESTAMP_RENAME: タイムスタンプでリネーム
SETTINGS_OPEN_UPLOAD_TIPS: アップロードのヒントを表示
SETTINGS_NOTIFICATION_SOUND: 通知音を再生
SETTINGS_MINI_WINDOW_ON_TOP: ミニウィンドウを常に最前面に
SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: アップロード後にURLを自動コピー
SETTINGS_TIPS_PLACEHOLDER_URL: $urlでURL位置を表します
SETTINGS_TIPS_PLACEHOLDER_FILENAME: $fileNameでファイル名の位置を表します
SETTINGS_TIPS_PLACEHOLDER_EXTNAME: $extNameでファイル拡張子の位置を表します
SETTINGS_TIPS_SUCH_AS: "例: $url/$fileName"
SETTINGS_UPLOAD_PROXY: アップロードプロキシ
SETTINGS_PLUGIN_INSTALL_PROXY: プラグインインストール用プロキシ
SETTINGS_PLUGIN_INSTALL_MIRROR: プラグインインストール用ミラー
SETTINGS_CURRENT_VERSION: 現在のバージョン
SETTINGS_NEWEST_VERSION: 最新バージョン
SETTINGS_GETING: 取得中...
SETTINGS_TIPS_HAS_NEW_VERSION: PicGoに新しいバージョンがあります。確認をクリックしてダウンロードページを開いてください
SETTINGS_LOG_FILE: ログファイル
SETTINGS_LOG_LEVEL: ログレベル
SETTINGS_LOG_FILE_SIZE: ログファイルサイズ
SETTINGS_SET_PICGO_SERVER: PicGoサーバーの設定
SETTINGS_TIPS_SERVER_NOTICE: サーバー機能がわからない場合は、ドキュメントを参照するか、設定を変更しないでください。
SETTINGS_ENABLE_SERVER: サーバーを有効にする
SETTINGS_SET_SERVER_HOST: サーバーホストを設定
SETTINGS_SET_SERVER_PORT: サーバーポートを設定
SETTINGS_TIP_PLACEHOLDER_HOST: デフォルト:127.0.0.1
SETTINGS_TIP_PLACEHOLDER_PORT: デフォルト:36677
SETTINGS_LOG_LEVEL_ALL: すべて
SETTINGS_LOG_LEVEL_SUCCESS: 成功
SETTINGS_LOG_LEVEL_ERROR: エラー
SETTINGS_LOG_LEVEL_INFO: 情報
SETTINGS_LOG_LEVEL_WARN: 警告
SETTINGS_LOG_LEVEL_NONE: なし
SETTINGS_RESULT: 結果
SETTINGS_DEFAULT_PICBED: デフォルトの画像ホスティング
SETTINGS_SET_DEFAULT_PICBED: デフォルトのアップローダーを設定
SETTINGS_NOT_CONFIG_OPTIONS: 設定項目なし
SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: 内蔵クリップボードを使用してアップロード
SETTINGS_CHOOSE_LANGUAGE: 言語を選択
UPLOADER_CONFIG_NAME: 設定名
BUILTIN_CLIPBOARD_TIPS: スクリプトの代わりに内蔵クリップボード機能を使用してアップロード
# url rewrite
URL_REWRITE_HELP: アップロードされた画像URLをリライトします。ルールは順番に評価され、最初にマッチしたルールが適用されます。
URL_REWRITE_ADD_RULE: ルールを追加
URL_REWRITE_EDIT_RULE: ルールを編集
URL_REWRITE_EMPTY: ルールなし
URL_REWRITE_ORDER: 順序
URL_REWRITE_MATCH: マッチパターン
URL_REWRITE_REPLACE: 置換文字列
URL_REWRITE_FLAGS: フラグ
URL_REWRITE_ENABLED: 有効
URL_REWRITE_ACTIONS: 操作
URL_REWRITE_MOVE_UP: 上へ
URL_REWRITE_MOVE_DOWN: 下へ
URL_REWRITE_EDIT: 編集
URL_REWRITE_DELETE: 削除
URL_REWRITE_DELETE_CONFIRM: このルールを削除しますか?
URL_REWRITE_MATCH_TIPS: 正規表現(JavaScript RegExp)に対応しています
URL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path
URL_REWRITE_REPLACE_TIPS: 置換文字列($1, $2, ... に対応)
URL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath
URL_REWRITE_OPTIONS: オプション
URL_REWRITE_RULE_ENABLED: このルールを有効にする
URL_REWRITE_FLAG_GLOBAL_LABEL: グローバル (g)
URL_REWRITE_FLAG_GLOBAL_DESC: 最初のマッチだけでなく、すべてのマッチを置換します
URL_REWRITE_FLAG_IGNORE_CASE_LABEL: 大文字小文字を区別しない (i)
URL_REWRITE_FLAG_IGNORE_CASE_DESC: "大文字小文字を区別せずにマッチします(例: JPGとjpgを同一視)"
URL_REWRITE_MATCH_REQUIRED: マッチパターンは必須です
URL_REWRITE_REPLACE_REQUIRED: 置換文字列は必須です
URL_REWRITE_INVALID_REGEX: 無効な正規表現です
URL_REWRITE_PREVIEW_TITLE: プレビュー
URL_REWRITE_PREVIEW_TIPS: URLを入力すると、現在のルールがどのようにリライトするかを確認できます(順番にマッチし、最初のマッチのみ適用されます)
URL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png
URL_REWRITE_PREVIEW_RUN: プレビュー
URL_REWRITE_PREVIEW_OUTPUT: 出力URL
URL_REWRITE_PREVIEW_INPUT_REQUIRED: プレビューするURLを入力してください
URL_REWRITE_PREVIEW_RULE_INVALID: 無効なルールです
URL_REWRITE_PREVIEW_MATCHED_RULE: マッチしたルール
URL_REWRITE_PREVIEW_NO_MATCH: マッチするルールなし
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: ミニウィンドウを開く
SETTINGS_STARTUP_MODE_HIDE: サイレント起動
# shortcut-page
SHORTCUT_NAME: ショートカット名
SHORTCUT_BIND: ショートカットバインド
SHORTCUT_STATUS: ステータス
SHORTCUT_ENABLED: 有効
SHORTCUT_DISABLED: 無効
SHORTCUT_SOURCE: ソース
SHORTCUT_HANDLE: ハンドル
SHORTCUT_ENABLE: 有効にする
SHORTCUT_DISABLE: 無効にする
SHORTCUT_EDIT: 編集
SHORTCUT_CHANGE_UPLOAD: アップロードショートカットを変更
# album-page
ALBUM_URL_REWRITE_TITLE: 選択した画像URLをリライト
ALBUM_URL_REWRITE_RESULT_TITLE: 画像URLリライト結果
ALBUM_URL_REWRITE_WARN_NO_SELECTION: 最低1枚の画像を選択してください
ALBUM_URL_REWRITE_APPLY_GLOBAL_RULES: グローバルURLリライトルールを適用
ALBUM_URL_REWRITE_GLOBAL_RULES_COUNT: グローバルルール
ALBUM_URL_REWRITE_TEMP_RULE_TIPS: 任意項目です。一時ルールをスキップするには空のままにしてください。一時ルールはグローバルルールより優先されます。
ALBUM_URL_REWRITE_TEMP_RULE_REQUIRED: 一時ルールにはマッチパターンと置換文字列の両方が必要です
ALBUM_URL_REWRITE_NO_RULES_TO_APPLY: 有効なグローバルルールがなく、一時ルールも入力されていません
ALBUM_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: 一時ルールをグローバルURLリライトルールに保存しますか?
ALBUM_URL_REWRITE_APPLY_AND_SAVE: 適用して保存
ALBUM_URL_REWRITE_APPLY_ONLY: 適用のみ
ALBUM_URL_REWRITE_NO_CHANGES: 変更されたURLはありません
ALBUM_URL_REWRITE_EMPTY_RESULT_WARN: リライト結果が空のためスキップしました
# tray-page
WAIT_TO_UPLOAD: アップロード待ち
ALREADY_UPLOAD: アップロード済み
# upload-page
PICTURE_UPLOAD: 画像アップロード
DRAG_FILE_TO_HERE: ファイルをここにドラッグ、または
CLICK_TO_UPLOAD: クリックしてアップロード
LINK_FORMAT: リンク形式
CUSTOM: カスタム
CLIPBOARD_PICTURE: クリップボード
TIPS_DRAG_VALID_PICTURE_OR_URL: 有効な画像またはURLをここにドラッグしてください
TIPS_INPUT_URL: URLを入力
TIPS_HTTP_PREFIX: http://またはhttps://で始めてください。複数URL対応(1行に1つ)
TIPS_INPUT_VALID_URL: 有効なURLを入力してください
# plugins
PLUGIN_SEARCH_PLACEHOLDER: npmでpicgoプラグインを検索するか、ボタンをクリックしておすすめプラグイン一覧を確認してください
PLUGIN_SEARCH_EXACT_MATCH: プラグイン名の完全一致
PLUGIN_INSTALL: インストール
PLUGIN_INSTALLING: インストール中...
PLUGIN_INSTALLED: インストール済み
PLUGIN_DEPRECATED_BADGE: 非推奨
PLUGIN_DEPRECATED_TITLE: このプラグインは作者によりサポートが終了しています
PLUGIN_DOING_SOMETHING: 処理中...
PLUGIN_LIST: プラグイン一覧
PLUGIN_IMPORT_LOCAL: ローカルプラグインをインポート
# tips
TIPS_REMOVE_LINK: この操作で画像がアルバムから削除されます。続行しますか?
TIPS_WILL_REMOVE_CHOOSED_IMAGES: この操作で画像がアルバムから削除されます。続行しますか?
TIPS_MUST_CONTAINS_URL: $url、$fileName、$extNameのいずれかを含める必要があります
TIPS_NETWORK_ERROR: ネットワークエラー
TIPS_NEED_RELOAD: アプリの再読み込みが必要です
TIPS_PLEASE_CHOOSE_LOG_LEVEL: ログレベルを選択してください
TIPS_SET_SUCCEED: 設定成功
TIPS_PLUGIN_NOT_GUI_IMPLEMENT: このプラグインはGUIに最適化されていません。続行しますか?
TIPS_CLICK_NOTIFICATION_TO_RELOAD: 通知をクリックしてアプリを再読み込み
TIPS_GET_PLUGIN_LIST_FAILED: プラグイン一覧の取得に失敗しました
# ---renderer i18n end---
# plugins
PLUGIN_INSTALL_SUCCEED: プラグインのインストールに成功
PLUGIN_INSTALL_FAILED: プラグインのインストールに失敗
PLUGIN_UNINSTALL_SUCCEED: プラグインのアンインストールに成功
PLUGIN_UNINSTALL_FAILED: プラグインのアンインストールに失敗
PLUGIN_UPDATE_SUCCEED: プラグインのアップデートに成功
PLUGIN_UPDATE_FAILED: プラグインのアップデートに失敗
PLUGIN_IMPORT_SUCCEED: プラグインのインポートに成功
PLUGIN_IMPORT_FAILED: プラグインのインポートに失敗
ENABLE_PLUGIN: プラグインを有効にする
DISABLE_PLUGIN: プラグインを無効にする
UNINSTALL_PLUGIN: プラグインをアンインストール
UPDATE_PLUGIN: プラグインをアップデート
# toolbox
TOOLBOX: ツールボックス
TOOLBOX_TITLE: PicGoの実行問題を診断
TOOLBOX_SUB_TITLE: 以下の項目を即座に検査して使用上の問題を修正します
TOOLBOX_CHECK_CONFIG_FILE_BROKEN: 設定ファイルの破損を確認
TOOLBOX_CHECK_ALBUM_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_ALBUM_FILE_PATH_TIPS: "アルバムファイルのパス: ${path}"
TOOLBOX_CHECK_ALBUM_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: 警告
TIPS_ERROR: エラー
TIPS_SKIPPED_INVALID_URLS: "無効なURL${n}件をスキップしました。詳細はログを確認してください"
TIPS_TOO_MANY_URLS_CONFIRM: "一度にURL${n}件をアップロードしようとしています。遅延が発生する可能性があるため、分割してアップロードすることをお勧めします。続行しますか?"
TIPS_NO_VALID_URLS: 有効なURLが見つかりません
TIPS_INSTALL_NODE_AND_RELOAD_PICGO: Node.jsをインストールしてPicGoを再起動してください
TIPS_PLUGIN_REMOVE_ALBUM_ITEM: プラグインがアルバムから一部の画像を削除しようとしています。続行しますか?
TIPS_PLUGIN_OVERWRITE_ALBUM: プラグインがアルバムを上書きしようとしています。続行しますか?
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_CUSTOM_CONFIG_FILE_PATH_ERROR: カスタム設定ファイルの解析エラーです。パスの内容を確認してください
TIPS_SHORTCUT_MODIFIED_SUCCEED: ショートカットの変更に成功しました
TIPS_SHORTCUT_MODIFIED_CONFLICT: ショートカットが競合しています。再設定してください
TIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED: カスタムリンクスタイルの変更に成功しました
TIPS_FIND_NEW_VERSION: "新しいバージョン${v}が見つかりました。多くの新機能が追加されています。最新バージョンをダウンロードしますか?"
TIPS_DELETE_UPLOADER_CONFIG: この設定を削除しますか?
TIPS_COPY_UPLOADER_CONFIG: この設定をコピーしますか?
TIPS_UPLOADER_CONFIG_NAME_EMPTY: 設定名を空にすることはできません
TIPS_UPLOADER_CONFIG_NOT_FOUND: 設定が見つかりません
TIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST: 最後の設定は削除できません
# privacy
PRIVACY: "ご利用の前にプライバシーポリシー${privacyUrl}と利用規約${termsUrl}をお読みいただき、同意してください。"
PRIVACY_TIPS: アップロードするにはプライバシーポリシーに同意してください
QUIT: 終了
ALBUM_ALL_PHOTOS: すべての写真
ALBUM_COLLECTIONS: コレクション
ALBUM_TAGS: タグ
ALBUM_MENU: メニュー
ALBUM_OPEN_INSPECTOR: インスペクターを開く
ALBUM_INSPECTOR_TITLE: アルバムインスペクター
ALBUM_INSPECTOR_DESCRIPTION: 選択した画像を検査・編集します。
ALBUM_CLEAR_SELECTION: 選択を解除
ALBUM_SELECTED_COUNT: "${count}件選択中"
ALBUM_GRID_VIEW: グリッド表示
ALBUM_LIST_VIEW: リスト表示
ALBUM_PREVIEW: フルスクリーンプレビュー
ALBUM_PREVIEW_EMPTY: プレビューする画像がありません
ALBUM_PREVIEW_PREV: 前へ
ALBUM_PREVIEW_NEXT: 次へ
ALBUM_PREVIEW_CLOSE: プレビューを閉じる
ALBUM_PREVIEW_COUNT: "${current} / ${total}"
ALBUM_QUICK_ACTIONS: クイック操作
ALBUM_EXPORT: ダウンロード
ALBUM_URL: URL
ALBUM_BATCH_REWRITE: 一括リライト
ALBUM_COLLECTION: コレクション
ALBUM_ADD_TAG: タグを追加
ALBUM_TAG_SUGGESTIONS: おすすめタグ
ALBUM_COLUMN_NAME: 名前
ALBUM_COLUMN_PROVIDER: プロバイダー
ALBUM_COLUMN_SIZE: サイズ
ALBUM_COLUMN_DATE: 日付
ALBUM_ADD: 追加
ALBUM_SOURCE_LOCAL: ローカル
ALBUM_SOURCE_CLOUD: クラウド
ALBUM_CLOUD_LOAD_FAILED: クラウドアルバムの読み込みに失敗しました
ALBUM_CLOUD_LOGIN_REQUIRED_TITLE: ログインが必要です
ALBUM_CLOUD_LOGIN_REQUIRED_DESC: クラウドアルバムを使用するには有料のPicGo Cloudアカウントでログインしてください
ALBUM_CLOUD_LOGIN_BUTTON: ログイン
ALBUM_CLOUD_UPGRADE_TITLE: アップグレードが必要です
ALBUM_CLOUD_UPGRADE_DESC: クラウドアルバムには有料プランが必要です
ALBUM_CLOUD_UPGRADE_BUTTON: アップグレード
ALBUM_CLOUD_FEATURES_TITLE: クラウドアルバムを使う理由
ALBUM_CLOUD_FEATURE_SYNC: すべてのデバイスからアップロード履歴にアクセス
ALBUM_CLOUD_FEATURE_AUTO_IMPORT: 新しいアップロード情報を自動インポート(オプトインが必要)
ALBUM_CLOUD_FEATURE_SECURE: ローカル履歴をワンクリックでクラウドにインポート
ALBUM_CLOUD_IMPORT_GUIDE_TITLE: ローカル記録をインポート
ALBUM_CLOUD_IMPORT_GUIDE_DESC: ローカルのアップロード履歴をクラウドアルバムにインポートします。実際の画像ファイルではなく、記録のみがインポートされます。
ALBUM_CLOUD_IMPORT_GUIDE_BUTTON: インポート開始
ALBUM_CLOUD_EMPTY_TITLE: 項目なし
ALBUM_CLOUD_EMPTY_DESC: クラウドアルバムは空です。画像をアップロードして始めましょう。
ALBUM_CLOUD_IMPORTING: クラウドアルバムにインポート中...
ALBUM_CLOUD_IMPORT_SUCCESS: "${num}件をクラウドアルバムにインポートしました"
ALBUM_CLOUD_IMPORT_FAILED: クラウドアルバムへのインポートに失敗しました
ALBUM_CLOUD_REFRESH: 更新
ALBUM_CLOUD_IMPORT_CONFIRM_TITLE: 自動インポートを有効にする
ALBUM_CLOUD_IMPORT_CONFIRM_DESC: この操作を実行すると自動インポートが有効になり、以降のアップロードがクラウドアルバムに自動記録されます。既存のローカル記録も同時にインポートされます。
ALBUM_CLOUD_DELETE_FAILED: クラウドアルバムからの削除に失敗しました
ALBUM_INSPECTOR_DETAILS_TITLE: 詳細情報
ALBUM_INSPECTOR_FILE_NAME: ファイル名
ALBUM_INSPECTOR_UPLOADER: アップローダー
ALBUM_INSPECTOR_CONTENT_TYPE: コンテンツタイプ
ALBUM_INSPECTOR_CREATED_AT: 作成日
ALBUM_INSPECTOR_UPDATED_AT: 更新日
ALBUM_INSPECTOR_FILE_SIZE: サイズ
ALBUM_CLOUD_IMPORT_STATUS_TITLE: クラウドアルバム
ALBUM_CLOUD_IMPORTED: クラウドにインポート済み
ALBUM_CLOUD_IMPORTED_TOOLTIP: これはローカルマーカーです。クラウドから記録が削除されても、このステータスは更新されません。必要に応じて再インポートできます。
ALBUM_CLOUD_REIMPORT: 再インポート
ALBUM_CLOUD_NATIVE: PicGo Cloud経由でアップロード
ALBUM_CLOUD_IMPORT_BUTTON: クラウドアルバムにインポート
ALBUM_CLOUD_IMPORT_BUTTON_COUNT: "${num}件をクラウドアルバムにインポート"
ALBUM_CLOUD_ALL_IMPORTED: すべてクラウドアルバムに登録済み
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_TITLE: 自動インポートを有効にする
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_DESC: クラウドアルバムにインポートするには、まず自動インポートを有効にする必要があります。今すぐ有効にして続行しますか?
ALBUM_CLOUD_AUTO_IMPORT_ENABLE_AND_IMPORT: 有効にしてインポート
ALBUM_CLOUD_IMPORT_SINGLE_SUCCESS: クラウドアルバムへのインポートに成功しました
PICGO_CLOUD_AUTO_IMPORT_LABEL: クラウドアルバムへ自動インポート
PICGO_CLOUD_AUTO_IMPORT_DESC: 他のアップローダーのアップロード記録をクラウドアルバムに自動インポートします(PicGo Cloudはデフォルトで保存されます)
SIDEBAR_PLUGINS: プラグイン
SIDEBAR_SYSTEM: システム
SIDEBAR_NOTIFICATIONS: 通知
SIDEBAR_EXPAND: クリックして展開
SIDEBAR_COLLAPSE: サイドバーを折りたたむ
HISTORY_PANEL_TITLE: 履歴
HISTORY_PANEL_FILTER_PLACEHOLDER: フィルター...
HISTORY_PANEL_TODAY: 今日
HISTORY_PANEL_YESTERDAY: 昨日
DASHBOARD_NO_VISIBLE_PROVIDERS: 表示するプロバイダーがありません
DASHBOARD_NO_VISIBLE_PROVIDERS_DESCRIPTION: すべてのプロバイダーが非表示になっています。設定でプロバイダーの表示を変更してください。
DASHBOARD_OPEN_SETTINGS: 設定を開く
DASHBOARD_DROP_IMAGES_HERE: ファイルをここにドロップ
DASHBOARD_OR: または
DASHBOARD_CLICK_TO_UPLOAD: クリックしてアップロード
DASHBOARD_PASTE_FROM_CLIPBOARD: クリップボードから貼り付け
DASHBOARD_PASTE_FROM_URL: URLから貼り付け
DASHBOARD_CLIPBOARD: クリップボード
FIELD_IS_REQUIRED: ${field}は必須です
CONFIG_RENAME: 設定名を変更
SETTINGS_SECTION_GENERAL: 一般
SETTINGS_SECTION_APPEARANCE: 外観
SETTINGS_SECTION_UPLOAD_WORKFLOW: アップロードワークフロー
SETTINGS_SECTION_NETWORK: ネットワーク
SETTINGS_SECTION_ADVANCED: 詳細設定
SETTINGS_SECTION_ABOUT: 情報
SETTINGS_NO_RESULTS_TITLE: 設定が見つかりません
SETTINGS_NO_RESULTS_DESCRIPTION: 別のキーワードを試すか、検索入力をクリアしてください。
SETTINGS_OPEN_SHORTCUTS: ショートカット
SETTINGS_LINK_WEBSITE: Webサイト
SETTINGS_LINK_GITHUB: GitHub
SETTINGS_LINK_DOCS: ドキュメント
SETTINGS_LINK_PRIVACY: プライバシーポリシー
SETTINGS_LINK_TERMS: 利用規約
SETTINGS_APPEARANCE_MODE: 外観
SETTINGS_APPEARANCE_MODE_LIGHT: ライト
SETTINGS_APPEARANCE_MODE_DARK: ダーク
SETTINGS_APPEARANCE_MODE_AUTO: 自動
COMMON_FOR_EXAMPLE: "例: ${value}"
SETTINGS_SET_DEFAULT_CONFIG: デフォルトに設定
PROVIDER_DRAFT_CONFIG: 下書き
PROVIDER_ACTIVE_UPLOADER_LABEL: アクティブ
PROVIDER_ACTIVE_UPLOADER_TOOLTIP: ${uploaderName}がアクティブなアップローダーです
PROVIDER_DEFAULT_CONFIG_LABEL: デフォルト
PROVIDER_DEFAULT_CONFIG_TOOLTIP: ${uploaderName}がアクティブな場合に使用されます。
PROVIDER_SIDEBAR_EMPTY: アップローダーまたは設定が見つかりません。
PROVIDER_UPLOADER_ACTIONS: ${uploaderName}の操作
PROVIDER_SIDEBAR_COLLAPSE: 折りたたむ
PROVIDER_SIDEBAR_EXPAND: 展開
PROVIDER_CONFIG_ACTIONS: 設定の操作
PROVIDER_CREATE_CONFIG: 設定を作成
PROVIDER_CREATE_CONFIG_DISABLED_EMPTY_SCHEMA: このアップローダーには設定項目がないため、追加の設定を作成する必要はありません
PROVIDER_INSTALL_MORE_UPLOADERS: アップローダーを追加インストール
PROVIDER_NO_UPLOADER_SELECTED: アップローダーが選択されていません。
PROVIDER_NO_CONFIG_YET: 設定はまだありません
PROVIDER_NO_CONFIG_DESCRIPTION: ${uploaderName}のスキーマベースのオプションを編集するには、新しい設定を作成してください。
PROVIDER_CONFIGURATION: 設定
PROVIDER_UPDATED_AT_LABEL: 更新日
PROVIDER_DELETE_CONFIG_HINT: この設定を完全に削除します。
UPLOADER_SWITCHER_SELECT_PROVIDER: プロバイダーを選択
UPLOADER_SWITCHER_CONFIG: 設定
UPLOADER_SWITCHER_NO_CONFIG: 設定なし
ALBUM_URL_REWRITE_DESCRIPTION: 選択した画像のURLを一括リライトします。
ALBUM_URL_REWRITE_CHANGED: 変更あり
ALBUM_URL_REWRITE_UNCHANGED: 変更なし
TRAY_ALREADY_UPLOAD_EMPTY: アップロードされた画像はまだありません
PLUGIN_EMPTY: プラグインが見つかりません。
PLUGIN_EMPTY_DESCRIPTION: npmを検索してプラグインをインストールして始めましょう。
PLUGIN_DETAIL: 詳細
PLUGIN_NO_CONFIG_SCHEMA: このプラグインは設定スキーマを提供していません。
PLUGIN_NO_TRANSFORMER_SCHEMA: このプラグインはトランスフォーマースキーマを提供していません。
PLUGIN_NO_README: このプラグインのREADMEはありません。
NO_OPTIONS_FOUND: オプションが見つかりません。
-593
View File
@@ -1,593 +0,0 @@
LANG_DISPLAY_LABEL: "한국어"
ABOUT: 정보
OPEN_MAIN_WINDOW: 메인 창 열기
CHOOSE_DEFAULT_PICBED: 기본 이미지 호스팅 선택
OPEN_UPDATE_HELPER: 업데이트 도우미 열기
PRIVACY_TERMS_AGREEMENT: 개인정보 처리방침 및 이용약관 동의
RELOAD_APP: 앱 새로고침
UPLOAD_FAILED: 업로드 실패
UPLOAD_SUCCEED: 업로드 성공
UPLOAD_PROGRESS: 업로드 진행률
OPERATION_FAILED: 작업 실패
OPERATION_SUCCEED: 작업 성공
UPLOADING: 업로드 중
QUICK_UPLOAD: 빠른 업로드
UPLOAD_BY_CLIPBOARD: 클립보드로 업로드
HIDE_WINDOW: 창 숨기기
SPONSOR_PICGO: PicGo 후원하기
SHOW_PICBED_QRCODE: 이미지 호스팅 QR 코드 표시
PICBED_QRCODE: 이미지 호스팅 QR 코드
ENABLE: 사용
DISABLE: 사용 안 함
CONFIG_THING: "${c} 설정"
FIND_NEW_VERSION: 새 버전 찾기
NO_MORE_NOTICE: 다시 알리지 않기
MORE: 더 보기
SHOW_DEVTOOLS: 개발자 도구 표시
CURRENT_PICBED: 현재 이미지 호스팅
OPEN_TOOLBOX: 도구 상자 열기
# ---renderer i18n begin---
CHOOSE_YOUR_DEFAULT_PICBED: "${d}을(를) 기본 이미지 호스팅으로 선택:"
SIDEBAR_DASHBOARD: 대시보드
UPLOAD_AREA: 업로드 영역
ALBUM: 앨범
ALBUM_PROVIDERS: 제공자
PICBEDS_SETTINGS: 이미지 호스팅 설정
PICGO_SETTINGS: PicGo 설정
PLUGIN_SETTINGS: 플러그인 설정
DASHBOARD_HISTORY_PANEL_TITLE: 기록 패널
PICGO_CLOUD_TITLE: PicGo Cloud
PICGO_CLOUD_DESCRIPTION: 모든 기기를 연결하는 PicGo의 클라우드 서비스입니다.
PICGO_CLOUD_BRAND_NAME: PicGo Cloud
PICGO_CLOUD_ERROR_TITLE: PicGo Cloud 오류
PICGO_CLOUD_LOADING: PicGo Cloud 상태를 불러오는 중...
PICGO_CLOUD_NOT_LOGGED_IN: PicGo Cloud에 로그인되어 있지 않습니다.
PICGO_CLOUD_LOGIN: 로그인
PICGO_CLOUD_LOGOUT: 로그아웃
PICGO_CLOUD_CANCEL_LOGIN: 로그인 취소
PICGO_CLOUD_RETRY: 재시도
PICGO_CLOUD_CONFIG_SYNC: 설정 동기화
PICGO_CLOUD_LOGIN_IN_PROGRESS: 로그인이 진행 중입니다. 브라우저에서 로그인을 완료해 주세요.
PICGO_CLOUD_LOGGED_IN_AS: "로그인 계정: ${user}"
PICGO_CLOUD_OPEN: PicGo Cloud 열기
PICGO_CLOUD_LOGIN_TIMEOUT: 로그인 시간이 초과되었습니다. 다시 시도해 주세요.
PICGO_CLOUD_LOGIN_FAILED: 로그인에 실패했습니다. 다시 시도해 주세요.
PICGO_CLOUD_AGREE_PREFIX: "PicGo의 "
PICGO_CLOUD_TERMS_OF_SERVICE: 이용약관
PICGO_CLOUD_AGREE_AND: " 및 "
PICGO_CLOUD_PRIVACY_POLICY: 개인정보 처리방침
PICGO_CLOUD_LOGIN_EXPIRED: 로그인이 만료되었습니다. 다시 로그인해 주세요.
PICGO_CLOUD_ENCRYPTION_MODE_LABEL: 암호화 모드
PICGO_CLOUD_ENCRYPTION_MODE_AUTO: 자동
PICGO_CLOUD_ENCRYPTION_MODE_SERVER: 서버 측 암호화
PICGO_CLOUD_ENCRYPTION_MODE_E2E: 종단간 암호화
PICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO: "자동: 클라우드에서 마지막으로 사용한 암호화 방식을 따릅니다(기본값은 서버 측 암호화)."
PICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER: "서버 측 암호화: 설정이 서버에 저장되기 전에 암호화됩니다. PIN이 필요하지 않습니다."
PICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E: "종단간 암호화: PIN을 사용해 데이터를 암호화/복호화합니다. PicGo는 PIN을 저장하지 않으며, 분실 시 데이터를 복구할 수 없습니다."
PICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC: 전체 문서 보기
PICGO_CLOUD_E2E_CHECKBOX_LABEL: 종단간 암호화 사용
PICGO_CLOUD_E2E_ENABLE_WARNING_TITLE: 종단간 암호화 사용
PICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE: "종단간 암호화를 사용하려면 PIN을 안전하게 보관해야 합니다. PIN을 분실하면 암호화된 데이터를 복구할 수 없습니다. PicGo는 PIN이나 복구 데이터를 저장하지 않습니다."
PICGO_CLOUD_REMOTE_E2E_AUTO_ENABLED: 원격 설정이 종단간 암호화되어 있습니다. 로컬에서도 종단간 암호화가 활성화되었습니다.
PICGO_CLOUD_E2E_PIN_SETUP_TITLE: 종단간 암호화 PIN 설정
PICGO_CLOUD_E2E_PIN_DECRYPT_TITLE: 종단간 암호화 PIN 입력
PICGO_CLOUD_E2E_PIN_RETRY_TITLE: "PIN이 올바르지 않습니다. 다시 시도해 주세요 (시도 ${retryCount}회)."
PICGO_CLOUD_E2E_PIN_PLACEHOLDER: PIN
PICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER: PIN 확인
PICGO_CLOUD_CONFIG_SYNC_SUCCESS: 설정 동기화에 성공했습니다.
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED: 충돌이 발견되었습니다. 충돌을 해결해 주세요.
PICGO_CLOUD_CONFIG_SYNC_FAILED: 설정 동기화에 실패했습니다.
PICGO_CLOUD_CONFIG_SYNC_ABORTED: 설정 동기화가 취소되었습니다.
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE: 암호화 방식을 전환하시겠습니까?
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY: "\"${from}\"에서 \"${to}\"(으)로 전환합니다.\n\n참고: 암호화 모드를 전환하면 클라우드 기록 버전이 모두 삭제됩니다.\n이는 이전 기록 버전을 새 모드에서 복호화하거나 검증할 수 없기 때문입니다.\n전환 후에는 시스템이 즉시 새로운 백업을 시작점으로 생성합니다."
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM: 전환 확인 및 기록 삭제
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL: 취소
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED: 사용자가 암호화 전환을 취소했습니다
PICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON: "설정 동기화 실패: ${reason}"
PICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY: PIN을 너무 많이 잘못 입력했습니다. 다시 시도해 주세요.
PICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID: 로컬 설정이 유효하지 않습니다. 설정 파일을 확인해 주세요.
PICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS: 설정 동기화가 진행 중입니다.
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING: 대기 중인 충돌 세션이 있습니다. 먼저 충돌을 해결해 주세요.
PICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION: 충돌 세션을 찾을 수 없습니다.
PICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE: 모든 충돌 항목에 대해 로컬 또는 클라우드를 선택해 주세요.
PICGO_CLOUD_CONFIG_SYNC_STARTING: 설정 동기화를 시작합니다...
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE: "충돌 발견 (${count}개)"
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL: 모두 로컬 선택
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD: 모두 클라우드 선택
PICGO_CLOUD_CONFIG_SYNC_RESET_ALL: 전체 초기화
PICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION: 로컬 버전
PICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION: 클라우드 버전
PICGO_CLOUD_CONFIG_SYNC_ABORT: 중단
PICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC: 확인 및 동기화
PICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED: (비어 있음)
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED: 충돌이 해결되었습니다.
PICGO_CLOUD_LAST_SYNC_TIME: "마지막 로컬 동기화 시간: ${time}"
PICGO_CLOUD_LAST_SYNC_TIME_NONE: 없음
PICGO_CLOUD_LAST_SYNC_LABEL: 마지막 동기화
PICGO_CLOUD_PLAN_USAGE_TITLE: 요금제 및 사용량
PICGO_CLOUD_PLAN_USAGE_DESC: 현재 요금제와 사용량 개요를 확인하세요.
PICGO_CLOUD_PLAN_PERIOD_LABEL: 요금제 기간
PICGO_CLOUD_STORAGE_LABEL: 저장 공간
PICGO_CLOUD_FILES_LABEL: 파일
PICGO_CLOUD_USAGE_PROGRESS: "${total} 중 ${used}"
PICGO_CLOUD_USAGE_UNLIMITED: 무제한
PICGO_CLOUD_PLAN_PERIOD_RENEWS: "${date}에 갱신"
PICGO_CLOUD_PLAN_PERIOD_CANCELS: "${date}에 해지"
PICGO_CLOUD_PLAN_PERIOD_UNTIL: "${date}까지 유효"
PICGO_CLOUD_PLAN_PERIOD_LIFETIME: 평생 이용
PICGO_CLOUD_PLAN_PERIOD_GRACE_LABEL: 유예 기간 종료일
PICGO_CLOUD_PLAN_PERIOD_GRACE_TOOLTIP: "유료 요금제가 만료되어 현재 유예 기간입니다. 이 날짜까지는 PicGo Cloud 이미지에 계속 접근할 수 있지만, 할당량은 일시적으로 무료 등급으로 낮아집니다. 대부분의 유료 기능은 일시적으로 사용할 수 없습니다. 이 날짜 이후에는 계정이 정지 상태가 됩니다."
PICGO_CLOUD_PLAN_PERIOD_FROZEN_LABEL: 정지 종료일
PICGO_CLOUD_PLAN_PERIOD_FROZEN_TOOLTIP: "계정이 정지되었습니다. PicGo Cloud 이미지는 오류를 반환합니다. 이 날짜 이후에는 데이터가 삭제될 수 있습니다. 접근 권한을 복구하려면 갱신해 주세요."
PICGO_CLOUD_QUOTA_DOWNGRADED: "유예 기간 동안 할당량이 일시적으로 ${plan}(으)로 낮아졌습니다. 복구하려면 갱신해 주세요."
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_TITLE: 요금제가 유예 기간입니다
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_DESC: "유료 요금제가 만료되어 ${days}일간의 유예 기간에 들어갔습니다. 할당량은 일시적으로 무료 등급으로 낮아지고, 대부분의 유료 기능을 사용할 수 없습니다. 전체 기능을 복구하려면 갱신해 주세요."
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_TITLE: 계정이 정지되었습니다
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_DESC: "계정이 정지되어 클라우드 이미지에 일시적으로 접근할 수 없으며, ${days}일 후 데이터가 삭제됩니다. 지금 갱신하면 접근 권한을 복구할 수 있습니다."
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_TITLE: 데이터 삭제 예정
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_DESC: 계정이 삭제 대기 상태이며 클라우드 데이터가 곧 영구적으로 삭제됩니다. 데이터를 유지하려면 즉시 갱신해 주세요.
PICGO_CLOUD_LIFECYCLE_BANNER_CTA: 지금 갱신
PICGO_CLOUD_LIFECYCLE_BANNER_DISMISS: 닫기
PICGO_CLOUD_AUTO_IMPORT_DISABLED_BY_LIFECYCLE: 요금제가 유예 또는 정지 상태인 동안에는 자동 가져오기가 일시 중지됩니다. 갱신 후 다시 시작됩니다.
PICGO_CLOUD_IMAGE_UNAVAILABLE: 이미지를 사용할 수 없음
PICGO_CLOUD_ERROR_GRACE_RESTRICTED: 유예 기간 동안에는 이 작업을 사용할 수 없습니다. 요금제를 갱신한 후 다시 시도해 주세요.
PICGO_CLOUD_ERROR_ACCOUNT_FROZEN: 계정이 정지되었습니다. 접근 권한을 복구하려면 요금제를 갱신해 주세요.
PICGO_CLOUD_ERROR_IMPORT_DISABLED: 자동 가져오기가 비활성화되어 있습니다. PicGo Cloud 설정에서 먼저 활성화해 주세요.
PICGO_CLOUD_ERROR_PLAN_INELIGIBLE: 현재 요금제에서는 이 기능을 지원하지 않습니다. 업그레이드 후 다시 시도해 주세요.
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_ACTIVE: 요금제 할당량에 도달했습니다. 더 많은 용량을 사용하려면 상위 요금제로 업그레이드하세요.
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_GRACE: 유예 기간 동안에는 할당량이 무료 등급으로 낮아집니다. 복구하려면 갱신해 주세요.
PICGO_CLOUD_ERROR_PLAN_REQUIRED: 이 기능은 유료 요금제가 필요합니다.
PICGO_CLOUD_USAGE_LOAD_FAILED: 사용량 데이터를 불러오지 못했습니다.
PICGO_CLOUD_CONFIG_SYNC_LOAD_FAILED: 동기화 상태를 불러오지 못했습니다.
PICGO_CLOUD_FREE_PLAN_BANNER: 무료 요금제를 사용 중입니다. 업그레이드하면 더 많은 할당량과 고급 클라우드 기능을 이용할 수 있습니다.
PICGO_CLOUD_VIEW_PLANS: 요금제 보기
PICGO_CLOUD_CONFIG_SYNC_TITLE: 설정 동기화
PICGO_CLOUD_CONFIG_SYNC_CARD_DESC: 기기 간 설정을 안전하게 동기화하세요.
PICGO_CLOUD_SYNC_NOW: 지금 동기화
PICGO_CLOUD_SYNC_QUOTA_LABEL: 동기화 할당량
PICGO_CLOUD_SYNC_QUOTA_TIP: "요금제는 최근 ${limit}개의 설정 스냅샷을 보관합니다. ${limit}개 중 ${limit}개를 모두 사용해도 동기화는 계속 가능합니다 — 한도를 초과한 오래된 스냅샷은 매번 동기화 후 자동으로 정리됩니다."
PICGO_CLOUD_LOGIN_PANEL_TITLE: PicGo Cloud에 로그인
PICGO_CLOUD_LOGIN_PANEL_DESC: PicGo Cloud를 PicGo의 공식 이미지 호스팅으로 사용하고 클라우드 기능을 활성화하려면 로그인하세요.
PICGO_CLOUD_LOGIN_FEATURES_TITLE: 클라우드 기능
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_TITLE: 공식 이미지 호스팅
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_DESC: 앨범 클라우드 동기화가 내장된 PicGo의 공식 이미지 호스팅입니다.
PICGO_CLOUD_PAID_PLAN_BADGE: 유료 요금제
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE: 재시작이 필요한가요?
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE: 일부 설정은 적용하려면 재시작이 필요할 수 있습니다. 지금 재시작하시겠습니까?
PICGO_CLOUD_CONFIG_SYNC_RESTART_NOW: 지금 재시작
PICGO_CLOUD_CONFIG_SYNC_RESTART_LATER: 나중에
INPUT_BOX_CONFIRM_MISMATCH: 두 입력값이 일치하지 않습니다.
PICGO_SPONSOR_TEXT: PicGo는 무료 소프트웨어입니다. 마음에 드신다면 커피 한 잔 후원하는 것을 잊지 말아 주세요.
ALIPAY: 알리페이
WECHATPAY: 위챗페이
CHOOSE_PICBED: 이미지 호스팅 선택
COPY_PICBED_CONFIG: 이미지 호스팅 설정 복사
COPY_PICBED_CONFIG_SUCCEED: 이미지 호스팅 설정 복사 성공
INPUT: 입력
CANCEL: 취소
CONFIRM: 확인
CHOOSE_SHOWED_PICBED: 표시할 이미지 호스팅 선택
CHOOSE_PASTE_FORMAT: 붙여넣기 형식 선택
SEARCH: 검색
COPY: 복사
DELETE: 삭제
SELECT_ALL: 전체 선택
CHANGE_IMAGE_URL: 이미지 URL 변경
CHANGE_IMAGE_URL_SUCCEED: 이미지 URL 변경 성공
COPY_LINK_SUCCEED: 링크 복사 성공
BATCH_COPY_LINK_SUCCEED: 링크 일괄 복사 성공
FILE_RENAME: 파일 이름 변경
COPY_FILE_PATH: 파일 경로 복사
OPEN_FILE_PATH: 파일 경로 열기
SUCCESS: 성공
FAILED: 실패
# settings
SETTINGS: 설정
SETTINGS_OPEN_CONFIG_FILE: 설정 파일 열기
SETTINGS_CLICK_TO_OPEN: 클릭하여 열기
SETTINGS_SET_LOG_FILE: 로그 파일 설정
SETTINGS_CLICK_TO_SET: 클릭하여 설정
SETTINGS_CLICK_TO_CHECK: 클릭하여 확인
SETTINGS_SET_SHORTCUT: 단축키 설정
SETTINGS_URL_REWRITE: URL 재작성
SETTINGS_CUSTOM_LINK_FORMAT: 사용자 지정 링크 형식
SETTINGS_SET_PROXY_AND_MIRROR: 프록시 및 미러 설정
SETTINGS_SET_SERVER: 서버 설정
SETTINGS_CHECK_UPDATE: 업데이트 확인
SETTINGS_UPDATE_CHECK_RESULT: "최신 버전은 ${version}이며, ${action}"
SETTINGS_UPDATE_CHECK_NO_UPDATE: 업데이트가 필요하지 않습니다
SETTINGS_UPDATE_CHECK_CAN_UPDATE: 업데이트 가능
SETTINGS_OPEN_UPDATE_HELPER: 업데이트 도우미 열기
SETTINGS_OPEN: 열기
SETTINGS_CLOSE: 닫기
SETTINGS_ACCEPT_BETA_UPDATE: 베타 업데이트 허용
SETTINGS_LAUNCH_ON_BOOT: 부팅 시 자동 실행
SETTINGS_RENAME_BEFORE_UPLOAD: 업로드 전 이름 변경
SETTINGS_TIMESTAMP_RENAME: 타임스탬프로 이름 변경
SETTINGS_OPEN_UPLOAD_TIPS: 업로드 팁 표시
SETTINGS_NOTIFICATION_SOUND: 알림 소리 재생
SETTINGS_MINI_WINDOW_ON_TOP: 미니 창 항상 위
SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: 업로드 후 URL 자동 복사
SETTINGS_TIPS_PLACEHOLDER_URL: $url를 사용하여 URL 위치를 표시
SETTINGS_TIPS_PLACEHOLDER_FILENAME: $fileName을 사용하여 파일 이름 위치를 표시
SETTINGS_TIPS_PLACEHOLDER_EXTNAME: $extName을 사용하여 파일 확장자 위치를 표시
SETTINGS_TIPS_SUCH_AS: "예: $url/$fileName"
SETTINGS_UPLOAD_PROXY: 업로드 프록시
SETTINGS_PLUGIN_INSTALL_PROXY: 플러그인 설치 프록시
SETTINGS_PLUGIN_INSTALL_MIRROR: 플러그인 설치 미러
SETTINGS_CURRENT_VERSION: 현재 버전
SETTINGS_NEWEST_VERSION: 최신 버전
SETTINGS_GETING: 가져오는 중...
SETTINGS_TIPS_HAS_NEW_VERSION: PicGo에 새 버전이 있습니다. 확인을 클릭하여 다운로드 페이지를 여세요
SETTINGS_LOG_FILE: 로그 파일
SETTINGS_LOG_LEVEL: 로그 수준
SETTINGS_LOG_FILE_SIZE: 로그 파일 크기
SETTINGS_SET_PICGO_SERVER: PicGo 서버 설정
SETTINGS_TIPS_SERVER_NOTICE: 서버 기능이 무엇인지 모른다면 문서를 참고하거나 설정을 변경하지 마세요.
SETTINGS_ENABLE_SERVER: 서버 사용
SETTINGS_SET_SERVER_HOST: 서버 호스트 설정
SETTINGS_SET_SERVER_PORT: 서버 포트 설정
SETTINGS_TIP_PLACEHOLDER_HOST: 기본값:127.0.0.1
SETTINGS_TIP_PLACEHOLDER_PORT: 기본값:36677
SETTINGS_LOG_LEVEL_ALL: 전체
SETTINGS_LOG_LEVEL_SUCCESS: 성공
SETTINGS_LOG_LEVEL_ERROR: 오류
SETTINGS_LOG_LEVEL_INFO: 정보
SETTINGS_LOG_LEVEL_WARN: 경고
SETTINGS_LOG_LEVEL_NONE: 없음
SETTINGS_RESULT: 결과
SETTINGS_DEFAULT_PICBED: 기본 이미지 호스팅
SETTINGS_SET_DEFAULT_PICBED: 기본 업로더 설정
SETTINGS_NOT_CONFIG_OPTIONS: 설정 옵션 없음
SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: 내장 클립보드로 업로드 사용
SETTINGS_CHOOSE_LANGUAGE: 언어 선택
UPLOADER_CONFIG_NAME: 설정 이름
BUILTIN_CLIPBOARD_TIPS: 스크립트 대신 내장 클립보드 기능을 사용하여 업로드
# url rewrite
URL_REWRITE_HELP: 업로드된 이미지 URL을 재작성합니다. 규칙은 순서대로 평가되며, 처음 일치한 규칙이 적용됩니다.
URL_REWRITE_ADD_RULE: 규칙 추가
URL_REWRITE_EDIT_RULE: 규칙 편집
URL_REWRITE_EMPTY: 규칙 없음
URL_REWRITE_ORDER: 순서
URL_REWRITE_MATCH: 일치 패턴
URL_REWRITE_REPLACE: 대체 문자열
URL_REWRITE_FLAGS: 플래그
URL_REWRITE_ENABLED: 사용함
URL_REWRITE_ACTIONS: 작업
URL_REWRITE_MOVE_UP: 위로
URL_REWRITE_MOVE_DOWN: 아래로
URL_REWRITE_EDIT: 편집
URL_REWRITE_DELETE: 삭제
URL_REWRITE_DELETE_CONFIRM: 이 규칙을 삭제하시겠습니까?
URL_REWRITE_MATCH_TIPS: 정규식(JavaScript RegExp)을 지원합니다
URL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path
URL_REWRITE_REPLACE_TIPS: 대체 문자열($1, $2, ... 지원)
URL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath
URL_REWRITE_OPTIONS: 옵션
URL_REWRITE_RULE_ENABLED: 이 규칙 사용
URL_REWRITE_FLAG_GLOBAL_LABEL: 전역(g)
URL_REWRITE_FLAG_GLOBAL_DESC: 첫 번째 항목만이 아니라 일치하는 모든 항목을 대체합니다
URL_REWRITE_FLAG_IGNORE_CASE_LABEL: 대소문자 무시(i)
URL_REWRITE_FLAG_IGNORE_CASE_DESC: "대소문자를 구분하지 않고 일치시킵니다(예: JPG와 jpg를 동일하게 취급)"
URL_REWRITE_MATCH_REQUIRED: 일치 패턴은 필수입니다
URL_REWRITE_REPLACE_REQUIRED: 대체 문자열은 필수입니다
URL_REWRITE_INVALID_REGEX: 잘못된 정규식입니다
URL_REWRITE_PREVIEW_TITLE: 미리보기
URL_REWRITE_PREVIEW_TIPS: URL을 입력하면 현재 규칙이 어떻게 재작성하는지 확인할 수 있습니다(순서대로 일치하며, 첫 번째 일치 항목만 적용됩니다)
URL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png
URL_REWRITE_PREVIEW_RUN: 미리보기
URL_REWRITE_PREVIEW_OUTPUT: 출력 URL
URL_REWRITE_PREVIEW_INPUT_REQUIRED: 미리보기할 URL을 입력해 주세요
URL_REWRITE_PREVIEW_RULE_INVALID: 잘못된 규칙입니다
URL_REWRITE_PREVIEW_MATCHED_RULE: 일치한 규칙
URL_REWRITE_PREVIEW_NO_MATCH: 일치하는 규칙 없음
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: 미니 창 열기
SETTINGS_STARTUP_MODE_HIDE: 조용히 시작
# shortcut-page
SHORTCUT_NAME: 단축키 이름
SHORTCUT_BIND: 단축키 바인딩
SHORTCUT_STATUS: 상태
SHORTCUT_ENABLED: 사용함
SHORTCUT_DISABLED: 사용 안 함
SHORTCUT_SOURCE: 소스
SHORTCUT_HANDLE: 처리
SHORTCUT_ENABLE: 사용
SHORTCUT_DISABLE: 사용 안 함
SHORTCUT_EDIT: 편집
SHORTCUT_CHANGE_UPLOAD: 업로드 단축키 변경
# album-page
ALBUM_URL_REWRITE_TITLE: 선택한 이미지 URL 재작성
ALBUM_URL_REWRITE_RESULT_TITLE: 이미지 URL 재작성 결과
ALBUM_URL_REWRITE_WARN_NO_SELECTION: 먼저 사진을 하나 이상 선택해야 합니다
ALBUM_URL_REWRITE_APPLY_GLOBAL_RULES: 전역 URL 재작성 규칙 적용
ALBUM_URL_REWRITE_GLOBAL_RULES_COUNT: 전역 규칙
ALBUM_URL_REWRITE_TEMP_RULE_TIPS: 선택 사항입니다. 임시 규칙을 건너뛰려면 비워 두세요. 임시 규칙은 전역 규칙보다 우선순위가 높습니다.
ALBUM_URL_REWRITE_TEMP_RULE_REQUIRED: 임시 규칙에는 일치 패턴과 대체 문자열이 모두 필요합니다
ALBUM_URL_REWRITE_NO_RULES_TO_APPLY: 사용 중인 전역 규칙이 없고 임시 규칙도 입력되지 않았습니다
ALBUM_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: 임시 규칙을 전역 URL 재작성 규칙에 저장하시겠습니까?
ALBUM_URL_REWRITE_APPLY_AND_SAVE: 적용 및 저장
ALBUM_URL_REWRITE_APPLY_ONLY: 적용만
ALBUM_URL_REWRITE_NO_CHANGES: 변경된 URL이 없습니다
ALBUM_URL_REWRITE_EMPTY_RESULT_WARN: 재작성 결과가 비어 있어 건너뛰었습니다
# tray-page
WAIT_TO_UPLOAD: 업로드 대기 중
ALREADY_UPLOAD: 업로드 완료
# upload-page
PICTURE_UPLOAD: 사진 업로드
DRAG_FILE_TO_HERE: 파일을 여기로 드래그하거나
CLICK_TO_UPLOAD: 클릭하여 업로드
LINK_FORMAT: 링크 형식
CUSTOM: 사용자 지정
CLIPBOARD_PICTURE: 클립보드
TIPS_DRAG_VALID_PICTURE_OR_URL: 유효한 사진 또는 URL을 여기로 드래그하세요
TIPS_INPUT_URL: URL 입력
TIPS_HTTP_PREFIX: http:// 또는 https://로 시작해야 합니다. 여러 URL 지원(줄당 하나씩)
TIPS_INPUT_VALID_URL: 유효한 URL을 입력하세요
# plugins
PLUGIN_SEARCH_PLACEHOLDER: npm에서 picgo 플러그인을 검색하거나, 버튼을 클릭하여 추천 플러그인 목록을 확인하세요
PLUGIN_SEARCH_EXACT_MATCH: 플러그인 이름 정확히 일치
PLUGIN_INSTALL: 설치
PLUGIN_INSTALLING: 설치 중...
PLUGIN_INSTALLED: 설치됨
PLUGIN_DEPRECATED_BADGE: 지원 종료
PLUGIN_DEPRECATED_TITLE: 이 플러그인은 제작자가 지원을 종료했습니다
PLUGIN_DOING_SOMETHING: 처리 중...
PLUGIN_LIST: 플러그인 목록
PLUGIN_IMPORT_LOCAL: 로컬 플러그인 가져오기
# tips
TIPS_REMOVE_LINK: 이 작업은 사진을 앨범에서 제거합니다. 계속하시겠습니까?
TIPS_WILL_REMOVE_CHOOSED_IMAGES: 이 작업은 사진을 앨범에서 제거합니다. 계속하시겠습니까?
TIPS_MUST_CONTAINS_URL: $url, $fileName, $extName 중 하나를 포함해야 합니다
TIPS_NETWORK_ERROR: 네트워크 오류
TIPS_NEED_RELOAD: 앱을 새로고침해야 합니다
TIPS_PLEASE_CHOOSE_LOG_LEVEL: 로그 수준을 선택해 주세요
TIPS_SET_SUCCEED: 설정 성공
TIPS_PLUGIN_NOT_GUI_IMPLEMENT: 이 플러그인은 GUI에 최적화되어 있지 않습니다. 계속하시겠습니까?
TIPS_CLICK_NOTIFICATION_TO_RELOAD: 알림을 클릭하여 앱을 새로고침하세요
TIPS_GET_PLUGIN_LIST_FAILED: 플러그인 목록을 가져오지 못했습니다
# ---renderer i18n end---
# plugins
PLUGIN_INSTALL_SUCCEED: 플러그인 설치 성공
PLUGIN_INSTALL_FAILED: 플러그인 설치 실패
PLUGIN_UNINSTALL_SUCCEED: 플러그인 제거 성공
PLUGIN_UNINSTALL_FAILED: 플러그인 제거 실패
PLUGIN_UPDATE_SUCCEED: 플러그인 업데이트 성공
PLUGIN_UPDATE_FAILED: 플러그인 업데이트 실패
PLUGIN_IMPORT_SUCCEED: 플러그인 가져오기 성공
PLUGIN_IMPORT_FAILED: 플러그인 가져오기 실패
ENABLE_PLUGIN: 플러그인 사용
DISABLE_PLUGIN: 플러그인 사용 안 함
UNINSTALL_PLUGIN: 플러그인 제거
UPDATE_PLUGIN: 플러그인 업데이트
# toolbox
TOOLBOX: 도구 상자
TOOLBOX_TITLE: PicGo 실행 문제 진단
TOOLBOX_SUB_TITLE: 다음 항목을 즉시 검사하여 사용 문제를 해결하세요
TOOLBOX_CHECK_CONFIG_FILE_BROKEN: 설정 파일 손상 여부 확인
TOOLBOX_CHECK_ALBUM_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_ALBUM_FILE_PATH_TIPS: "앨범 파일 경로: ${path}"
TOOLBOX_CHECK_ALBUM_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: 경고
TIPS_ERROR: 오류
TIPS_SKIPPED_INVALID_URLS: "잘못된 URL ${n}개를 건너뛰었습니다. 자세한 내용은 로그를 확인하세요"
TIPS_TOO_MANY_URLS_CONFIRM: "한 번에 URL ${n}개를 업로드하려고 합니다. 지연이 발생할 수 있으니 나누어 업로드하는 것을 권장합니다. 계속하시겠습니까?"
TIPS_NO_VALID_URLS: 유효한 URL을 찾을 수 없습니다
TIPS_INSTALL_NODE_AND_RELOAD_PICGO: Node.js를 설치한 후 PicGo를 재시작해 주세요
TIPS_PLUGIN_REMOVE_ALBUM_ITEM: 플러그인이 앨범에서 일부 이미지를 제거하려고 합니다. 계속하시겠습니까?
TIPS_PLUGIN_OVERWRITE_ALBUM: 플러그인이 앨범을 덮어쓰려고 합니다. 계속하시겠습니까?
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_CUSTOM_CONFIG_FILE_PATH_ERROR: 사용자 지정 설정 파일 구문 분석 오류입니다. 경로 내용을 확인해 주세요
TIPS_SHORTCUT_MODIFIED_SUCCEED: 단축키가 성공적으로 변경되었습니다
TIPS_SHORTCUT_MODIFIED_CONFLICT: 단축키가 충돌합니다. 다시 설정해 주세요
TIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED: 사용자 지정 링크 스타일이 성공적으로 변경되었습니다
TIPS_FIND_NEW_VERSION: "새 버전 ${v}을(를) 찾았습니다. 다양한 새 기능이 추가되었습니다. 최신 버전을 다운로드하시겠습니까?"
TIPS_DELETE_UPLOADER_CONFIG: 이 설정을 삭제하시겠습니까?
TIPS_COPY_UPLOADER_CONFIG: 이 설정을 복사하시겠습니까?
TIPS_UPLOADER_CONFIG_NAME_EMPTY: 설정 이름은 비워 둘 수 없습니다
TIPS_UPLOADER_CONFIG_NOT_FOUND: 설정을 찾을 수 없습니다
TIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST: 마지막 설정은 삭제할 수 없습니다
# privacy
PRIVACY: "이용하기 전에 개인정보 처리방침 ${privacyUrl} 및 이용약관 ${termsUrl}을(를) 읽고 동의해 주세요."
PRIVACY_TIPS: 업로드하려면 개인정보 처리방침에 동의해 주세요
QUIT: 종료
ALBUM_ALL_PHOTOS: 전체 사진
ALBUM_COLLECTIONS: 컬렉션
ALBUM_TAGS: 태그
ALBUM_MENU: 메뉴
ALBUM_OPEN_INSPECTOR: 인스펙터 열기
ALBUM_INSPECTOR_TITLE: 앨범 인스펙터
ALBUM_INSPECTOR_DESCRIPTION: 선택한 이미지를 검사하고 편집합니다.
ALBUM_CLEAR_SELECTION: 선택 해제
ALBUM_SELECTED_COUNT: "${count}개 선택됨"
ALBUM_GRID_VIEW: 그리드 보기
ALBUM_LIST_VIEW: 목록 보기
ALBUM_PREVIEW: 전체 화면 미리보기
ALBUM_PREVIEW_EMPTY: 미리볼 이미지가 없습니다
ALBUM_PREVIEW_PREV: 이전
ALBUM_PREVIEW_NEXT: 다음
ALBUM_PREVIEW_CLOSE: 미리보기 닫기
ALBUM_PREVIEW_COUNT: "${current} / ${total}"
ALBUM_QUICK_ACTIONS: 빠른 작업
ALBUM_EXPORT: 다운로드
ALBUM_URL: URL
ALBUM_BATCH_REWRITE: 일괄 재작성
ALBUM_COLLECTION: 컬렉션
ALBUM_ADD_TAG: 태그 추가
ALBUM_TAG_SUGGESTIONS: 추천 태그
ALBUM_COLUMN_NAME: 이름
ALBUM_COLUMN_PROVIDER: 제공자
ALBUM_COLUMN_SIZE: 크기
ALBUM_COLUMN_DATE: 날짜
ALBUM_ADD: 추가
ALBUM_SOURCE_LOCAL: 로컬
ALBUM_SOURCE_CLOUD: 클라우드
ALBUM_CLOUD_LOAD_FAILED: 클라우드 앨범을 불러오지 못했습니다
ALBUM_CLOUD_LOGIN_REQUIRED_TITLE: 로그인 필요
ALBUM_CLOUD_LOGIN_REQUIRED_DESC: 클라우드 앨범을 사용하려면 유료 PicGo Cloud 계정으로 로그인하세요
ALBUM_CLOUD_LOGIN_BUTTON: 로그인
ALBUM_CLOUD_UPGRADE_TITLE: 업그레이드 필요
ALBUM_CLOUD_UPGRADE_DESC: 클라우드 앨범은 유료 요금제가 필요합니다
ALBUM_CLOUD_UPGRADE_BUTTON: 업그레이드
ALBUM_CLOUD_FEATURES_TITLE: 클라우드 앨범을 사용해야 하는 이유
ALBUM_CLOUD_FEATURE_SYNC: 모든 기기에서 업로드 기록에 접근
ALBUM_CLOUD_FEATURE_AUTO_IMPORT: 새 업로드 정보 자동 가져오기(옵트인 필요)
ALBUM_CLOUD_FEATURE_SECURE: 로컬 기록을 클라우드로 원클릭 가져오기
ALBUM_CLOUD_IMPORT_GUIDE_TITLE: 로컬 기록 가져오기
ALBUM_CLOUD_IMPORT_GUIDE_DESC: 로컬 업로드 기록을 클라우드 앨범으로 가져옵니다. 실제 이미지 파일이 아닌 기록만 가져옵니다.
ALBUM_CLOUD_IMPORT_GUIDE_BUTTON: 가져오기 시작
ALBUM_CLOUD_EMPTY_TITLE: 항목 없음
ALBUM_CLOUD_EMPTY_DESC: 클라우드 앨범이 비어 있습니다. 이미지를 업로드하여 시작하세요.
ALBUM_CLOUD_IMPORTING: 클라우드 앨범으로 가져오는 중...
ALBUM_CLOUD_IMPORT_SUCCESS: "클라우드 앨범으로 ${num}개 항목을 성공적으로 가져왔습니다"
ALBUM_CLOUD_IMPORT_FAILED: 클라우드 앨범으로 가져오지 못했습니다
ALBUM_CLOUD_REFRESH: 새로고침
ALBUM_CLOUD_IMPORT_CONFIRM_TITLE: 자동 가져오기 사용
ALBUM_CLOUD_IMPORT_CONFIRM_DESC: 이 작업을 사용하면 자동 가져오기가 활성화되어 이후 업로드가 클라우드 앨범에 자동으로 기록됩니다. 기존 로컬 기록도 지금 함께 가져옵니다.
ALBUM_CLOUD_DELETE_FAILED: 클라우드 앨범에서 삭제하지 못했습니다
ALBUM_INSPECTOR_DETAILS_TITLE: 상세 정보
ALBUM_INSPECTOR_FILE_NAME: 파일 이름
ALBUM_INSPECTOR_UPLOADER: 업로더
ALBUM_INSPECTOR_CONTENT_TYPE: 콘텐츠 유형
ALBUM_INSPECTOR_CREATED_AT: 생성일
ALBUM_INSPECTOR_UPDATED_AT: 수정일
ALBUM_INSPECTOR_FILE_SIZE: 크기
ALBUM_CLOUD_IMPORT_STATUS_TITLE: 클라우드 앨범
ALBUM_CLOUD_IMPORTED: 클라우드로 가져옴
ALBUM_CLOUD_IMPORTED_TOOLTIP: 이것은 로컬 표시자입니다. 클라우드에서 기록이 삭제되어도 이 상태는 갱신되지 않습니다. 필요하면 다시 가져올 수 있습니다.
ALBUM_CLOUD_REIMPORT: 다시 가져오기
ALBUM_CLOUD_NATIVE: PicGo Cloud를 통해 업로드됨
ALBUM_CLOUD_IMPORT_BUTTON: 클라우드 앨범으로 가져오기
ALBUM_CLOUD_IMPORT_BUTTON_COUNT: "항목 ${num}개를 클라우드 앨범으로 가져오기"
ALBUM_CLOUD_ALL_IMPORTED: 모두 클라우드 앨범에 있음
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_TITLE: 자동 가져오기 사용
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_DESC: 항목을 클라우드 앨범으로 가져오려면 먼저 자동 가져오기를 사용해야 합니다. 지금 사용하고 계속하시겠습니까?
ALBUM_CLOUD_AUTO_IMPORT_ENABLE_AND_IMPORT: 사용 및 가져오기
ALBUM_CLOUD_IMPORT_SINGLE_SUCCESS: 클라우드 앨범으로 성공적으로 가져왔습니다
PICGO_CLOUD_AUTO_IMPORT_LABEL: 클라우드 앨범으로 자동 가져오기
PICGO_CLOUD_AUTO_IMPORT_DESC: 다른 업로더의 업로드 기록을 클라우드 앨범으로 자동 가져오기(PicGo Cloud는 기본적으로 저장됨)
SIDEBAR_PLUGINS: 플러그인
SIDEBAR_SYSTEM: 시스템
SIDEBAR_NOTIFICATIONS: 알림
SIDEBAR_EXPAND: 클릭하여 펼치기
SIDEBAR_COLLAPSE: 사이드바 접기
HISTORY_PANEL_TITLE: 기록
HISTORY_PANEL_FILTER_PLACEHOLDER: 필터...
HISTORY_PANEL_TODAY: 오늘
HISTORY_PANEL_YESTERDAY: 어제
DASHBOARD_NO_VISIBLE_PROVIDERS: 표시할 제공자가 없습니다
DASHBOARD_NO_VISIBLE_PROVIDERS_DESCRIPTION: 모든 제공자가 숨겨져 있습니다. 설정에서 제공자 표시 여부를 변경하세요.
DASHBOARD_OPEN_SETTINGS: 설정 열기
DASHBOARD_DROP_IMAGES_HERE: 파일을 여기로 드롭하세요
DASHBOARD_OR: 또는
DASHBOARD_CLICK_TO_UPLOAD: 클릭하여 업로드
DASHBOARD_PASTE_FROM_CLIPBOARD: 클립보드에서 붙여넣기
DASHBOARD_PASTE_FROM_URL: URL에서 붙여넣기
DASHBOARD_CLIPBOARD: 클립보드
FIELD_IS_REQUIRED: "${field}은(는) 필수입니다"
CONFIG_RENAME: 설정 이름 변경
SETTINGS_SECTION_GENERAL: 일반
SETTINGS_SECTION_APPEARANCE: 외관
SETTINGS_SECTION_UPLOAD_WORKFLOW: 업로드 워크플로
SETTINGS_SECTION_NETWORK: 네트워크
SETTINGS_SECTION_ADVANCED: 고급
SETTINGS_SECTION_ABOUT: 정보
SETTINGS_NO_RESULTS_TITLE: 설정을 찾을 수 없습니다
SETTINGS_NO_RESULTS_DESCRIPTION: 다른 키워드를 시도하거나 검색어를 지워 보세요.
SETTINGS_OPEN_SHORTCUTS: 단축키
SETTINGS_LINK_WEBSITE: 웹사이트
SETTINGS_LINK_GITHUB: GitHub
SETTINGS_LINK_DOCS: 문서
SETTINGS_LINK_PRIVACY: 개인정보 처리방침
SETTINGS_LINK_TERMS: 이용약관
SETTINGS_APPEARANCE_MODE: 외관
SETTINGS_APPEARANCE_MODE_LIGHT: 밝게
SETTINGS_APPEARANCE_MODE_DARK: 어둡게
SETTINGS_APPEARANCE_MODE_AUTO: 자동
COMMON_FOR_EXAMPLE: "예: ${value}"
SETTINGS_SET_DEFAULT_CONFIG: 기본값으로 설정
PROVIDER_DRAFT_CONFIG: 임시 저장
PROVIDER_ACTIVE_UPLOADER_LABEL: 활성
PROVIDER_ACTIVE_UPLOADER_TOOLTIP: "${uploaderName}이(가) 활성 업로더입니다"
PROVIDER_DEFAULT_CONFIG_LABEL: 기본값
PROVIDER_DEFAULT_CONFIG_TOOLTIP: "${uploaderName}이(가) 활성 상태일 때 사용됩니다."
PROVIDER_SIDEBAR_EMPTY: 업로더 또는 설정을 찾을 수 없습니다.
PROVIDER_UPLOADER_ACTIONS: "${uploaderName} 작업"
PROVIDER_SIDEBAR_COLLAPSE: 접기
PROVIDER_SIDEBAR_EXPAND: 펼치기
PROVIDER_CONFIG_ACTIONS: 설정 작업
PROVIDER_CREATE_CONFIG: 설정 생성
PROVIDER_CREATE_CONFIG_DISABLED_EMPTY_SCHEMA: 이 업로더에는 설정 옵션이 없어 추가 설정을 생성할 필요가 없습니다
PROVIDER_INSTALL_MORE_UPLOADERS: 업로더 추가 설치
PROVIDER_NO_UPLOADER_SELECTED: 선택된 업로더가 없습니다.
PROVIDER_NO_CONFIG_YET: 아직 설정이 없습니다
PROVIDER_NO_CONFIG_DESCRIPTION: "${uploaderName}의 스키마 기반 옵션을 편집하려면 새 설정을 생성하세요."
PROVIDER_CONFIGURATION: 설정
PROVIDER_UPDATED_AT_LABEL: 수정일
PROVIDER_DELETE_CONFIG_HINT: 이 설정을 영구적으로 삭제합니다.
UPLOADER_SWITCHER_SELECT_PROVIDER: 제공자 선택
UPLOADER_SWITCHER_CONFIG: 설정
UPLOADER_SWITCHER_NO_CONFIG: 설정 없음
ALBUM_URL_REWRITE_DESCRIPTION: 선택한 이미지의 URL을 일괄 재작성합니다.
ALBUM_URL_REWRITE_CHANGED: 변경됨
ALBUM_URL_REWRITE_UNCHANGED: 변경 없음
TRAY_ALREADY_UPLOAD_EMPTY: 아직 업로드된 이미지가 없습니다
PLUGIN_EMPTY: 플러그인을 찾을 수 없습니다.
PLUGIN_EMPTY_DESCRIPTION: npm을 검색하여 플러그인을 설치해 시작하세요.
PLUGIN_DETAIL: 상세 정보
PLUGIN_NO_CONFIG_SCHEMA: 이 플러그인은 설정 스키마를 제공하지 않습니다.
PLUGIN_NO_TRANSFORMER_SCHEMA: 이 플러그인은 트랜스포머 스키마를 제공하지 않습니다.
PLUGIN_NO_README: 이 플러그인에 대한 README가 없습니다.
NO_OPTIONS_FOUND: 옵션을 찾을 수 없습니다.
-593
View File
@@ -1,593 +0,0 @@
LANG_DISPLAY_LABEL: 中文
ABOUT: 关于
OPEN_MAIN_WINDOW: 打开主窗口
CHOOSE_DEFAULT_PICBED: 选择默认图床
OPEN_UPDATE_HELPER: 打开更新助手
PRIVACY_TERMS_AGREEMENT: 隐私与条款协议
RELOAD_APP: 重启应用
UPLOAD_SUCCEED: 上传成功
UPLOAD_FAILED: 上传失败
UPLOAD_PROGRESS: 上传进度
OPERATION_SUCCEED: 操作成功
OPERATION_FAILED: 操作失败
UPLOADING: 正在上传
QUICK_UPLOAD: 快捷上传
UPLOAD_BY_CLIPBOARD: 剪贴板图片上传
HIDE_WINDOW: 隐藏窗口
SPONSOR_PICGO: 赞助 PicGo
SHOW_PICBED_QRCODE: 生成图床配置二维码
PICBED_QRCODE: 图床配置二维码
ENABLE: 启用
DISABLE: 禁用
CONFIG_THING: 配置${c}
FIND_NEW_VERSION: 发现新版本
NO_MORE_NOTICE: 以后不再提醒
MORE: 更多
SHOW_DEVTOOLS: 打开开发者工具
CURRENT_PICBED: 当前图床
OPEN_TOOLBOX: 打开修复工具箱
# ---renderer i18n begin---
CHOOSE_YOUR_DEFAULT_PICBED: 选择 ${d} 作为你默认图床:
SIDEBAR_DASHBOARD: 仪表盘
UPLOAD_AREA: 上传区
ALBUM: 相册
ALBUM_PROVIDERS: 图床列表
PICBEDS_SETTINGS: 图床设置
PICGO_SETTINGS: PicGo设置
PLUGIN_SETTINGS: 插件设置
DASHBOARD_HISTORY_PANEL_TITLE: 历史面板
PICGO_CLOUD_TITLE: PicGo Cloud
PICGO_CLOUD_DESCRIPTION: PicGo 打造的云端服务,连接你的每一台设备。
PICGO_CLOUD_BRAND_NAME: PicGo Cloud
PICGO_CLOUD_ERROR_TITLE: PicGo Cloud 错误
PICGO_CLOUD_LOADING: 正在加载 PicGo Cloud 状态…
PICGO_CLOUD_NOT_LOGGED_IN: 尚未登录 PicGo Cloud
PICGO_CLOUD_LOGIN: 登录
PICGO_CLOUD_LOGOUT: 退出登录
PICGO_CLOUD_CANCEL_LOGIN: 取消登录
PICGO_CLOUD_RETRY: 重试
PICGO_CLOUD_CONFIG_SYNC: 配置同步
PICGO_CLOUD_LOGIN_IN_PROGRESS: 登录进行中,请在浏览器完成登录。
PICGO_CLOUD_LOGGED_IN_AS: "已登录:${user}"
PICGO_CLOUD_OPEN: 打开 PicGo Cloud
PICGO_CLOUD_LOGIN_TIMEOUT: 登录超时,请重试。
PICGO_CLOUD_LOGIN_FAILED: 登录失败,请重试。
PICGO_CLOUD_AGREE_PREFIX: 我已阅读并同意 PicGo 的
PICGO_CLOUD_TERMS_OF_SERVICE: 服务条款
PICGO_CLOUD_AGREE_AND: 以及
PICGO_CLOUD_PRIVACY_POLICY: 隐私政策
PICGO_CLOUD_LOGIN_EXPIRED: 登录失效,请重新登录。
PICGO_CLOUD_ENCRYPTION_MODE_LABEL: 加密模式
PICGO_CLOUD_ENCRYPTION_MODE_AUTO: 自动
PICGO_CLOUD_ENCRYPTION_MODE_SERVER: 服务端加密
PICGO_CLOUD_ENCRYPTION_MODE_E2E: 端到端加密
PICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO: 自动:跟随云端上一次配置的加密方式(默认服务端加密)。
PICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER: 服务端加密:我们会在服务端对你的配置加密后再存储,不需要 PIN。
PICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E: 端到端加密:需要你输入 PIN 来加密/解密数据。PicGo 不存储 PIN,丢失将无法找回数据。
PICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC: 查看完整文档
PICGO_CLOUD_E2E_CHECKBOX_LABEL: 开启 E2E 加密
PICGO_CLOUD_E2E_ENABLE_WARNING_TITLE: 开启 E2E 加密
PICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE: E2E 加密需要你自行妥善保管 PIN。PIN 丢失将无法恢复加密数据。PicGo 不会存储 PIN 或任何恢复信息。
PICGO_CLOUD_REMOTE_E2E_AUTO_ENABLED: 检测到远端配置已开启 E2E 加密,已自动在本地开启 E2E。
PICGO_CLOUD_E2E_PIN_SETUP_TITLE: 设置 E2E PIN
PICGO_CLOUD_E2E_PIN_DECRYPT_TITLE: 输入 E2E PIN
PICGO_CLOUD_E2E_PIN_RETRY_TITLE: "PIN 错误,请重试(第 ${retryCount} 次)。"
PICGO_CLOUD_E2E_PIN_PLACEHOLDER: PIN
PICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER: 确认 PIN
PICGO_CLOUD_CONFIG_SYNC_SUCCESS: 配置同步成功。
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED: 检测到配置冲突,请选择解决方案。
PICGO_CLOUD_CONFIG_SYNC_FAILED: 配置同步失败。
PICGO_CLOUD_CONFIG_SYNC_ABORTED: 已取消配置同步。
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE: 确认切换加密方式吗?
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY: "您正在从“${from}”切换为“${to}”。\n\n注意:切换加密模式将清空您所有的云端历史版本记录。\n这是因为旧的历史版本无法在新模式下被解密或验证。\n切换后,系统将立即为您创建一份新的备份作为起点。"
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM: 确认切换并清空历史
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL: 取消
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED: 已取消切换加密方式
PICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON: "配置同步失败:${reason}"
PICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY: PIN 错误次数过多,请稍后重试。
PICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID: 本地配置文件格式错误,请检查配置文件。
PICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS: 配置同步进行中…
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING: 存在未解决的冲突,请先处理。
PICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION: 未找到可处理的冲突会话。
PICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE: 请为所有冲突项选择本地或云端。
PICGO_CLOUD_CONFIG_SYNC_STARTING: 配置同步已开始…
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE: "冲突处理(${count} 项)"
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL: 全选本地
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD: 全选云端
PICGO_CLOUD_CONFIG_SYNC_RESET_ALL: 重置全部
PICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION: 本地版本
PICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION: 云端版本
PICGO_CLOUD_CONFIG_SYNC_ABORT: 放弃同步
PICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC: 确认并同步
PICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED: (空)
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED: 配置冲突已解决。
PICGO_CLOUD_LAST_SYNC_TIME: "上次本地同步时间:${time}"
PICGO_CLOUD_LAST_SYNC_TIME_NONE:
PICGO_CLOUD_LAST_SYNC_LABEL: 上次同步
PICGO_CLOUD_PLAN_USAGE_TITLE: 套餐与用量
PICGO_CLOUD_PLAN_USAGE_DESC: 查看当前套餐与用量概览。
PICGO_CLOUD_PLAN_PERIOD_LABEL: 套餐周期
PICGO_CLOUD_STORAGE_LABEL: 存储空间
PICGO_CLOUD_FILES_LABEL: 文件数
PICGO_CLOUD_USAGE_PROGRESS: "${used} / ${total}"
PICGO_CLOUD_USAGE_UNLIMITED: 无限
PICGO_CLOUD_PLAN_PERIOD_RENEWS: 续费日 ${date}
PICGO_CLOUD_PLAN_PERIOD_CANCELS: 失效日 ${date}
PICGO_CLOUD_PLAN_PERIOD_UNTIL: 有效期至 ${date}
PICGO_CLOUD_PLAN_PERIOD_LIFETIME: 长期有效
PICGO_CLOUD_PLAN_PERIOD_GRACE_LABEL: 宽限期至
PICGO_CLOUD_PLAN_PERIOD_GRACE_TOOLTIP: 付费套餐已过期,目前处于宽限期。该日期前 PicGo Cloud 图片仍可正常访问,但配额已临时降级到免费版。大部分付费功能将暂时不可使用。过期后账户将进入冻结状态。
PICGO_CLOUD_PLAN_PERIOD_FROZEN_LABEL: 冻结期至
PICGO_CLOUD_PLAN_PERIOD_FROZEN_TOOLTIP: 账户已冻结,PicGo Cloud 图片无法访问。该日期后数据可能被清理,请尽快续费以恢复权益。
PICGO_CLOUD_QUOTA_DOWNGRADED: 宽限期内配额已临时降级到 ${plan},续费后将恢复
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_TITLE: 套餐已进入宽限期
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_DESC: 你的付费套餐已过期,目前进入 ${days} 天宽限期。期间配额已临时降级到免费版,大部分付费功能将暂时不可使用。请尽快续费以恢复完整权益。
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_TITLE: 账户已冻结
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_DESC: 你的账户已进入冻结期,云端图片暂时无法访问,距离数据被清理还剩 ${days} 天。请立即续费以恢复访问。
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_TITLE: 数据即将清理
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_DESC: 你的账户已进入待清理状态,云端数据即将被永久删除。如需保留数据,请立即续费。
PICGO_CLOUD_LIFECYCLE_BANNER_CTA: 立即续费
PICGO_CLOUD_LIFECYCLE_BANNER_DISMISS: 关闭
PICGO_CLOUD_AUTO_IMPORT_DISABLED_BY_LIFECYCLE: 套餐处于宽限期或冻结期,自动导入已暂停。续费后将恢复。
PICGO_CLOUD_IMAGE_UNAVAILABLE: 图片暂不可用
PICGO_CLOUD_ERROR_GRACE_RESTRICTED: 套餐处于宽限期,此操作暂不可用,请续费后重试。
PICGO_CLOUD_ERROR_ACCOUNT_FROZEN: 账户已冻结,请续费以恢复使用。
PICGO_CLOUD_ERROR_IMPORT_DISABLED: 自动导入已被关闭,请先在 PicGo Cloud 设置中启用。
PICGO_CLOUD_ERROR_PLAN_INELIGIBLE: 当前套餐不支持此功能,请升级后重试。
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_ACTIVE: 已达套餐配额上限,升级到更高套餐可获得更多配额。
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_GRACE: 宽限期内配额已降级到免费版,请续费以恢复完整配额。
PICGO_CLOUD_ERROR_PLAN_REQUIRED: 此功能需要付费套餐。
PICGO_CLOUD_USAGE_LOAD_FAILED: 用量数据加载失败
PICGO_CLOUD_CONFIG_SYNC_LOAD_FAILED: 同步状态加载失败
PICGO_CLOUD_FREE_PLAN_BANNER: 你当前使用免费套餐,升级后可解锁更多配额和高级云功能。
PICGO_CLOUD_VIEW_PLANS: 查看套餐
PICGO_CLOUD_CONFIG_SYNC_TITLE: 配置同步
PICGO_CLOUD_CONFIG_SYNC_CARD_DESC: 在不同设备间安全同步你的配置。
PICGO_CLOUD_SYNC_NOW: 立即同步
PICGO_CLOUD_SYNC_QUOTA_LABEL: 同步配额
PICGO_CLOUD_SYNC_QUOTA_TIP: "当前套餐保留最近 ${limit} 份历史版本。即使显示 ${limit} / ${limit},依然可以同步,每次同步后会自动清理超出范围的旧版本。"
PICGO_CLOUD_LOGIN_PANEL_TITLE: 登录 PicGo Cloud
PICGO_CLOUD_LOGIN_PANEL_DESC: 登录即可使用 PicGo 官方图床,并启用云端相关功能。
PICGO_CLOUD_LOGIN_FEATURES_TITLE: 云端功能
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_TITLE: 官方图床
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_DESC: PicGo 官方图床,内置相册云同步。
PICGO_CLOUD_PAID_PLAN_BADGE: 付费套餐
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE: 需要重启吗?
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE: 部分配置可能需要重启后生效,是否立即重启?
PICGO_CLOUD_CONFIG_SYNC_RESTART_NOW: 立即重启
PICGO_CLOUD_CONFIG_SYNC_RESTART_LATER: 稍后
INPUT_BOX_CONFIRM_MISMATCH: 两次输入不一致,请重新输入。
PICGO_SPONSOR_TEXT: PicGo是免费开源的软件,如果你喜欢它,对你有帮助,不妨请我喝杯咖啡?
ALIPAY: 支付宝
WECHATPAY: 微信支付
CHOOSE_PICBED: 选择图床
COPY_PICBED_CONFIG: 复制图床配置
COPY_PICBED_CONFIG_SUCCEED: 复制图床配置成功
INPUT: 输入框
CANCEL: 取消
CONFIRM: 确定
CHOOSE_SHOWED_PICBED: 请选择显示的图床
CHOOSE_PASTE_FORMAT: 请选择粘贴的格式
SEARCH: 搜索
COPY: 复制
DELETE: 删除
SELECT_ALL: 全选
CHANGE_IMAGE_URL: 修改图片URL
CHANGE_IMAGE_URL_SUCCEED: 修改图片URL成功
COPY_LINK_SUCCEED: 复制链接成功
BATCH_COPY_LINK_SUCCEED: 批量复制链接成功
FILE_RENAME: 文件改名
COPY_FILE_PATH: 复制文件路径
OPEN_FILE_PATH: 打开文件路径
SUCCESS: 成功
FAILED: 失败
# settings
SETTINGS: 设置
SETTINGS_OPEN_CONFIG_FILE: 打开配置文件
SETTINGS_CLICK_TO_OPEN: 点击打开
SETTINGS_SET_LOG_FILE: 设置日志文件
SETTINGS_CLICK_TO_SET: 点击设置
SETTINGS_CLICK_TO_CHECK: 点击检查
SETTINGS_SET_SHORTCUT: 设置快捷键
SETTINGS_URL_REWRITE: URL 重写
SETTINGS_CUSTOM_LINK_FORMAT: 自定义链接格式
SETTINGS_SET_PROXY_AND_MIRROR: 设置代理和镜像地址
SETTINGS_SET_SERVER: 设置Server
SETTINGS_CHECK_UPDATE: 检查更新
SETTINGS_UPDATE_CHECK_RESULT: 最新版本为 ${version}${action}
SETTINGS_UPDATE_CHECK_NO_UPDATE: 无需更新
SETTINGS_UPDATE_CHECK_CAN_UPDATE: 可以更新
SETTINGS_OPEN_UPDATE_HELPER: 打开更新助手
SETTINGS_OPEN:
SETTINGS_CLOSE:
SETTINGS_ACCEPT_BETA_UPDATE: 接受Beta版本更新
SETTINGS_LAUNCH_ON_BOOT: 开机自启
SETTINGS_RENAME_BEFORE_UPLOAD: 上传前重命名
SETTINGS_TIMESTAMP_RENAME: 时间戳重命名
SETTINGS_OPEN_UPLOAD_TIPS: 开启上传提示
SETTINGS_NOTIFICATION_SOUND: 开启通知提示音
SETTINGS_MINI_WINDOW_ON_TOP: Mini窗口置顶
SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: 上传后自动复制URL
SETTINGS_TIPS_PLACEHOLDER_URL: 用占位符 $url 来表示url的位置
SETTINGS_TIPS_PLACEHOLDER_FILENAME: 用占位符 $fileName 来表示文件名的位置
SETTINGS_TIPS_PLACEHOLDER_EXTNAME: 用占位符 $extName 来表示文件格式的位置
SETTINGS_TIPS_SUCH_AS:
SETTINGS_UPLOAD_PROXY: 上传代理
SETTINGS_PLUGIN_INSTALL_PROXY: 插件安装代理
SETTINGS_PLUGIN_INSTALL_MIRROR: 插件安装镜像
SETTINGS_CURRENT_VERSION: 当前版本
SETTINGS_NEWEST_VERSION: 最新版本
SETTINGS_GETING: 正在获取中
SETTINGS_TIPS_HAS_NEW_VERSION: PicGo更新啦,请点击确定打开下载页面
SETTINGS_LOG_FILE: 日志文件
SETTINGS_LOG_LEVEL: 日志记录等级
SETTINGS_LOG_FILE_SIZE: 日志文件大小
SETTINGS_SET_PICGO_SERVER: 设置PicGo-Server
SETTINGS_TIPS_SERVER_NOTICE: 如果你不知道Server的作用,请阅读文档,或者不用修改配置。
SETTINGS_ENABLE_SERVER: 是否开启Server
SETTINGS_SET_SERVER_HOST: 设置监听地址
SETTINGS_SET_SERVER_PORT: 设置监听端口
SETTINGS_TIP_PLACEHOLDER_HOST: 推荐默认地址:127.0.0.1
SETTINGS_TIP_PLACEHOLDER_PORT: 推荐默认端口:36677
SETTINGS_LOG_LEVEL_ALL: 全部-All
SETTINGS_LOG_LEVEL_SUCCESS: 成功-Success
SETTINGS_LOG_LEVEL_ERROR: 错误-Error
SETTINGS_LOG_LEVEL_INFO: 普通-Info
SETTINGS_LOG_LEVEL_WARN: 提醒-Warn
SETTINGS_LOG_LEVEL_NONE: 不记录日志-None
SETTINGS_RESULT: 设置结果
SETTINGS_DEFAULT_PICBED: 设置默认图床
SETTINGS_SET_DEFAULT_PICBED: 设为默认图床
SETTINGS_NOT_CONFIG_OPTIONS: 暂无配置项
SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: 使用内置剪贴板上传
SETTINGS_CHOOSE_LANGUAGE: 选择语言
BUILTIN_CLIPBOARD_TIPS: 使用内置剪贴板函数而不是调用脚本获取剪贴板图片
UPLOADER_CONFIG_NAME: 图床配置名
# url rewrite
URL_REWRITE_HELP: 用于重写上传后的图片 URL。规则按顺序匹配,命中的第一条匹配生效。
URL_REWRITE_ADD_RULE: 新增规则
URL_REWRITE_EDIT_RULE: 编辑规则
URL_REWRITE_EMPTY: 暂无规则
URL_REWRITE_ORDER: 顺序
URL_REWRITE_MATCH: 匹配
URL_REWRITE_REPLACE: 替换
URL_REWRITE_FLAGS: 标志
URL_REWRITE_ENABLED: 启用
URL_REWRITE_ACTIONS: 操作
URL_REWRITE_MOVE_UP: 上移
URL_REWRITE_MOVE_DOWN: 下移
URL_REWRITE_EDIT: 编辑
URL_REWRITE_DELETE: 删除
URL_REWRITE_DELETE_CONFIRM: 确定删除该规则?
URL_REWRITE_MATCH_TIPS: 支持正则(JavaScript RegExp
URL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path
URL_REWRITE_REPLACE_TIPS: 替换内容(支持 $1、$2...
URL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath
URL_REWRITE_OPTIONS: 选项
URL_REWRITE_RULE_ENABLED: 启用该规则
URL_REWRITE_FLAG_GLOBAL_LABEL: 全局(g
URL_REWRITE_FLAG_GLOBAL_DESC: 替换所有找到的内容,而不仅仅是第一个
URL_REWRITE_FLAG_IGNORE_CASE_LABEL: 忽略大小写(i
URL_REWRITE_FLAG_IGNORE_CASE_DESC: 匹配时不区分字母大小写(例如 JPG 等同于 jpg)
URL_REWRITE_MATCH_REQUIRED: 匹配规则不能为空
URL_REWRITE_REPLACE_REQUIRED: 替换内容不能为空
URL_REWRITE_INVALID_REGEX: 正则表达式不合法
URL_REWRITE_PREVIEW_TITLE: 预览
URL_REWRITE_PREVIEW_TIPS: 输入一个 URL,查看当前规则的重写结果(按顺序匹配,仅第一条命中的规则生效)
URL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png
URL_REWRITE_PREVIEW_RUN: 预览
URL_REWRITE_PREVIEW_OUTPUT: 输出 URL
URL_REWRITE_PREVIEW_INPUT_REQUIRED: 请输入要预览的 URL
URL_REWRITE_PREVIEW_RULE_INVALID: 规则无效
URL_REWRITE_PREVIEW_MATCHED_RULE: 命中规则
URL_REWRITE_PREVIEW_NO_MATCH: 没有规则匹配
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
SHORTCUT_NAME: 快捷键名称
SHORTCUT_BIND: 快捷键绑定
SHORTCUT_STATUS: 状态
SHORTCUT_ENABLED: 已启用
SHORTCUT_DISABLED: 已禁用
SHORTCUT_SOURCE: 来源
SHORTCUT_HANDLE: 操作
SHORTCUT_ENABLE: 启用
SHORTCUT_DISABLE: 禁用
SHORTCUT_EDIT: 编辑
SHORTCUT_CHANGE_UPLOAD: 修改上传快捷键
# album-page
ALBUM_URL_REWRITE_TITLE: 重写选中图片 URL
ALBUM_URL_REWRITE_RESULT_TITLE: 重写图片 URL 结果
ALBUM_URL_REWRITE_WARN_NO_SELECTION: 你必须先选中至少一张图片
ALBUM_URL_REWRITE_APPLY_GLOBAL_RULES: 应用全局 URL 重写规则
ALBUM_URL_REWRITE_GLOBAL_RULES_COUNT: 全局规则数量
ALBUM_URL_REWRITE_TEMP_RULE_TIPS: 可选,留空则不使用临时规则(临时规则优先级高于全局规则)。
ALBUM_URL_REWRITE_TEMP_RULE_REQUIRED: 临时规则需要同时填写「匹配」和「替换」
ALBUM_URL_REWRITE_NO_RULES_TO_APPLY: 没有可用的全局规则,且未填写临时规则
ALBUM_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: 是否将临时规则写入全局 URL 重写规则列表?
ALBUM_URL_REWRITE_APPLY_AND_SAVE: 应用并写入
ALBUM_URL_REWRITE_APPLY_ONLY: 仅应用
ALBUM_URL_REWRITE_NO_CHANGES: 没有任何 URL 被修改
ALBUM_URL_REWRITE_EMPTY_RESULT_WARN: 重写结果为空,已跳过
# tray-page
WAIT_TO_UPLOAD: 等待上传
ALREADY_UPLOAD: 已上传
# upload-page
PICTURE_UPLOAD: 图片上传
DRAG_FILE_TO_HERE: 将文件拖拽到此处,或
CLICK_TO_UPLOAD: 点击上传
LINK_FORMAT: 链接格式
CUSTOM: 自定义
CLIPBOARD_PICTURE: 剪贴板图片
TIPS_DRAG_VALID_PICTURE_OR_URL: 请拖入合法的图片文件或者图片URL地址
TIPS_INPUT_URL: 请输入URL
TIPS_HTTP_PREFIX: http:// 或者 https:// 开头,支持上传多条 URL(请换行输入)
TIPS_INPUT_VALID_URL: 请输入合法的URL
# plugins
PLUGIN_SEARCH_PLACEHOLDER: 搜索npm上的PicGo插件,或者点击上方按钮查看优秀插件列表
PLUGIN_SEARCH_EXACT_MATCH: 插件名精确匹配
PLUGIN_INSTALL: 安装
PLUGIN_INSTALLING: 安装中
PLUGIN_INSTALLED: 已安装
PLUGIN_DEPRECATED_BADGE: 已弃用
PLUGIN_DEPRECATED_TITLE: 此插件已被作者标记为弃用
PLUGIN_DOING_SOMETHING: 进行中
PLUGIN_LIST: 插件列表
PLUGIN_IMPORT_LOCAL: 导入本地插件
# tips
TIPS_REMOVE_LINK: 此操作将把该图片移出相册, 是否继续?
TIPS_WILL_REMOVE_CHOOSED_IMAGES: 将在相册中移除刚才选中的 ${m} 张图片,是否继续?
TIPS_MUST_CONTAINS_URL: 必须含有$url 或 $fileName 或 $extName
TIPS_NETWORK_ERROR: 网络错误暂时无法获取
TIPS_NEED_RELOAD: 需要重启生效
TIPS_PLEASE_CHOOSE_LOG_LEVEL: 请选择日志记录等级
TIPS_SET_SUCCEED: 设置成功
TIPS_PLUGIN_NOT_GUI_IMPLEMENT: 该插件未对可视化界面进行优化, 是否继续安装?
TIPS_CLICK_NOTIFICATION_TO_RELOAD: 请点击此通知重启应用以生效
TIPS_GET_PLUGIN_LIST_FAILED: 获取插件列表失败
# ---renderer i18n end---
# plugins
PLUGIN_INSTALL_SUCCEED: 插件安装成功
PLUGIN_INSTALL_FAILED: 插件安装失败
PLUGIN_UNINSTALL_SUCCEED: 插件卸载成功
PLUGIN_UNINSTALL_FAILED: 插件卸载失败
PLUGIN_UPDATE_SUCCEED: 插件更新成功
PLUGIN_UPDATE_FAILED: 插件更新失败
PLUGIN_IMPORT_SUCCEED: 插件导入成功
PLUGIN_IMPORT_FAILED: 插件导入失败
ENABLE_PLUGIN: 启用插件
DISABLE_PLUGIN: 禁用插件
UNINSTALL_PLUGIN: 卸载插件
UPDATE_PLUGIN: 更新插件
# toolbox
TOOLBOX: 工具箱
TOOLBOX_TITLE: 排查 PicGo 运行时问题
TOOLBOX_SUB_TITLE: 立即扫描以下项目,修复使用问题
TOOLBOX_CHECK_CONFIG_FILE_BROKEN: 检查配置文件是否损坏
TOOLBOX_CHECK_ALBUM_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_ALBUM_FILE_PATH_TIPS: 相册文件路径是:${path}
TOOLBOX_CHECK_ALBUM_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: 警告
TIPS_ERROR: 发生错误
TIPS_SKIPPED_INVALID_URLS: 已跳过 ${n} 条非法 URL,请查看日志了解详情
TIPS_TOO_MANY_URLS_CONFIRM: 你将一次上传 ${n} 条 URL,可能会引起卡顿,建议分批上传。是否继续?
TIPS_NO_VALID_URLS: 未检测到合法的 URL
TIPS_INSTALL_NODE_AND_RELOAD_PICGO: 请安装Node.js并重启PicGo再继续操作
TIPS_PLUGIN_REMOVE_ALBUM_ITEM: 有插件正在试图删除一些相册图片,是否继续
TIPS_PLUGIN_OVERWRITE_ALBUM: 有插件正在试图覆盖相册列表,是否继续
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_CUSTOM_CONFIG_FILE_PATH_ERROR: 自定义文件解析出错,请检查路径内容是否正确
TIPS_SHORTCUT_MODIFIED_SUCCEED: 快捷键已经修改成功
TIPS_SHORTCUT_MODIFIED_CONFLICT: 快捷键冲突,请重新设置
TIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED: 自定义链接格式已经修改成功
TIPS_FIND_NEW_VERSION: 发现新版本${v},更新了很多功能,是否去下载最新的版本?
TIPS_DELETE_UPLOADER_CONFIG: 是否要删除这个配置?
TIPS_COPY_UPLOADER_CONFIG: 是否要复制这个配置?
TIPS_UPLOADER_CONFIG_NAME_EMPTY: 配置名称不能为空
TIPS_UPLOADER_CONFIG_NOT_FOUND: 未找到该配置
TIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST: 无法删除最后一个配置
# privacy
PRIVACY: "使用前请阅读并同意隐私政策 ${privacyUrl} 与服务条款 ${termsUrl},同意后方可使用。"
PRIVACY_TIPS: 请同意隐私协议,否则无法上传。
QUIT: 退出
ALBUM_ALL_PHOTOS: 全部照片
ALBUM_COLLECTIONS: 合集
ALBUM_TAGS: 标签
ALBUM_MENU: 菜单
ALBUM_OPEN_INSPECTOR: 打开检查器
ALBUM_INSPECTOR_TITLE: 相册检查器
ALBUM_INSPECTOR_DESCRIPTION: 查看并编辑已选图片。
ALBUM_CLEAR_SELECTION: 取消选择
ALBUM_SELECTED_COUNT: 已选 ${count} 项
ALBUM_GRID_VIEW: 网格视图
ALBUM_LIST_VIEW: 列表视图
ALBUM_PREVIEW: 全屏预览
ALBUM_PREVIEW_EMPTY: 没有可预览的图片
ALBUM_PREVIEW_PREV: 上一张
ALBUM_PREVIEW_NEXT: 下一张
ALBUM_PREVIEW_CLOSE: 关闭预览
ALBUM_PREVIEW_COUNT: ${current} / ${total}
ALBUM_QUICK_ACTIONS: 快速操作
ALBUM_EXPORT: 下载
ALBUM_URL: URL
ALBUM_BATCH_REWRITE: 批量重写
ALBUM_COLLECTION: 合集
ALBUM_ADD_TAG: 添加标签
ALBUM_TAG_SUGGESTIONS: 推荐标签
ALBUM_COLUMN_NAME: 名称
ALBUM_COLUMN_PROVIDER: 图床
ALBUM_COLUMN_SIZE: 大小
ALBUM_COLUMN_DATE: 日期
ALBUM_ADD: 添加
ALBUM_SOURCE_LOCAL: 本地
ALBUM_SOURCE_CLOUD: 云端
ALBUM_CLOUD_LOAD_FAILED: 加载云端相册失败
ALBUM_CLOUD_LOGIN_REQUIRED_TITLE: 需要登录
ALBUM_CLOUD_LOGIN_REQUIRED_DESC: 登录 PicGo Cloud 付费账户以使用云端相册
ALBUM_CLOUD_LOGIN_BUTTON: 前往登录
ALBUM_CLOUD_UPGRADE_TITLE: 需要升级
ALBUM_CLOUD_UPGRADE_DESC: 云端相册需要付费套餐
ALBUM_CLOUD_UPGRADE_BUTTON: 升级套餐
ALBUM_CLOUD_FEATURES_TITLE: 为什么使用云端相册?
ALBUM_CLOUD_FEATURE_SYNC: 多设备访问上传历史记录
ALBUM_CLOUD_FEATURE_AUTO_IMPORT: 上传后自动导入相册记录到云端(需开启该功能)
ALBUM_CLOUD_FEATURE_SECURE: 一键导入本地历史到云端
ALBUM_CLOUD_IMPORT_GUIDE_TITLE: 导入本地记录
ALBUM_CLOUD_IMPORT_GUIDE_DESC: 将本地上传记录导入到云端相册。仅导入记录,图片文件本身不会被上传。
ALBUM_CLOUD_IMPORT_GUIDE_BUTTON: 开始导入
ALBUM_CLOUD_EMPTY_TITLE: 暂无内容
ALBUM_CLOUD_EMPTY_DESC: 云端相册为空,上传图片即可开始使用。
ALBUM_CLOUD_IMPORTING: 正在导入到云端相册...
ALBUM_CLOUD_IMPORT_SUCCESS: 成功导入 ${num} 条记录到云端相册
ALBUM_CLOUD_IMPORT_FAILED: 导入云端相册失败
ALBUM_CLOUD_REFRESH: 刷新
ALBUM_CLOUD_IMPORT_CONFIRM_TITLE: 开启自动导入
ALBUM_CLOUD_IMPORT_CONFIRM_DESC: 此操作将开启自动导入功能,后续上传的图片记录会自动同步到云端相册。同时会将现有的本地记录一并导入。
ALBUM_CLOUD_DELETE_FAILED: 从云端相册删除失败
ALBUM_INSPECTOR_DETAILS_TITLE: 详情
ALBUM_INSPECTOR_FILE_NAME: 文件名
ALBUM_INSPECTOR_UPLOADER: 上传器
ALBUM_INSPECTOR_CONTENT_TYPE: 内容类型
ALBUM_INSPECTOR_CREATED_AT: 创建时间
ALBUM_INSPECTOR_UPDATED_AT: 更新时间
ALBUM_INSPECTOR_FILE_SIZE: 文件大小
ALBUM_CLOUD_IMPORT_STATUS_TITLE: 云端相册
ALBUM_CLOUD_IMPORTED: 已导入云端
ALBUM_CLOUD_IMPORTED_TOOLTIP: 这是本地标记,不会与云端同步。如果云端记录已被删除,此状态不会更新。你可以重新导入。
ALBUM_CLOUD_REIMPORT: 重新导入
ALBUM_CLOUD_NATIVE: 通过 PicGo Cloud 上传
ALBUM_CLOUD_IMPORT_BUTTON: 导入到云端相册
ALBUM_CLOUD_IMPORT_BUTTON_COUNT: 导入 ${num} 条记录到云端相册
ALBUM_CLOUD_ALL_IMPORTED: 已全部在云端相册中
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_TITLE: 需要开启自动导入
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_DESC: 导入到云端相册前需要先开启自动导入功能。是否立即开启并继续导入?
ALBUM_CLOUD_AUTO_IMPORT_ENABLE_AND_IMPORT: 开启并导入
ALBUM_CLOUD_IMPORT_SINGLE_SUCCESS: 已成功导入到云端相册
PICGO_CLOUD_AUTO_IMPORT_LABEL: 自动导入云端相册
PICGO_CLOUD_AUTO_IMPORT_DESC: 自动将其他图床的上传记录导入云端相册(PicGo Cloud 默认已写入)
SIDEBAR_PLUGINS: 插件
SIDEBAR_SYSTEM: 系统
SIDEBAR_NOTIFICATIONS: 通知
SIDEBAR_EXPAND: 点击展开
SIDEBAR_COLLAPSE: 收起侧边栏
HISTORY_PANEL_TITLE: 历史记录
HISTORY_PANEL_FILTER_PLACEHOLDER: 筛选...
HISTORY_PANEL_TODAY: 今天
HISTORY_PANEL_YESTERDAY: 昨天
DASHBOARD_NO_VISIBLE_PROVIDERS: 没有可见图床
DASHBOARD_NO_VISIBLE_PROVIDERS_DESCRIPTION: 当前所有图床都被隐藏了,请前往设置调整可见图床。
DASHBOARD_OPEN_SETTINGS: 打开设置
DASHBOARD_DROP_IMAGES_HERE: 将文件拖拽到此处
DASHBOARD_OR:
DASHBOARD_CLICK_TO_UPLOAD: 点击上传
DASHBOARD_PASTE_FROM_CLIPBOARD: 从剪贴板粘贴
DASHBOARD_PASTE_FROM_URL: 从 URL 粘贴
DASHBOARD_CLIPBOARD: 剪贴板
FIELD_IS_REQUIRED: ${field} 为必填项
CONFIG_RENAME: 配置改名
SETTINGS_SECTION_GENERAL: 通用
SETTINGS_SECTION_APPEARANCE: 外观
SETTINGS_SECTION_UPLOAD_WORKFLOW: 上传流程
SETTINGS_SECTION_NETWORK: 网络
SETTINGS_SECTION_ADVANCED: 高级
SETTINGS_SECTION_ABOUT: 关于
SETTINGS_NO_RESULTS_TITLE: 没有匹配的设置项
SETTINGS_NO_RESULTS_DESCRIPTION: 请尝试其他关键词,或清空搜索条件。
SETTINGS_OPEN_SHORTCUTS: 快捷键
SETTINGS_LINK_WEBSITE: 官网
SETTINGS_LINK_GITHUB: GitHub
SETTINGS_LINK_DOCS: 文档
SETTINGS_LINK_PRIVACY: 隐私协议
SETTINGS_LINK_TERMS: 服务条款
SETTINGS_APPEARANCE_MODE: 主题模式
SETTINGS_APPEARANCE_MODE_LIGHT: 浅色
SETTINGS_APPEARANCE_MODE_DARK: 深色
SETTINGS_APPEARANCE_MODE_AUTO: 跟随系统
COMMON_FOR_EXAMPLE: 例如:${value}
SETTINGS_SET_DEFAULT_CONFIG: 设为默认配置
PROVIDER_DRAFT_CONFIG: 草稿
PROVIDER_ACTIVE_UPLOADER_LABEL: 当前
PROVIDER_ACTIVE_UPLOADER_TOOLTIP: ${uploaderName} 是当前使用的上传器
PROVIDER_DEFAULT_CONFIG_LABEL: 默认
PROVIDER_DEFAULT_CONFIG_TOOLTIP: 当 ${uploaderName} 处于当前激活状态时使用。
PROVIDER_SIDEBAR_EMPTY: 未找到图床或配置。
PROVIDER_UPLOADER_ACTIONS: ${uploaderName} 操作
PROVIDER_SIDEBAR_COLLAPSE: 收起
PROVIDER_SIDEBAR_EXPAND: 展开
PROVIDER_CONFIG_ACTIONS: 配置操作
PROVIDER_CREATE_CONFIG: 新建配置
PROVIDER_CREATE_CONFIG_DISABLED_EMPTY_SCHEMA: 该上传器没有配置项,无需新建配置
PROVIDER_INSTALL_MORE_UPLOADERS: 安装更多图床
PROVIDER_NO_UPLOADER_SELECTED: 尚未选择图床。
PROVIDER_NO_CONFIG_YET: 暂无配置
PROVIDER_NO_CONFIG_DESCRIPTION: 为 ${uploaderName} 新建一个配置后即可开始编辑配置项。
PROVIDER_CONFIGURATION: 配置项
PROVIDER_UPDATED_AT_LABEL: 更新时间
PROVIDER_DELETE_CONFIG_HINT: 该操作会永久删除当前配置。
UPLOADER_SWITCHER_SELECT_PROVIDER: 选择图床
UPLOADER_SWITCHER_CONFIG: 配置
UPLOADER_SWITCHER_NO_CONFIG: 暂无配置
ALBUM_URL_REWRITE_DESCRIPTION: 批量重写所选图片的 URL。
ALBUM_URL_REWRITE_CHANGED: 已修改
ALBUM_URL_REWRITE_UNCHANGED: 未修改
TRAY_ALREADY_UPLOAD_EMPTY: 暂无已上传图片
PLUGIN_EMPTY: 暂未发现插件
PLUGIN_EMPTY_DESCRIPTION: 可以先在上方搜索并安装一些插件
PLUGIN_DETAIL: 详情
PLUGIN_NO_CONFIG_SCHEMA: 该插件未暴露 config 配置项。
PLUGIN_NO_TRANSFORMER_SCHEMA: 该插件未暴露 transformer 配置项。
PLUGIN_NO_README: 该插件暂无 README 内容。
NO_OPTIONS_FOUND: 没有可选项
-593
View File
@@ -1,593 +0,0 @@
LANG_DISPLAY_LABEL: 繁體中文
ABOUT: 關於
OPEN_MAIN_WINDOW: 打開主視窗
CHOOSE_DEFAULT_PICBED: 選擇預設圖床
OPEN_UPDATE_HELPER: 開啟更新助手
PRIVACY_TERMS_AGREEMENT: 隱私與條款協議
RELOAD_APP: 重啟程式
UPLOAD_SUCCEED: 上傳成功
UPLOAD_FAILED: 上傳失敗
UPLOAD_PROGRESS: 上傳進度
OPERATION_SUCCEED: 操作成功
OPERATION_FAILED: 操作失敗
UPLOADING: 正在上傳
QUICK_UPLOAD: 快速上傳
UPLOAD_BY_CLIPBOARD: 剪貼簿圖片上傳
HIDE_WINDOW: 隱藏視窗
SPONSOR_PICGO: 贊助 PicGo
SHOW_PICBED_QRCODE: 產生圖床配置 QRCODE
PICBED_QRCODE: 圖床配置 QRCODE
ENABLE: 啟用
DISABLE: 禁用
CONFIG_THING: 設定${c}
FIND_NEW_VERSION: 發現新版本
NO_MORE_NOTICE: 以後不再提醒
MORE: 更多
SHOW_DEVTOOLS: 開啟開發者工具
CURRENT_PICBED: 當前圖床
OPEN_TOOLBOX: 開啟修復工具箱
# ---renderer i18n begin---
CHOOSE_YOUR_DEFAULT_PICBED: 選擇 ${d} 作為你的預設圖床:
SIDEBAR_DASHBOARD: 儀表板
UPLOAD_AREA: 上傳區
ALBUM: 相簿
ALBUM_PROVIDERS: 圖床列表
PICBEDS_SETTINGS: 圖床設定
PICGO_SETTINGS: PicGo設定
PLUGIN_SETTINGS: 插件設定
DASHBOARD_HISTORY_PANEL_TITLE: 歷史面板
PICGO_CLOUD_TITLE: PicGo Cloud
PICGO_CLOUD_DESCRIPTION: PicGo 打造的雲端服務,連接你的每一台裝置。
PICGO_CLOUD_BRAND_NAME: PicGo Cloud
PICGO_CLOUD_ERROR_TITLE: PicGo Cloud 錯誤
PICGO_CLOUD_LOADING: 正在載入 PicGo Cloud 狀態…
PICGO_CLOUD_NOT_LOGGED_IN: 尚未登入 PicGo Cloud
PICGO_CLOUD_LOGIN: 登入
PICGO_CLOUD_LOGOUT: 登出
PICGO_CLOUD_CANCEL_LOGIN: 取消登入
PICGO_CLOUD_RETRY: 重試
PICGO_CLOUD_CONFIG_SYNC: 配置同步
PICGO_CLOUD_LOGIN_IN_PROGRESS: 登入進行中,請在瀏覽器完成登入。
PICGO_CLOUD_LOGGED_IN_AS: "已登入:${user}"
PICGO_CLOUD_OPEN: 打開 PicGo Cloud
PICGO_CLOUD_LOGIN_TIMEOUT: 登入逾時,請重試。
PICGO_CLOUD_LOGIN_FAILED: 登入失敗,請重試。
PICGO_CLOUD_AGREE_PREFIX: 我已閱讀並同意 PicGo 的
PICGO_CLOUD_TERMS_OF_SERVICE: 服務條款
PICGO_CLOUD_AGREE_AND: 以及
PICGO_CLOUD_PRIVACY_POLICY: 隱私政策
PICGO_CLOUD_LOGIN_EXPIRED: 登入失效,請重新登入。
PICGO_CLOUD_ENCRYPTION_MODE_LABEL: 加密模式
PICGO_CLOUD_ENCRYPTION_MODE_AUTO: 自動
PICGO_CLOUD_ENCRYPTION_MODE_SERVER: 服務端加密
PICGO_CLOUD_ENCRYPTION_MODE_E2E: 端到端加密
PICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO: 自動:跟隨雲端上一次配置的加密方式(預設服務端加密)。
PICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER: 服務端加密:我們會在服務端對你的配置加密後再儲存,不需要 PIN。
PICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E: 端到端加密:需要你輸入 PIN 來加密/解密資料。PicGo 不儲存 PIN,遺失將無法找回資料。
PICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC: 查看完整文檔
PICGO_CLOUD_E2E_CHECKBOX_LABEL: 開啟 E2E 加密
PICGO_CLOUD_E2E_ENABLE_WARNING_TITLE: 開啟 E2E 加密
PICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE: E2E 加密需要你自行妥善保管 PIN。PIN 遺失將無法恢復加密資料。PicGo 不會儲存 PIN 或任何恢復資訊。
PICGO_CLOUD_REMOTE_E2E_AUTO_ENABLED: 偵測到遠端配置已開啟 E2E 加密,已自動在本地開啟 E2E。
PICGO_CLOUD_E2E_PIN_SETUP_TITLE: 設定 E2E PIN
PICGO_CLOUD_E2E_PIN_DECRYPT_TITLE: 輸入 E2E PIN
PICGO_CLOUD_E2E_PIN_RETRY_TITLE: "PIN 錯誤,請重試(第 ${retryCount} 次)。"
PICGO_CLOUD_E2E_PIN_PLACEHOLDER: PIN
PICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER: 確認 PIN
PICGO_CLOUD_CONFIG_SYNC_SUCCESS: 配置同步成功。
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED: 偵測到配置衝突,請選擇解決方案。
PICGO_CLOUD_CONFIG_SYNC_FAILED: 配置同步失敗。
PICGO_CLOUD_CONFIG_SYNC_ABORTED: 已取消配置同步。
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE: 確認切換加密方式嗎?
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY: "您正在從「${from}」切換為「${to}」。\n\n注意:切換加密模式將清空您所有的雲端歷史版本記錄。\n這是因為舊的歷史版本無法在新模式下被解密或驗證。\n切換後,系統將立即為您建立一份新的備份作為起點。"
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM: 確認切換並清空歷史
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL: 取消
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED: 已取消切換加密方式
PICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON: "配置同步失敗:${reason}"
PICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY: PIN 錯誤次數過多,請稍後重試。
PICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID: 本地配置檔格式錯誤,請檢查配置檔。
PICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS: 配置同步進行中…
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING: 存在未解決的衝突,請先處理。
PICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION: 未找到可處理的衝突工作階段。
PICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE: 請為所有衝突項選擇本地或雲端。
PICGO_CLOUD_CONFIG_SYNC_STARTING: 配置同步已開始…
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE: "衝突處理(${count} 項)"
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL: 全選本地
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD: 全選雲端
PICGO_CLOUD_CONFIG_SYNC_RESET_ALL: 重置全部
PICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION: 本地版本
PICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION: 雲端版本
PICGO_CLOUD_CONFIG_SYNC_ABORT: 放棄同步
PICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC: 確認並同步
PICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED: (空)
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED: 配置衝突已解決。
PICGO_CLOUD_LAST_SYNC_TIME: "上次本地同步時間:${time}"
PICGO_CLOUD_LAST_SYNC_TIME_NONE:
PICGO_CLOUD_LAST_SYNC_LABEL: 上次同步
PICGO_CLOUD_PLAN_USAGE_TITLE: 方案與用量
PICGO_CLOUD_PLAN_USAGE_DESC: 查看目前方案與用量概覽。
PICGO_CLOUD_PLAN_PERIOD_LABEL: 方案週期
PICGO_CLOUD_STORAGE_LABEL: 儲存空間
PICGO_CLOUD_FILES_LABEL: 檔案數
PICGO_CLOUD_USAGE_PROGRESS: "${used} / ${total}"
PICGO_CLOUD_USAGE_UNLIMITED: 無限
PICGO_CLOUD_PLAN_PERIOD_RENEWS: 續費日 ${date}
PICGO_CLOUD_PLAN_PERIOD_CANCELS: 失效日 ${date}
PICGO_CLOUD_PLAN_PERIOD_UNTIL: 有效期至 ${date}
PICGO_CLOUD_PLAN_PERIOD_LIFETIME: 長期有效
PICGO_CLOUD_PLAN_PERIOD_GRACE_LABEL: 寬限期至
PICGO_CLOUD_PLAN_PERIOD_GRACE_TOOLTIP: 付費套餐已過期,目前處於寬限期。該日期前 PicGo Cloud 圖片仍可正常存取,但配額已暫時降級到免費版。大部分付費功能將暫時不可使用。過期後帳戶將進入凍結狀態。
PICGO_CLOUD_PLAN_PERIOD_FROZEN_LABEL: 凍結期至
PICGO_CLOUD_PLAN_PERIOD_FROZEN_TOOLTIP: 帳戶已凍結,PicGo Cloud 圖片無法存取。該日期後資料可能被清理,請儘快續費以恢復權益。
PICGO_CLOUD_QUOTA_DOWNGRADED: 寬限期內配額已暫時降級到 ${plan},續費後將恢復
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_TITLE: 套餐已進入寬限期
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_DESC: 你的付費套餐已過期,目前進入 ${days} 天寬限期。期間配額已暫時降級到免費版,大部分付費功能將暫時不可使用。請儘快續費以恢復完整權益。
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_TITLE: 帳戶已凍結
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_DESC: 你的帳戶已進入凍結期,雲端圖片暫時無法存取,距離資料被清理還剩 ${days} 天。請立即續費以恢復存取。
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_TITLE: 資料即將清理
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_DESC: 你的帳戶已進入待清理狀態,雲端資料即將被永久刪除。如需保留資料,請立即續費。
PICGO_CLOUD_LIFECYCLE_BANNER_CTA: 立即續費
PICGO_CLOUD_LIFECYCLE_BANNER_DISMISS: 關閉
PICGO_CLOUD_AUTO_IMPORT_DISABLED_BY_LIFECYCLE: 套餐處於寬限期或凍結期,自動匯入已暫停。續費後將恢復。
PICGO_CLOUD_IMAGE_UNAVAILABLE: 圖片暫時無法顯示
PICGO_CLOUD_ERROR_GRACE_RESTRICTED: 套餐處於寬限期,此操作暫不可用,請續費後重試。
PICGO_CLOUD_ERROR_ACCOUNT_FROZEN: 帳戶已凍結,請續費以恢復使用。
PICGO_CLOUD_ERROR_IMPORT_DISABLED: 自動匯入已被關閉,請先在 PicGo Cloud 設定中啟用。
PICGO_CLOUD_ERROR_PLAN_INELIGIBLE: 目前方案不支援此功能,請升級後重試。
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_ACTIVE: 已達方案配額上限,升級到更高方案可獲得更多配額。
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_GRACE: 寬限期內配額已降級到免費版,請續費以恢復完整配額。
PICGO_CLOUD_ERROR_PLAN_REQUIRED: 此功能需要付費方案。
PICGO_CLOUD_USAGE_LOAD_FAILED: 用量資料載入失敗
PICGO_CLOUD_CONFIG_SYNC_LOAD_FAILED: 同步狀態載入失敗
PICGO_CLOUD_FREE_PLAN_BANNER: 你目前使用免費方案,升級後可解鎖更多配額與進階雲端功能。
PICGO_CLOUD_VIEW_PLANS: 查看方案
PICGO_CLOUD_CONFIG_SYNC_TITLE: 配置同步
PICGO_CLOUD_CONFIG_SYNC_CARD_DESC: 在不同裝置間安全同步你的配置。
PICGO_CLOUD_SYNC_NOW: 立即同步
PICGO_CLOUD_SYNC_QUOTA_LABEL: 同步配額
PICGO_CLOUD_SYNC_QUOTA_TIP: "目前方案保留最近 ${limit} 份歷史版本。即使顯示 ${limit} / ${limit},依然可以同步,每次同步後會自動清理超出範圍的舊版本。"
PICGO_CLOUD_LOGIN_PANEL_TITLE: 登入 PicGo Cloud
PICGO_CLOUD_LOGIN_PANEL_DESC: 登入即可使用 PicGo 官方圖床,並啟用雲端相關功能。
PICGO_CLOUD_LOGIN_FEATURES_TITLE: 雲端功能
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_TITLE: 官方圖床
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_DESC: PicGo 官方圖床,內建相簿雲同步。
PICGO_CLOUD_PAID_PLAN_BADGE: 付費方案
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE: 需要重啟嗎?
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE: 部分配置可能需要重啟後生效,是否立即重啟?
PICGO_CLOUD_CONFIG_SYNC_RESTART_NOW: 立即重啟
PICGO_CLOUD_CONFIG_SYNC_RESTART_LATER: 稍後
INPUT_BOX_CONFIRM_MISMATCH: 兩次輸入不一致,請重新輸入。
PICGO_SPONSOR_TEXT: PicGo是開放原始碼的軟體,如果你喜歡它,對你有幫助,不妨請我喝杯咖啡?
ALIPAY: 支付寶
WECHATPAY: 微信支付
CHOOSE_PICBED: 選擇圖床
COPY_PICBED_CONFIG: 複製圖床設定
COPY_PICBED_CONFIG_SUCCEED: 複製圖床設定成功
INPUT: 輸入框
CANCEL: 取消
CONFIRM: 確定
CHOOSE_SHOWED_PICBED: 請選擇顯示的圖床
CHOOSE_PASTE_FORMAT: 請選擇貼上的格式
SEARCH: 搜尋
COPY: 複製
DELETE: 刪除
SELECT_ALL: 全選
CHANGE_IMAGE_URL: 修改圖片URL
CHANGE_IMAGE_URL_SUCCEED: 修改圖片URL成功
COPY_LINK_SUCCEED: 複製連結成功
BATCH_COPY_LINK_SUCCEED: 批量複製連結成功
FILE_RENAME: 檔案改名
COPY_FILE_PATH: 複製檔案路徑
OPEN_FILE_PATH: 打開檔案路徑
SUCCESS: 成功
FAILED: 失敗
# settings
SETTINGS: 設定
SETTINGS_OPEN_CONFIG_FILE: 打開設定檔案
SETTINGS_CLICK_TO_OPEN: 點擊打開
SETTINGS_SET_LOG_FILE: 設定記錄檔案
SETTINGS_CLICK_TO_SET: 點擊設定
SETTINGS_CLICK_TO_CHECK: 點擊檢查
SETTINGS_SET_SHORTCUT: 設定快捷鍵
SETTINGS_URL_REWRITE: URL 重寫
SETTINGS_CUSTOM_LINK_FORMAT: 自訂連結格式
SETTINGS_SET_PROXY_AND_MIRROR: 設定PROXY和鏡像地址
SETTINGS_SET_SERVER: 設定Server
SETTINGS_CHECK_UPDATE: 檢查更新
SETTINGS_UPDATE_CHECK_RESULT: 最新版本為 ${version}${action}
SETTINGS_UPDATE_CHECK_NO_UPDATE: 無需更新
SETTINGS_UPDATE_CHECK_CAN_UPDATE: 可以更新
SETTINGS_OPEN_UPDATE_HELPER: 打開更新助手
SETTINGS_OPEN:
SETTINGS_CLOSE:
SETTINGS_ACCEPT_BETA_UPDATE: 接受Beta版本更新
SETTINGS_LAUNCH_ON_BOOT: 開機時啟動
SETTINGS_RENAME_BEFORE_UPLOAD: 上傳前重新命名
SETTINGS_TIMESTAMP_RENAME: 以時間戳命名
SETTINGS_OPEN_UPLOAD_TIPS: 開啟上傳提示
SETTINGS_NOTIFICATION_SOUND: 開啟通知提示音
SETTINGS_MINI_WINDOW_ON_TOP: Mini視窗置頂
SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: 上傳後自動複製URL
SETTINGS_TIPS_PLACEHOLDER_URL: 用佔位符 $url 來表示URL的位置
SETTINGS_TIPS_PLACEHOLDER_FILENAME: 用佔位符 $fileName 來表示檔案名稱的位置
SETTINGS_TIPS_PLACEHOLDER_EXTNAME: 用佔位符 $extName 來表示檔案格式的位置
SETTINGS_TIPS_SUCH_AS:
SETTINGS_UPLOAD_PROXY: 上傳PROXY
SETTINGS_PLUGIN_INSTALL_PROXY: 插件安裝PROXY
SETTINGS_PLUGIN_INSTALL_MIRROR: 插件安裝鏡像
SETTINGS_CURRENT_VERSION: 當前版本
SETTINGS_NEWEST_VERSION: 最新版本
SETTINGS_GETING: 正在取得中
SETTINGS_TIPS_HAS_NEW_VERSION: PicGo更新啦,請點擊確定開啟下載頁面
SETTINGS_LOG_FILE: 記錄檔案
SETTINGS_LOG_LEVEL: 記錄等级
SETTINGS_LOG_FILE_SIZE: 記錄檔案大小
SETTINGS_SET_PICGO_SERVER: 設定PicGo-Server
SETTINGS_TIPS_SERVER_NOTICE: 如果你不知道Server的作用,請閱讀文檔,或者不用修改設定。
SETTINGS_ENABLE_SERVER: 是否開啟Server
SETTINGS_SET_SERVER_HOST: 設定監聽地址
SETTINGS_SET_SERVER_PORT: 設定監聽端口
SETTINGS_TIP_PLACEHOLDER_HOST: 推薦預設地址:127.0.0.1
SETTINGS_TIP_PLACEHOLDER_PORT: 推薦預設端口:36677
SETTINGS_LOG_LEVEL_ALL: 全部-All
SETTINGS_LOG_LEVEL_SUCCESS: 成功-Success
SETTINGS_LOG_LEVEL_ERROR: 錯誤-Error
SETTINGS_LOG_LEVEL_INFO: 普通-Info
SETTINGS_LOG_LEVEL_WARN: 提醒-Warn
SETTINGS_LOG_LEVEL_NONE: 不記錄-None
SETTINGS_RESULT: 設定結果
SETTINGS_DEFAULT_PICBED: 設定預設圖床
SETTINGS_SET_DEFAULT_PICBED: 設為預設圖床
SETTINGS_NOT_CONFIG_OPTIONS: 暫無設定選項
SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: 使用內建剪貼簿上傳
SETTINGS_CHOOSE_LANGUAGE: 選擇語言
BUILTIN_CLIPBOARD_TIPS: 使用內建剪貼簿函數而不是調用腳本取得剪貼簿內的照片
UPLOADER_CONFIG_NAME: 圖床配置名
# url rewrite
URL_REWRITE_HELP: 用於重寫上傳後的圖片 URL。規則按順序匹配,命中的第一條匹配生效。
URL_REWRITE_ADD_RULE: 新增規則
URL_REWRITE_EDIT_RULE: 編輯規則
URL_REWRITE_EMPTY: 暫無規則
URL_REWRITE_ORDER: 順序
URL_REWRITE_MATCH: 匹配
URL_REWRITE_REPLACE: 替換
URL_REWRITE_FLAGS: 標誌
URL_REWRITE_ENABLED: 啟用
URL_REWRITE_ACTIONS: 操作
URL_REWRITE_MOVE_UP: 上移
URL_REWRITE_MOVE_DOWN: 下移
URL_REWRITE_EDIT: 編輯
URL_REWRITE_DELETE: 刪除
URL_REWRITE_DELETE_CONFIRM: 確定刪除該規則?
URL_REWRITE_MATCH_TIPS: 支援正則(JavaScript RegExp
URL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path
URL_REWRITE_REPLACE_TIPS: 替換內容(支援 $1、$2...
URL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath
URL_REWRITE_OPTIONS: 選項
URL_REWRITE_RULE_ENABLED: 啟用該規則
URL_REWRITE_FLAG_GLOBAL_LABEL: 全域(g
URL_REWRITE_FLAG_GLOBAL_DESC: 替換所有找到的內容,而不僅僅是第一個
URL_REWRITE_FLAG_IGNORE_CASE_LABEL: 忽略大小寫(i
URL_REWRITE_FLAG_IGNORE_CASE_DESC: 匹配時不區分字母大小寫(例如 JPG 等同於 jpg)
URL_REWRITE_MATCH_REQUIRED: 匹配規則不能為空
URL_REWRITE_REPLACE_REQUIRED: 替換內容不能為空
URL_REWRITE_INVALID_REGEX: 正則表達式不合法
URL_REWRITE_PREVIEW_TITLE: 預覽
URL_REWRITE_PREVIEW_TIPS: 輸入一個 URL,查看目前規則的重寫結果(按順序匹配,僅第一條命中的規則生效)
URL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png
URL_REWRITE_PREVIEW_RUN: 預覽
URL_REWRITE_PREVIEW_OUTPUT: 輸出 URL
URL_REWRITE_PREVIEW_INPUT_REQUIRED: 請輸入要預覽的 URL
URL_REWRITE_PREVIEW_RULE_INVALID: 規則無效
URL_REWRITE_PREVIEW_MATCHED_RULE: 命中規則
URL_REWRITE_PREVIEW_NO_MATCH: 沒有規則匹配
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
SHORTCUT_NAME: 快捷鍵名稱
SHORTCUT_BIND: 快捷鍵綁定
SHORTCUT_STATUS: 狀態
SHORTCUT_ENABLED: 已啟用
SHORTCUT_DISABLED: 已禁用
SHORTCUT_SOURCE: 來源
SHORTCUT_HANDLE: 操作
SHORTCUT_ENABLE: 啟用
SHORTCUT_DISABLE: 禁用
SHORTCUT_EDIT: 編輯
SHORTCUT_CHANGE_UPLOAD: 修改上傳快捷鍵
# album-page
ALBUM_URL_REWRITE_TITLE: 重寫選中圖片 URL
ALBUM_URL_REWRITE_RESULT_TITLE: 重寫圖片 URL 結果
ALBUM_URL_REWRITE_WARN_NO_SELECTION: 你必須先選中至少一張圖片
ALBUM_URL_REWRITE_APPLY_GLOBAL_RULES: 套用全域 URL 重寫規則
ALBUM_URL_REWRITE_GLOBAL_RULES_COUNT: 全域規則數量
ALBUM_URL_REWRITE_TEMP_RULE_TIPS: 可選,留空則不使用臨時規則(臨時規則優先級高於全域規則)。
ALBUM_URL_REWRITE_TEMP_RULE_REQUIRED: 臨時規則需要同時填寫「匹配」和「替換」
ALBUM_URL_REWRITE_NO_RULES_TO_APPLY: 沒有可用的全域規則,且未填寫臨時規則
ALBUM_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: 是否將臨時規則寫入全域 URL 重寫規則列表?
ALBUM_URL_REWRITE_APPLY_AND_SAVE: 套用並寫入
ALBUM_URL_REWRITE_APPLY_ONLY: 僅套用
ALBUM_URL_REWRITE_NO_CHANGES: 沒有任何 URL 被修改
ALBUM_URL_REWRITE_EMPTY_RESULT_WARN: 重寫結果為空,已跳過
# tray-page
WAIT_TO_UPLOAD: 等待上傳
ALREADY_UPLOAD: 已上傳
# upload-page
PICTURE_UPLOAD: 圖片上傳
DRAG_FILE_TO_HERE: 將檔案拖曳到此處,或
CLICK_TO_UPLOAD: 點擊上傳
LINK_FORMAT: 連結格式
CUSTOM: 自訂
CLIPBOARD_PICTURE: 剪貼簿圖片
TIPS_DRAG_VALID_PICTURE_OR_URL: 請拖入合法的圖片檔案或者圖片URL地址
TIPS_INPUT_URL: 請輸入URL
TIPS_HTTP_PREFIX: http:// 或者 https:// 開頭,支援上傳多條 URL(請換行輸入)
TIPS_INPUT_VALID_URL: 請輸入合法的URL
# plugins
PLUGIN_SEARCH_PLACEHOLDER: 搜尋npm上的PicGo插件,或者點擊上方按鈕查看優秀插件列表
PLUGIN_SEARCH_EXACT_MATCH: 插件名精確匹配
PLUGIN_INSTALL: 安裝
PLUGIN_INSTALLING: 安裝中
PLUGIN_INSTALLED: 已安裝
PLUGIN_DEPRECATED_BADGE: 已棄用
PLUGIN_DEPRECATED_TITLE: 此插件已被作者標記為棄用
PLUGIN_DOING_SOMETHING: 進行中
PLUGIN_LIST: 插件列表
PLUGIN_IMPORT_LOCAL: 導入本地插件
# tips
TIPS_REMOVE_LINK: 此操作將在相簿中移除該圖片,是否繼續?
TIPS_WILL_REMOVE_CHOOSED_IMAGES: 將在相簿中移除剛才選中的 ${m} 張圖片,是否繼續?
TIPS_MUST_CONTAINS_URL: 必須含有$url 或 $fileName 或 $extName
TIPS_NETWORK_ERROR: 網路錯誤,暫時無法取得
TIPS_NEED_RELOAD: 需要重新啟動生效
TIPS_PLEASE_CHOOSE_LOG_LEVEL: 請選擇記錄等級
TIPS_SET_SUCCEED: 設定成功
TIPS_PLUGIN_NOT_GUI_IMPLEMENT: 該插件未對GUI進行優化,是否繼續安裝?
TIPS_CLICK_NOTIFICATION_TO_RELOAD: 請點擊此通知重新啟動程式以生效
TIPS_GET_PLUGIN_LIST_FAILED: 取得插件列表失敗
# ---renderer i18n end---
# plugins
PLUGIN_INSTALL_SUCCEED: 插件安裝成功
PLUGIN_INSTALL_FAILED: 插件安裝失敗
PLUGIN_UNINSTALL_SUCCEED: 插件卸載成功
PLUGIN_UNINSTALL_FAILED: 插件卸載失敗
PLUGIN_UPDATE_SUCCEED: 插件更新成功
PLUGIN_UPDATE_FAILED: 插件更新失敗
PLUGIN_IMPORT_SUCCEED: 插件導入成功
PLUGIN_IMPORT_FAILED: 插件導入失敗
ENABLE_PLUGIN: 啟用插件
DISABLE_PLUGIN: 禁用插件
UNINSTALL_PLUGIN: 卸載插件
UPDATE_PLUGIN: 更新插件
# toolbox
TOOLBOX: 工具箱
TOOLBOX_TITLE: 排查 PicGo 執行時問題
TOOLBOX_SUB_TITLE: 立即掃描以下項目,修復使用問題
TOOLBOX_CHECK_CONFIG_FILE_BROKEN: 檢查配置文件是否損壞
TOOLBOX_CHECK_ALBUM_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_ALBUM_FILE_PATH_TIPS: 相冊文件路徑是:${path}
TOOLBOX_CHECK_ALBUM_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: 警告
TIPS_ERROR: 發生錯誤
TIPS_SKIPPED_INVALID_URLS: 已跳過 ${n} 條非法 URL,請查看日誌了解詳情
TIPS_TOO_MANY_URLS_CONFIRM: 你將一次上傳 ${n} 條 URL,可能會引起卡頓,建議分批上傳。是否繼續?
TIPS_NO_VALID_URLS: 未偵測到合法的 URL
TIPS_INSTALL_NODE_AND_RELOAD_PICGO: 請安裝Node.js並重新啟動PicGo再繼續操作
TIPS_PLUGIN_REMOVE_ALBUM_ITEM: 有插件正在試圖刪除一些相簿圖片,是否繼續?
TIPS_PLUGIN_OVERWRITE_ALBUM: 有插件正在試圖覆蓋相簿列表,是否繼續?
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_CUSTOM_CONFIG_FILE_PATH_ERROR: 自訂設定檔案解析出錯,請檢查路徑內容是否正確
TIPS_SHORTCUT_MODIFIED_SUCCEED: 快捷鍵已經修改成功
TIPS_SHORTCUT_MODIFIED_CONFLICT: 快捷鍵衝突,請重新設定
TIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED: 自訂連結格式已經修改成功
TIPS_FIND_NEW_VERSION: 發現新版本${v},更新了很多功能,是否去下載最新的版本?
TIPS_DELETE_UPLOADER_CONFIG: 是否要刪除這個配置?
TIPS_COPY_UPLOADER_CONFIG: 是否要複製這個配置?
TIPS_UPLOADER_CONFIG_NAME_EMPTY: 配置名稱不能為空
TIPS_UPLOADER_CONFIG_NOT_FOUND: 未找到該配置
TIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST: 無法刪除最後一個配置
# privacy
PRIVACY: "使用前請閱讀並同意隱私政策 ${privacyUrl} 與服務條款 ${termsUrl},同意後方可使用。"
PRIVACY_TIPS: 請同意隱私協議,否則無法上傳。
QUIT: 退出
ALBUM_ALL_PHOTOS: 全部相片
ALBUM_COLLECTIONS: 合輯
ALBUM_TAGS: 標籤
ALBUM_MENU: 選單
ALBUM_OPEN_INSPECTOR: 開啟檢查器
ALBUM_INSPECTOR_TITLE: 相簿檢查器
ALBUM_INSPECTOR_DESCRIPTION: 檢視並編輯已選圖片。
ALBUM_CLEAR_SELECTION: 取消選取
ALBUM_SELECTED_COUNT: 已選 ${count} 項
ALBUM_GRID_VIEW: 網格檢視
ALBUM_LIST_VIEW: 清單檢視
ALBUM_PREVIEW: 全螢幕預覽
ALBUM_PREVIEW_EMPTY: 沒有可預覽的圖片
ALBUM_PREVIEW_PREV: 上一張
ALBUM_PREVIEW_NEXT: 下一張
ALBUM_PREVIEW_CLOSE: 關閉預覽
ALBUM_PREVIEW_COUNT: ${current} / ${total}
ALBUM_QUICK_ACTIONS: 快速操作
ALBUM_EXPORT: 下載
ALBUM_URL: URL
ALBUM_BATCH_REWRITE: 批次重寫
ALBUM_COLLECTION: 合輯
ALBUM_ADD_TAG: 新增標籤
ALBUM_TAG_SUGGESTIONS: 建議標籤
ALBUM_COLUMN_NAME: 名稱
ALBUM_COLUMN_PROVIDER: 圖床
ALBUM_COLUMN_SIZE: 大小
ALBUM_COLUMN_DATE: 日期
ALBUM_ADD: 新增
ALBUM_SOURCE_LOCAL: 本機
ALBUM_SOURCE_CLOUD: 雲端
ALBUM_CLOUD_LOAD_FAILED: 載入雲端相簿失敗
ALBUM_CLOUD_LOGIN_REQUIRED_TITLE: 需要登入
ALBUM_CLOUD_LOGIN_REQUIRED_DESC: 登入 PicGo Cloud 付費帳戶以使用雲端相簿
ALBUM_CLOUD_LOGIN_BUTTON: 前往登入
ALBUM_CLOUD_UPGRADE_TITLE: 需要升級
ALBUM_CLOUD_UPGRADE_DESC: 雲端相簿需要付費方案
ALBUM_CLOUD_UPGRADE_BUTTON: 升級方案
ALBUM_CLOUD_FEATURES_TITLE: 為什麼使用雲端相簿?
ALBUM_CLOUD_FEATURE_SYNC: 多裝置存取上傳歷史記錄
ALBUM_CLOUD_FEATURE_AUTO_IMPORT: 上傳後自動匯入相簿記錄到雲端(需開啟該功能)
ALBUM_CLOUD_FEATURE_SECURE: 一鍵匯入本機歷史到雲端
ALBUM_CLOUD_IMPORT_GUIDE_TITLE: 匯入本機記錄
ALBUM_CLOUD_IMPORT_GUIDE_DESC: 將本機上傳記錄匯入到雲端相簿。僅匯入記錄,圖片檔案本身不會被上傳。
ALBUM_CLOUD_IMPORT_GUIDE_BUTTON: 開始匯入
ALBUM_CLOUD_EMPTY_TITLE: 暫無內容
ALBUM_CLOUD_EMPTY_DESC: 雲端相簿為空,上傳圖片即可開始使用。
ALBUM_CLOUD_IMPORTING: 正在匯入到雲端相簿...
ALBUM_CLOUD_IMPORT_SUCCESS: 成功匯入 ${num} 條記錄到雲端相簿
ALBUM_CLOUD_IMPORT_FAILED: 匯入雲端相簿失敗
ALBUM_CLOUD_REFRESH: 重新整理
ALBUM_CLOUD_IMPORT_CONFIRM_TITLE: 開啟自動匯入
ALBUM_CLOUD_IMPORT_CONFIRM_DESC: 此操作將開啟自動匯入功能,後續上傳的圖片記錄會自動同步到雲端相簿。同時會將現有的本地記錄一併匯入。
ALBUM_CLOUD_DELETE_FAILED: 從雲端相簿刪除失敗
ALBUM_INSPECTOR_DETAILS_TITLE: 詳情
ALBUM_INSPECTOR_FILE_NAME: 檔案名稱
ALBUM_INSPECTOR_UPLOADER: 上傳器
ALBUM_INSPECTOR_CONTENT_TYPE: 內容類型
ALBUM_INSPECTOR_CREATED_AT: 建立時間
ALBUM_INSPECTOR_UPDATED_AT: 更新時間
ALBUM_INSPECTOR_FILE_SIZE: 檔案大小
ALBUM_CLOUD_IMPORT_STATUS_TITLE: 雲端相簿
ALBUM_CLOUD_IMPORTED: 已匯入雲端
ALBUM_CLOUD_IMPORTED_TOOLTIP: 這是本機標記,不會與雲端同步。如果雲端記錄已被刪除,此狀態不會更新。你可以重新匯入。
ALBUM_CLOUD_REIMPORT: 重新匯入
ALBUM_CLOUD_NATIVE: 透過 PicGo Cloud 上傳
ALBUM_CLOUD_IMPORT_BUTTON: 匯入到雲端相簿
ALBUM_CLOUD_IMPORT_BUTTON_COUNT: 匯入 ${num} 條記錄到雲端相簿
ALBUM_CLOUD_ALL_IMPORTED: 已全部在雲端相簿中
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_TITLE: 需要開啟自動匯入
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_DESC: 匯入到雲端相簿前需要先開啟自動匯入功能。是否立即開啟並繼續匯入?
ALBUM_CLOUD_AUTO_IMPORT_ENABLE_AND_IMPORT: 開啟並匯入
ALBUM_CLOUD_IMPORT_SINGLE_SUCCESS: 已成功匯入到雲端相簿
PICGO_CLOUD_AUTO_IMPORT_LABEL: 自動匯入雲端相簿
PICGO_CLOUD_AUTO_IMPORT_DESC: 自動將其他圖床的上傳記錄匯入雲端相簿(PicGo Cloud 預設已寫入)
SIDEBAR_PLUGINS: 插件
SIDEBAR_SYSTEM: 系統
SIDEBAR_NOTIFICATIONS: 通知
SIDEBAR_EXPAND: 點擊展開
SIDEBAR_COLLAPSE: 收合側邊欄
HISTORY_PANEL_TITLE: 歷史記錄
HISTORY_PANEL_FILTER_PLACEHOLDER: 篩選...
HISTORY_PANEL_TODAY: 今天
HISTORY_PANEL_YESTERDAY: 昨天
DASHBOARD_NO_VISIBLE_PROVIDERS: 沒有可見圖床
DASHBOARD_NO_VISIBLE_PROVIDERS_DESCRIPTION: 目前所有圖床都被隱藏了,請前往設定調整可見圖床。
DASHBOARD_OPEN_SETTINGS: 打開設定
DASHBOARD_DROP_IMAGES_HERE: 將檔案拖曳到此處
DASHBOARD_OR:
DASHBOARD_CLICK_TO_UPLOAD: 點擊上傳
DASHBOARD_PASTE_FROM_CLIPBOARD: 從剪貼簿貼上
DASHBOARD_PASTE_FROM_URL: 從 URL 貼上
DASHBOARD_CLIPBOARD: 剪貼簿
FIELD_IS_REQUIRED: ${field} 為必填項
CONFIG_RENAME: 配置改名
SETTINGS_SECTION_GENERAL: 一般
SETTINGS_SECTION_APPEARANCE: 外觀
SETTINGS_SECTION_UPLOAD_WORKFLOW: 上傳流程
SETTINGS_SECTION_NETWORK: 網路
SETTINGS_SECTION_ADVANCED: 進階
SETTINGS_SECTION_ABOUT: 關於
SETTINGS_NO_RESULTS_TITLE: 沒有符合的設定項
SETTINGS_NO_RESULTS_DESCRIPTION: 請嘗試其他關鍵字,或清空搜尋條件。
SETTINGS_OPEN_SHORTCUTS: 快捷鍵
SETTINGS_LINK_WEBSITE: 官網
SETTINGS_LINK_GITHUB: GitHub
SETTINGS_LINK_DOCS: 文件
SETTINGS_LINK_PRIVACY: 隱私政策
SETTINGS_LINK_TERMS: 服務條款
SETTINGS_APPEARANCE_MODE: 主題模式
SETTINGS_APPEARANCE_MODE_LIGHT: 淺色
SETTINGS_APPEARANCE_MODE_DARK: 深色
SETTINGS_APPEARANCE_MODE_AUTO: 跟隨系統
COMMON_FOR_EXAMPLE: 例如:${value}
SETTINGS_SET_DEFAULT_CONFIG: 設為預設配置
PROVIDER_DRAFT_CONFIG: 草稿
PROVIDER_ACTIVE_UPLOADER_LABEL: 目前
PROVIDER_ACTIVE_UPLOADER_TOOLTIP: ${uploaderName} 是目前使用的上傳器
PROVIDER_DEFAULT_CONFIG_LABEL: 預設
PROVIDER_DEFAULT_CONFIG_TOOLTIP: 當 ${uploaderName} 處於目前啟用狀態時使用。
PROVIDER_SIDEBAR_EMPTY: 找不到圖床或配置。
PROVIDER_UPLOADER_ACTIONS: ${uploaderName} 操作
PROVIDER_SIDEBAR_COLLAPSE: 收合
PROVIDER_SIDEBAR_EXPAND: 展開
PROVIDER_CONFIG_ACTIONS: 配置操作
PROVIDER_CREATE_CONFIG: 新增配置
PROVIDER_CREATE_CONFIG_DISABLED_EMPTY_SCHEMA: 該上傳器沒有配置項,無需新增配置
PROVIDER_INSTALL_MORE_UPLOADERS: 安裝更多圖床
PROVIDER_NO_UPLOADER_SELECTED: 尚未選擇圖床。
PROVIDER_NO_CONFIG_YET: 尚無配置
PROVIDER_NO_CONFIG_DESCRIPTION: 為 ${uploaderName} 建立一個新配置後即可開始編輯配置項。
PROVIDER_CONFIGURATION: 配置項
PROVIDER_UPDATED_AT_LABEL: 更新時間
PROVIDER_DELETE_CONFIG_HINT: 此操作會永久刪除目前配置。
UPLOADER_SWITCHER_SELECT_PROVIDER: 選擇圖床
UPLOADER_SWITCHER_CONFIG: 配置
UPLOADER_SWITCHER_NO_CONFIG: 暫無配置
ALBUM_URL_REWRITE_DESCRIPTION: 批量重寫所選圖片的 URL。
ALBUM_URL_REWRITE_CHANGED: 已修改
ALBUM_URL_REWRITE_UNCHANGED: 未修改
TRAY_ALREADY_UPLOAD_EMPTY: 暫無已上傳圖片
PLUGIN_EMPTY: 尚未找到任何插件
PLUGIN_EMPTY_DESCRIPTION: 可以先在上方搜尋並安裝一些插件
PLUGIN_DETAIL: 詳情
PLUGIN_NO_CONFIG_SCHEMA: 該插件未暴露 config 配置項。
PLUGIN_NO_TRANSFORMER_SCHEMA: 該插件未暴露 transformer 配置項。
PLUGIN_NO_README: 該插件暫無 README 內容。
NO_OPTIONS_FOUND: 沒有可選項
+1
View File
@@ -6,6 +6,7 @@
<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>
+13 -29
View File
@@ -1,34 +1,18 @@
#!/bin/sh
if [ "$XDG_SESSION_TYPE" = "x11" ]; then
# require xclip(see http://stackoverflow.com/questions/592620/check-if-a-program-exists-from-a-bash-script/677212#677212)
command -v xclip >/dev/null 2>&1 || { echo >&1 "no xclip"; exit 1; }
# write image in clipboard to file (see http://unix.stackexchange.com/questions/145131/copy-image-from-clipboard-to-file)
filePath=`xclip -selection clipboard -o 2>/dev/null | grep ^file:// | cut -c8-`
if [ ! -n "$filePath" ] ;then
if
xclip -selection clipboard -target image/png -o >/dev/null 2>&1
then
xclip -selection clipboard -target image/png -o >$1 2>/dev/null
echo $1
else
echo "no image"
fi
# require xclip(see http://stackoverflow.com/questions/592620/check-if-a-program-exists-from-a-bash-script/677212#677212)
command -v xclip >/dev/null 2>&1 || { echo >&1 "no xclip"; exit 1; }
# write image in clipboard to file (see http://unix.stackexchange.com/questions/145131/copy-image-from-clipboard-to-file)
filePath=`xclip -selection clipboard -o 2>/dev/null | grep ^file:// | cut -c8-`
if [ ! -n "$filePath" ] ;then
if
xclip -selection clipboard -target image/png -o >/dev/null 2>&1
then
xclip -selection clipboard -target image/png -o >$1 2>/dev/null
echo $1
else
echo $filePath
fi
elif [ "$XDG_SESSION_TYPE" = "wayland" ]; then
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 "no image"
exit 1
echo "no image"
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
echo $filePath
fi
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

-1
View File
@@ -27,7 +27,6 @@ function main {
$stream.Dispose() | out-null
$imagePath
Exit 1
}
try {
-18
View File
@@ -1,18 +0,0 @@
#!/bin/sh
# grab the paths
scriptPath=$(echo $0 | awk '{ print substr( $0, 1, length($0)-6 ) }')"windows10.ps1"
imagePath=$(echo $1 | awk '{ print substr( $0, 1, length($0)-18 ) }')
imageName=$(echo $1 | awk '{ print substr( $0, length($0)-17, length($0) ) }')
# run the powershell script
res=$(powershell.exe -noprofile -noninteractive -nologo -sta -executionpolicy unrestricted -file $(wslpath -w $scriptPath) $(wslpath -w $imagePath)"\\"$imageName)
# note that there is a return symbol in powershell result
noImage=$(echo "no image\r")
# check whether image exists
if [ "$res" = "$noImage" ] ;then
echo "no image"
else
echo $(wslpath -u $res)
fi
-66
View File
@@ -1,66 +0,0 @@
// different platform has different format
// macos (dmg, x64 + arm64)
const darwin = [{
appNameWithPrefix: 'PicGo',
ext: 'dmg',
arch: 'arm64',
'version-file': 'latest-mac.yml'
}, {
appNameWithPrefix: 'PicGo',
ext: 'dmg',
arch: 'x64',
'version-file': 'latest-mac.yml'
}]
// linux (AppImage, deb, snap)
const linux = [{
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: '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 (nsis, x64 + ia32 + arm64)
const win32 = [{
appNameWithPrefix: 'PicGo',
ext: 'exe',
arch: 'ia32',
'version-file': 'latest.yml'
}, {
appNameWithPrefix: 'PicGo',
ext: 'exe',
arch: 'x64',
'version-file': 'latest.yml'
}, {
appNameWithPrefix: 'PicGo',
ext: 'exe',
arch: 'arm64',
'version-file': 'latest.yml'
}]
module.exports = {
darwin,
linux,
win32
}
-39
View File
@@ -1,39 +0,0 @@
const pkg = require('../package.json')
const version = pkg.version
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 macos = [
{ label: 'Intel', arch: '-x64', ext: '.dmg' },
{ label: 'Apple Silicon', arch: '-arm64', ext: '.dmg' }
]
const linux = {
AppImage: [
{ label: '64 bit', arch: '-amd64', ext: '.AppImage' },
{ label: 'ARM64', arch: '-arm64', ext: '.AppImage' }
],
Deb: [
{ label: '64 bit', arch: '-amd64', ext: '.deb' },
{ label: 'ARM64', arch: '-arm64', ext: '.deb' }
],
Snap: [
{ label: '64 bit', arch: '-amd64', 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'))
-197
View File
@@ -1,197 +0,0 @@
/**
* Merge artifacts from different platforms and architectures
* Also merge latest*.yml files for electron-updater
*/
const fs = require('fs')
const path = require('path')
const yaml = require('js-yaml')
const ARTIFACTS_DIR = path.join(__dirname, '../artifacts')
const DIST_DIR = path.join(__dirname, '../dist')
// yml 文件分组规则
const YML_MERGE_RULES = {
// macOS: 合并 x64 和 arm64 的 latest-mac.yml
'latest-mac.yml': ['latest-mac.yml'],
// Windows: 合并所有架构的 latest.yml
'latest.yml': ['latest.yml'],
// Linux x64: latest-linux.yml
'latest-linux.yml': ['latest-linux.yml'],
// Linux arm64: latest-linux-arm64.yml
'latest-linux-arm64.yml': ['latest-linux-arm64.yml']
}
/**
* 递归查找指定文件名的所有文件
*/
function findFiles(dir, filename) {
const results = []
if (!fs.existsSync(dir)) {
return results
}
const items = fs.readdirSync(dir)
for (const item of items) {
const fullPath = path.join(dir, item)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
results.push(...findFiles(fullPath, filename))
} else if (item === filename) {
results.push(fullPath)
}
}
return results
}
/**
* 合并多个 yml 文件
*/
function mergeYmlFiles(files) {
if (files.length === 0) return null
if (files.length === 1) {
return yaml.load(fs.readFileSync(files[0], 'utf8'))
}
const contents = files.map(f => yaml.load(fs.readFileSync(f, 'utf8')))
// 以第一个为基准,合并 files 数组
const merged = {
version: contents[0].version,
files: [],
releaseDate: contents[0].releaseDate
}
for (const content of contents) {
if (content.files && Array.isArray(content.files)) {
merged.files.push(...content.files)
}
}
// 去重(根据 sha512
const seen = new Set()
merged.files = merged.files.filter(file => {
const key = file.sha512
if (seen.has(key)) return false
seen.add(key)
return true
})
// 设置 path/sha512/size 为第一个文件(electron-updater 兼容性)
if (merged.files.length > 0) {
merged.path = merged.files[0].url
merged.sha512 = merged.files[0].sha512
merged.size = merged.files[0].size
}
return merged
}
/**
* 复制所有构建产物到 dist 目录
*/
function copyArtifacts() {
console.log('📁 Copying all artifacts to dist...\n')
if (!fs.existsSync(ARTIFACTS_DIR)) {
console.log('⚠️ No artifacts directory found')
return
}
const platformDirs = fs.readdirSync(ARTIFACTS_DIR)
for (const platformDir of platformDirs) {
const platformPath = path.join(ARTIFACTS_DIR, platformDir)
const stat = fs.statSync(platformPath)
if (!stat.isDirectory()) continue
console.log(`📦 Processing ${platformDir}...`)
const files = fs.readdirSync(platformPath)
for (const file of files) {
const srcPath = path.join(platformPath, file)
const destPath = path.join(DIST_DIR, file)
const fileStat = fs.statSync(srcPath)
// 跳过目录和 yml 文件(yml 文件会单独处理合并)
if (fileStat.isDirectory()) continue
if (file.endsWith('.yml')) continue
// 如果目标文件已存在且大小相同,跳过
if (fs.existsSync(destPath)) {
const destStat = fs.statSync(destPath)
if (destStat.size === fileStat.size) {
console.log(` ⏭️ Skipped (exists): ${file}`)
continue
}
}
fs.copyFileSync(srcPath, destPath)
console.log(` ✅ Copied: ${file}`)
}
}
}
/**
* 合并 yml 文件
*/
function mergeYmlFilesFromArtifacts() {
console.log('\n🔀 Merging yml files...\n')
for (const [outputName, sourceNames] of Object.entries(YML_MERGE_RULES)) {
const allFiles = []
for (const sourceName of sourceNames) {
const files = findFiles(ARTIFACTS_DIR, sourceName)
allFiles.push(...files)
}
if (allFiles.length === 0) {
console.log(`⏭️ No ${outputName} found, skipping...`)
continue
}
console.log(`📄 Found ${allFiles.length} ${outputName} file(s):`)
allFiles.forEach(f => console.log(` - ${path.relative(ARTIFACTS_DIR, f)}`))
const merged = mergeYmlFiles(allFiles)
if (merged) {
const outputPath = path.join(DIST_DIR, outputName)
fs.writeFileSync(outputPath, yaml.dump(merged, { lineWidth: -1 }))
console.log(`✅ Merged -> ${outputName}`)
if (merged.files) {
console.log(` Files: ${merged.files.map(f => f.url).join(', ')}`)
}
console.log('')
}
}
}
async function main() {
console.log('🚀 Starting artifact merge process...\n')
// 确保 dist 目录存在
if (!fs.existsSync(DIST_DIR)) {
fs.mkdirSync(DIST_DIR, { recursive: true })
}
// 1. 复制所有构建产物
copyArtifacts()
// 2. 合并 yml 文件
mergeYmlFilesFromArtifacts()
console.log('🎉 Artifact merge completed!')
}
main().catch(err => {
console.error('❌ Error:', err)
process.exit(1)
})
-39
View File
@@ -1,39 +0,0 @@
require('dotenv').config()
const { notarize } = require('@electron/notarize')
const { APPLE_ID, APPLE_TEAM_ID, APPLE_APP_SPECIFIC_PASSWORD } = process.env
const APP_BUNDLE_ID = 'com.molunerfinn.picgo'
async function main(context) {
const { electronPlatformName, appOutDir, packager } = context
if (
electronPlatformName !== 'darwin' ||
!APPLE_ID ||
!APPLE_APP_SPECIFIC_PASSWORD ||
!APPLE_TEAM_ID
) {
console.log('Skip notarization.')
return
}
const appName = packager.appInfo.productFilename
const appPath = `${appOutDir}/${appName}.app`
const now = Date.now()
console.log('Starting Apple notarization for', appPath)
await notarize({
appPath,
appBundleId: APP_BUNDLE_ID,
appleId: APPLE_ID,
appleIdPassword: APPLE_APP_SPECIFIC_PASSWORD,
teamId: APPLE_TEAM_ID
})
console.log('Finished Apple notarization for', appPath, `in ${(Date.now() - now) / 1000}s`)
}
module.exports = main
-335
View File
@@ -1,335 +0,0 @@
const fs = require('fs')
const http = require('http')
const https = require('https')
const path = require('path')
const API_URL = process.env.ATOM_API_URL || 'https://api.atomgit.com/api/v5'
const ATOM_PAT = process.env.ATOM_PAT
const ATOM_OWNER = process.env.ATOM_OWNER
const ATOM_REPO = process.env.ATOM_REPO
const RELEASE_TAG = process.env.RELEASE_TAG
const TARGET_COMMITISH = process.env.ATOM_TARGET_COMMITISH
const DIST_DIR = path.resolve(process.env.DIST_DIR || 'dist')
const RELEASE_METADATA_PATH = path.resolve(process.env.GITHUB_RELEASE_JSON || 'github-release.json')
const MAX_ATTEMPTS = 3
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504])
const UPLOAD_TIMEOUT_MINUTES = Number.parseInt(process.env.ATOM_UPLOAD_TIMEOUT_MINUTES || '70', 10)
function requireEnvironmentVariable(name, value) {
if (!value) {
throw new Error(`Missing required environment variable: ${name}`)
}
}
function encodePathSegment(value) {
return encodeURIComponent(value)
}
function wait(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
function formatFileSize(bytes) {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
}
function formatDuration(milliseconds) {
return `${Math.round(milliseconds / 1000)}s`
}
async function requestWithRetry(url, createOptions, label) {
let lastError
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
try {
const response = await fetch(url, createOptions())
if (!RETRYABLE_STATUS_CODES.has(response.status) || attempt === MAX_ATTEMPTS) {
return response
}
await response.text()
console.warn(`[AtomGit] ${label} returned ${response.status}; retrying (${attempt}/${MAX_ATTEMPTS})`)
} catch (error) {
lastError = error
if (attempt === MAX_ATTEMPTS) {
throw error
}
console.warn(`[AtomGit] ${label} failed; retrying (${attempt}/${MAX_ATTEMPTS})`)
}
await wait(1000 * attempt)
}
throw lastError || new Error(`${label} failed after ${MAX_ATTEMPTS} attempts`)
}
function atomGitApiUrl(relativePath) {
return `${API_URL}/repos/${encodePathSegment(ATOM_OWNER)}/${encodePathSegment(ATOM_REPO)}${relativePath}`
}
function atomGitHeaders(additionalHeaders = {}) {
return {
Authorization: `Bearer ${ATOM_PAT}`,
Accept: 'application/json',
...additionalHeaders
}
}
function parseResponseText(text) {
if (!text) {
return null
}
try {
return JSON.parse(text)
} catch {
return text
}
}
async function readResponseBody(response) {
return parseResponseText(await response.text())
}
function formatResponseBody(body) {
return typeof body === 'string' ? body : JSON.stringify(body)
}
async function assertSuccessfulResponse(response, label) {
const body = await readResponseBody(response)
if (!response.ok) {
throw new Error(`${label} failed with HTTP ${response.status}: ${formatResponseBody(body)}`)
}
return body
}
async function verifyCredentials() {
const response = await requestWithRetry(
`${API_URL}/user`,
() => ({ headers: atomGitHeaders() }),
'credential verification'
)
await assertSuccessfulResponse(response, 'AtomGit credential verification')
}
async function getRelease() {
const response = await requestWithRetry(
atomGitApiUrl(`/releases/tags/${encodePathSegment(RELEASE_TAG)}`),
() => ({ headers: atomGitHeaders() }),
`get release ${RELEASE_TAG}`
)
if (response.status === 404) {
await response.text()
return null
}
return assertSuccessfulResponse(response, `Get AtomGit release ${RELEASE_TAG}`)
}
async function createRelease(metadata) {
const name = metadata.name || RELEASE_TAG
const payload = {
tag_name: RELEASE_TAG,
name,
body: metadata.body || `Release ${name}`,
release_status: metadata.isPrerelease ? 'pre' : 'latest'
}
const targetCommitish = TARGET_COMMITISH || metadata.targetCommitish
if (targetCommitish) {
payload.target_commitish = targetCommitish
}
const response = await requestWithRetry(
atomGitApiUrl('/releases'),
() => ({
method: 'POST',
headers: atomGitHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload)
}),
`create release ${RELEASE_TAG}`
)
if (response.status === 409) {
await response.text()
return getRelease()
}
return assertSuccessfulResponse(response, `Create AtomGit release ${RELEASE_TAG}`)
}
async function getOrCreateRelease(metadata) {
const existingRelease = await getRelease()
if (existingRelease) {
console.log(`[AtomGit] Release ${RELEASE_TAG} already exists`)
return existingRelease
}
console.log(`[AtomGit] Creating release ${RELEASE_TAG}`)
const createdRelease = await createRelease(metadata)
if (!createdRelease) {
throw new Error(`AtomGit release ${RELEASE_TAG} was not available after creation`)
}
return createdRelease
}
async function getUploadRequest(fileName) {
const url = new URL(atomGitApiUrl(`/releases/${encodePathSegment(RELEASE_TAG)}/upload_url`))
url.searchParams.set('file_name', fileName)
const response = await requestWithRetry(
url,
() => ({ headers: atomGitHeaders() }),
`request upload URL for ${fileName}`
)
const body = await assertSuccessfulResponse(response, `Get upload URL for ${fileName}`)
if (!body || typeof body !== 'object' || typeof body.url !== 'string' || !body.headers || typeof body.headers !== 'object') {
throw new Error(`AtomGit returned an invalid upload request for ${fileName}`)
}
return body
}
function putAssetBuffer(url, headers, fileBuffer) {
return new Promise((resolve, reject) => {
const targetUrl = new URL(url)
const requestClient = targetUrl.protocol === 'http:' ? http : https
const request = requestClient.request(targetUrl, {
method: 'PUT',
headers: Object.fromEntries(headers.entries())
}, response => {
const chunks = []
response.on('data', chunk => {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
})
response.on('end', () => {
resolve({
status: response.statusCode || 0,
body: Buffer.concat(chunks).toString('utf8')
})
})
response.on('error', reject)
})
request.setTimeout(UPLOAD_TIMEOUT_MINUTES * 60 * 1000, () => {
request.destroy(new Error(`Upload request timed out after ${UPLOAD_TIMEOUT_MINUTES} minutes`))
})
request.on('error', reject)
request.end(fileBuffer)
})
}
async function uploadAsset(filePath) {
const fileName = path.basename(filePath)
const fileBuffer = await fs.promises.readFile(filePath)
const fileSize = fileBuffer.byteLength
const startedAt = Date.now()
console.log(`[AtomGit] Uploading ${fileName} (${formatFileSize(fileSize)})`)
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
const uploadRequest = await getUploadRequest(fileName)
const uploadHeaders = new Headers(uploadRequest.headers)
uploadHeaders.set('Content-Length', String(fileSize))
let response
try {
response = await putAssetBuffer(uploadRequest.url, uploadHeaders, fileBuffer)
} catch (error) {
if (attempt === MAX_ATTEMPTS) {
throw error
}
console.warn(`[AtomGit] Upload ${fileName} failed; requesting a new upload URL (${attempt}/${MAX_ATTEMPTS})`)
await wait(1000 * attempt)
continue
}
if (response.status >= 200 && response.status < 300) {
console.log(`[AtomGit] Uploaded ${fileName} in ${formatDuration(Date.now() - startedAt)}`)
return
}
const responseBody = parseResponseText(response.body)
if (!RETRYABLE_STATUS_CODES.has(response.status) || attempt === MAX_ATTEMPTS) {
throw new Error(`Upload ${fileName} failed with HTTP ${response.status}: ${formatResponseBody(responseBody)}`)
}
console.warn(`[AtomGit] Upload ${fileName} returned ${response.status}; requesting a new upload URL (${attempt}/${MAX_ATTEMPTS})`)
await wait(1000 * attempt)
}
}
function readReleaseMetadata() {
const metadata = JSON.parse(fs.readFileSync(RELEASE_METADATA_PATH, 'utf8'))
if (metadata.isDraft) {
throw new Error(`GitHub release ${RELEASE_TAG} is still a draft and will not be mirrored`)
}
if (metadata.tagName !== RELEASE_TAG) {
throw new Error(`GitHub release tag ${metadata.tagName} does not match requested tag ${RELEASE_TAG}`)
}
return metadata
}
function findAssets() {
return fs.readdirSync(DIST_DIR, { withFileTypes: true })
.filter(entry => entry.isFile())
.map(entry => path.join(DIST_DIR, entry.name))
.sort((left, right) => left.localeCompare(right))
}
function getExistingAssetNames(release) {
if (!Array.isArray(release.assets)) {
return new Set()
}
return new Set(release.assets
.map(asset => asset && asset.name)
.filter(name => typeof name === 'string'))
}
async function publishRelease() {
requireEnvironmentVariable('ATOM_PAT', ATOM_PAT)
requireEnvironmentVariable('ATOM_OWNER', ATOM_OWNER)
requireEnvironmentVariable('ATOM_REPO', ATOM_REPO)
requireEnvironmentVariable('RELEASE_TAG', RELEASE_TAG)
if (!Number.isInteger(UPLOAD_TIMEOUT_MINUTES) || UPLOAD_TIMEOUT_MINUTES < 1 || UPLOAD_TIMEOUT_MINUTES > 180) {
throw new Error('ATOM_UPLOAD_TIMEOUT_MINUTES must be an integer between 1 and 180')
}
const metadata = readReleaseMetadata()
const assetPaths = findAssets()
if (assetPaths.length === 0) {
throw new Error(`No GitHub release assets found in ${DIST_DIR}`)
}
console.log(`[AtomGit] Mirroring ${assetPaths.length} assets to ${ATOM_OWNER}/${ATOM_REPO}@${RELEASE_TAG}`)
await verifyCredentials()
const release = await getOrCreateRelease(metadata)
const existingAssetNames = getExistingAssetNames(release)
for (const assetPath of assetPaths) {
const fileName = path.basename(assetPath)
if (existingAssetNames.has(fileName)) {
console.log(`[AtomGit] Skipping existing asset ${fileName}`)
continue
}
await uploadAsset(assetPath)
}
console.log(`[AtomGit] Release ${RELEASE_TAG} mirror completed`)
}
publishRelease().catch(error => {
console.error('[AtomGit] Publish failed:', error)
process.exitCode = 1
})
-79
View File
@@ -1,79 +0,0 @@
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')
const yaml = require('js-yaml')
const distDir = path.join(__dirname, '../dist')
const yamlPath = path.join(distDir, 'latest.yml')
if (!fs.existsSync(yamlPath)) {
console.log('⚠️ latest.yml not found in dist/. Skipping update.')
process.exit(0)
}
console.log(`Reading ${yamlPath}...`)
const yamlContent = fs.readFileSync(yamlPath, 'utf8')
let doc
try {
doc = yaml.load(yamlContent)
} catch (e) {
console.error('❌ Failed to parse latest.yml:', e)
process.exit(1)
}
// Get all .exe files in dist directory
const files = fs.readdirSync(distDir).filter(f => f.endsWith('.exe'))
if (files.length === 0) {
console.log('⚠️ No .exe files found in dist/.')
process.exit(0)
}
let updated = false
files.forEach(file => {
const filePath = path.join(distDir, file)
// 1. Calculate new Hash and Size
const buffer = fs.readFileSync(filePath)
const hash = crypto.createHash('sha512').update(buffer).digest('base64')
const size = fs.statSync(filePath).size
console.log(`Processing ${file}:`)
console.log(` -> New Hash: ${hash}`)
console.log(` -> New Size: ${size}`)
// 2. Update entries in 'files' list
if (Array.isArray(doc.files)) {
const fileEntry = doc.files.find(f => f.url === file)
if (fileEntry) {
fileEntry.sha512 = hash
fileEntry.size = size
updated = true
console.log(' -> Updated file entry in yaml object')
}
}
// 3. Update root path entry (if exists)
// electron-builder's latest.yml usually has root path, sha512, size
// corresponding to the main file of current build (usually x64 or current arch)
if (doc.path === file) {
doc.sha512 = hash
doc.size = size
updated = true
console.log(' -> Updated root path entry in yaml object')
}
})
if (updated) {
// 4. Dump back to file
// lineWidth: -1 prevents long strings from wrapping, keeping it clean
const newYamlContent = yaml.dump(doc, { lineWidth: -1 })
fs.writeFileSync(yamlPath, newYamlContent, 'utf8')
console.log('✅ latest.yml updated successfully.')
console.log('New yaml content:')
console.log(newYamlContent)
} else {
console.log('⚠️ No matching entries found in latest.yml to update.')
}
-181
View File
@@ -1,181 +0,0 @@
// upload dist bundled-app to r2
// upload version file to cos
require('dotenv').config()
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 uploadToDev = process.argv.includes('--dev')
const uploadVersionFilesOnly = process.argv.includes('--version-files-only')
const S3_BUCKET = 'release'
const S3_LEGACY_BUCKET = 'picgo'
const VERSION = pkg.version
const DEV_DIST_PREFIX = 'dev/'
const FILE_PATH = uploadToDev ? `${DEV_DIST_PREFIX}${VERSION}/` : `${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'
}
/**
* 检查是否使用 --all 参数(上传所有平台)
*/
function shouldUploadAll() {
return process.argv.includes('--all')
}
/**
* 获取要上传的配置列表
*/
function getUploadConfigs() {
if (shouldUploadAll()) {
// 合并所有平台的配置
return [
...configList.darwin,
...configList.win32,
...configList.linux
]
}
// 原有逻辑:根据当前平台
const platform = process.platform
return configList[platform] || []
}
/**
* 上传单个文件到 S3
*/
async function uploadFileToS3(client, bucket, key, filePath, contentType = 'application/octet-stream') {
const upload = new Upload({
client,
params: {
Bucket: bucket,
Key: key,
Body: fs.createReadStream(filePath),
ContentType: contentType
}
})
upload.on('httpUploadProgress', progress => {
const percent = progress.total ? Math.round((progress.loaded / progress.total) * 100) : 0
process.stdout.write(`\r Progress: ${progress.loaded}/${progress.total || '?'} (${percent}%)`)
})
await upload.done()
console.log('') // 换行
}
const uploadDist = async () => {
try {
const configs = getUploadConfigs()
if (configs.length === 0) {
console.warn('[PicGo] No upload config found!')
return
}
console.log(`[PicGo] Upload mode: ${shouldUploadAll() ? 'ALL PLATFORMS' : process.platform}`)
console.log(`[PicGo] Version: ${VERSION}`)
console.log(`[PicGo] Install package upload: ${uploadVersionFilesOnly ? 'SKIPPED' : 'ENABLED'}`)
console.log(`[PicGo] Total files to upload: ${configs.length}\n`)
const uploadedVersionFiles = new Set()
const client = new S3Client(S3Options)
const legacyClient = new S3Client(S3LegacyOptions)
for (const [index, config] of configs.entries()) {
const fileName = `${config.appNameWithPrefix}-${VERSION}-${config.arch}.${config.ext}`
const filePath = path.join(distPath, fileName)
let versionFileName = config['version-file']
console.log(`[${index + 1}/${configs.length}] Processing ${fileName}`)
// 上传构建产物
if (!uploadVersionFilesOnly && fs.existsSync(filePath)) {
console.log(` Uploading to S3: ${FILE_PATH}${fileName}`)
await uploadFileToS3(client, S3_BUCKET, `${FILE_PATH}${fileName}`, filePath)
console.log(` ✅ Uploaded: ${fileName}`)
} else if (!uploadVersionFilesOnly) {
console.warn(` ⚠️ File not found: ${fileName}`)
}
let versionFilePath = path.join(distPath, versionFileName)
// Beta 版本使用不同的 yml 文件名
if (VERSION.toLowerCase().includes('beta') && fs.existsSync(versionFilePath)) {
versionFileName = versionFileName.replace('.yml', '.beta.yml')
const betaVersionFilePath = path.join(distPath, versionFileName)
// change to beta version file path
fs.renameSync(versionFilePath, betaVersionFilePath)
versionFilePath = betaVersionFilePath
}
// 上传版本文件(每个 yml 只上传一次)
if (!uploadedVersionFiles.has(versionFileName) && fs.existsSync(versionFilePath)) {
console.log(` Uploading version file: ${versionFileName}`)
const versionFileNameFinal = uploadToDev ? `${DEV_DIST_PREFIX}${versionFileName}` : versionFileName
// 上传到主 bucket
await uploadFileToS3(
client,
S3_BUCKET,
versionFileNameFinal,
versionFilePath,
mime.lookup(versionFileName) || 'text/yaml'
)
// 上传到 legacy bucket
await uploadFileToS3(
legacyClient,
S3_LEGACY_BUCKET,
versionFileNameFinal,
versionFilePath,
mime.lookup(versionFileName) || 'text/yaml'
)
uploadedVersionFiles.add(versionFileName)
console.log(` ✅ Version file uploaded: ${versionFileName}`)
}
console.log('')
}
console.log('[PicGo] 🎉 All uploads completed!')
} catch (e) {
console.error('[PicGo] ❌ Upload error:', e)
process.exit(1)
}
}
const main = async () => {
await uploadDist()
}
main()
-115
View File
@@ -1,115 +0,0 @@
import fs from 'node:fs'
import path from 'node:path'
import { parse } from 'yaml'
import type { Plugin } from 'vite'
const I18N_SOURCE_DIR = 'public/i18n'
const SOURCE_RELATIVE_PATH = `${I18N_SOURCE_DIR}/en.yml`
const UNIVERSAL_OUTPUT_RELATIVE_PATH = 'src/universal/types/i18n.d.ts'
const RENDERER_OUTPUT_RELATIVE_PATH = 'src/renderer/i18n/i18next.d.ts'
function formatKey (key: string) {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key)
}
function readTranslationKeys (sourcePath: string) {
if (!fs.existsSync(sourcePath)) {
return []
}
const raw = fs.readFileSync(sourcePath, 'utf8')
const data = parse(raw)
if (!data || typeof data !== 'object' || Array.isArray(data)) {
return []
}
return Object.keys(data as Record<string, unknown>)
}
function buildUniversalTypeContent (keys: string[]) {
const uniqueKeys = Array.from(new Set(keys))
const lines = uniqueKeys.map((key) => ` ${formatKey(key)}: string`)
return `// This file is auto-generated by vite-plugin-i18n-types. Do not edit.
interface ILocales {
${lines.join('\n')}
}
type ILocalesKey = keyof ILocales
`
}
function buildRendererTypeContent () {
return `// This file is auto-generated by vite-plugin-i18n-types. Do not edit.
import "i18next"
declare module "i18next" {
interface CustomTypeOptions {
resources: {
translation: ILocales
}
}
}
`
}
function writeFileIfChanged (outputPath: string, nextContent: string) {
const previousContent = fs.existsSync(outputPath)
? fs.readFileSync(outputPath, 'utf8')
: ''
if (previousContent === nextContent) {
return
}
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
fs.writeFileSync(outputPath, nextContent, 'utf8')
}
function writeTypesFiles (sourcePath: string, universalOutputPath: string, rendererOutputPath: string) {
const keys = readTranslationKeys(sourcePath)
writeFileIfChanged(universalOutputPath, buildUniversalTypeContent(keys))
writeFileIfChanged(rendererOutputPath, buildRendererTypeContent())
}
export function i18nTypesPlugin (): Plugin {
let sourcePath = ''
let universalOutputPath = ''
let rendererOutputPath = ''
let projectRoot = ''
const generate = () => {
try {
writeTypesFiles(sourcePath, universalOutputPath, rendererOutputPath)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(`[vite-plugin-i18n-types] ${message}`)
}
}
return {
name: 'vite-plugin-i18n-types',
enforce: 'pre',
configResolved (config) {
projectRoot = path.resolve(config.root, '../..')
sourcePath = path.resolve(projectRoot, SOURCE_RELATIVE_PATH)
universalOutputPath = path.resolve(projectRoot, UNIVERSAL_OUTPUT_RELATIVE_PATH)
rendererOutputPath = path.resolve(projectRoot, RENDERER_OUTPUT_RELATIVE_PATH)
},
buildStart () {
generate()
},
configureServer (server) {
server.watcher.add(path.resolve(projectRoot, I18N_SOURCE_DIR))
},
handleHotUpdate ({ file }) {
const normalizedFile = path.resolve(file)
const normalizedSourceDir = path.resolve(path.dirname(sourcePath))
if (normalizedFile.startsWith(normalizedSourceDir)) {
generate()
}
}
}
}
@@ -1,194 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { IpcMainInvokeEvent } from 'electron'
import os from 'node:os'
import path from 'node:path'
import fs from 'fs-extra'
import { IRPCActionType } from '~/universal/types/enum'
import { IPicGoCloudConfigSyncToastType } from '#/types/cloudConfigSync'
vi.mock('electron', () => ({
app: {
isPackaged: false,
getPath: vi.fn(() => os.tmpdir()),
getAppPath: vi.fn(() => process.cwd())
}
}))
type OnAskEncryptionSwitch = (context: { from: string; to: string }) => Promise<boolean>
type SyncImplementation = (askSwitch?: OnAskEncryptionSwitch) => Promise<{ status: string; message?: string }>
const showMessageBoxMock = vi.fn()
const showInputBoxMock = vi.fn()
const getUserInfoMock = vi.fn()
const getConfigMock = vi.fn()
const saveConfigMock = vi.fn()
const i18nTranslateMock = vi.fn((key: string) => key)
let baseDir = ''
let syncImplementation: SyncImplementation | null = null
const createInvokeEvent = (): IpcMainInvokeEvent => {
const event = {
sender: {
send: vi.fn()
}
}
return event as unknown as IpcMainInvokeEvent
}
vi.mock('@core/picgo', () => {
return {
default: {
get baseDir () {
return baseDir
},
getConfig: getConfigMock,
saveConfig: saveConfigMock,
cloud: {
getUserInfo: getUserInfoMock,
login: vi.fn(),
logout: vi.fn(),
disposeLoginFlow: vi.fn()
},
i18n: {
translate: i18nTranslateMock
}
}
}
})
vi.mock('apis/gui', () => {
return {
default: {
getInstance: () => ({
showInputBox: showInputBoxMock,
showMessageBox: showMessageBoxMock
})
}
}
})
vi.mock('apis/core/picgo/logger', () => {
return {
default: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn()
}
}
})
vi.mock('picgo', () => {
const SyncStatus = {
SUCCESS: 'success',
CONFLICT: 'conflict',
FAILED: 'failed'
}
const EncryptionMethod = {
AUTO: 'auto',
SSE: 'sse',
E2EE: 'e2ee'
}
const E2EAskPinReason = {
SETUP: 'setup',
DECRYPT: 'decrypt',
RETRY: 'retry'
}
const ConflictType = {
CONFLICT: 'conflict'
}
class ConfigSyncManager {
private readonly onAskEncryptionSwitch?: OnAskEncryptionSwitch
constructor (_ctx: unknown, options: { onAskEncryptionSwitch?: OnAskEncryptionSwitch } = {}) {
this.onAskEncryptionSwitch = options.onAskEncryptionSwitch
}
async sync (): Promise<{ status: string; message?: string }> {
if (syncImplementation) {
return syncImplementation(this.onAskEncryptionSwitch)
}
return { status: SyncStatus.SUCCESS }
}
}
return {
ConfigSyncManager,
ConflictType,
E2EAskPinReason,
EncryptionMethod,
SyncStatus
}
})
describe('config sync encryption switch confirmation (main)', () => {
beforeEach(async () => {
vi.clearAllMocks()
syncImplementation = null
getConfigMock.mockReturnValue(undefined)
getUserInfoMock.mockResolvedValue({ user: 'tester' })
i18nTranslateMock.mockImplementation((key: string) => key)
baseDir = await fs.mkdtemp(path.join(os.tmpdir(), 'picgo-gui-config-sync-'))
})
afterEach(async () => {
if (baseDir) {
await fs.remove(baseDir)
}
})
it('prompts for confirmation and proceeds on confirm', async () => {
showMessageBoxMock.mockResolvedValue({ result: 0, checkboxChecked: false })
syncImplementation = async (askSwitch) => {
if (askSwitch) {
const confirmed = await askSwitch({ from: 'e2ee', to: 'sse' })
if (!confirmed) {
return { status: 'failed', message: i18nTranslateMock('CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED') }
}
}
return { status: 'success', message: 'ok' }
}
const { cloudRouter } = await import('../../main/events/rpc/routes/cloud')
const handler = cloudRouter.routes().get(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_START)
const res = await handler?.([], createInvokeEvent())
expect(showMessageBoxMock).toHaveBeenCalledTimes(1)
expect(showMessageBoxMock).toHaveBeenCalledWith({
title: 'PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE',
message: 'PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY',
type: 'warning',
buttons: [
'PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM',
'PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL'
]
})
expect(res?.success).toBe(true)
expect(res?.data.toastType).toBe(IPicGoCloudConfigSyncToastType.SUCCESS)
expect(res?.data.message).toBe('PICGO_CLOUD_CONFIG_SYNC_SUCCESS')
})
it('maps encryption-switch cancel to warning', async () => {
showMessageBoxMock.mockResolvedValue({ result: 1, checkboxChecked: false })
syncImplementation = async (askSwitch) => {
if (askSwitch) {
const confirmed = await askSwitch({ from: 'sse', to: 'e2ee' })
if (!confirmed) {
return { status: 'failed', message: i18nTranslateMock('CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED') }
}
}
return { status: 'success', message: 'ok' }
}
const { cloudRouter } = await import('../../main/events/rpc/routes/cloud')
const handler = cloudRouter.routes().get(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_START)
const res = await handler?.([], createInvokeEvent())
expect(showMessageBoxMock).toHaveBeenCalledTimes(1)
expect(res?.success).toBe(true)
expect(res?.data.toastType).toBe(IPicGoCloudConfigSyncToastType.WARNING)
expect(res?.data.message).toBe('PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED')
})
})
-108
View File
@@ -1,108 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { RELEASE_URL, RELEASE_URL_BACKUP } from '#/utils/static'
import { getLatestVersion } from '~/main/utils/getLatestVersion'
const { axiosGetMock } = vi.hoisted(() => {
return {
axiosGetMock: vi.fn()
}
})
vi.mock('axios', () => {
return {
default: {
get: axiosGetMock
}
}
})
describe('main/utils/getLatestVersion', () => {
beforeEach(() => {
axiosGetMock.mockReset()
})
it('returns stable release when beta channel is older', async () => {
axiosGetMock.mockResolvedValueOnce({
data: [
{ tag_name: 'v2.5.2', prerelease: false, draft: false },
{ tag_name: 'v2.4.2-beta.0', prerelease: true, draft: false }
]
})
const version = await getLatestVersion(true)
expect(version).toBe('2.5.2')
expect(axiosGetMock).toHaveBeenCalledWith(RELEASE_URL, {
headers: {
Referer: 'https://github.com'
}
})
})
it('returns prerelease when beta channel has a newer version', async () => {
axiosGetMock.mockResolvedValueOnce({
data: [
{ tag_name: 'v2.5.2', prerelease: false, draft: false },
{ tag_name: 'v2.6.0-beta.1', prerelease: true, draft: false }
]
})
const version = await getLatestVersion(true)
expect(version).toBe('2.6.0-beta.1')
})
it('ignores prerelease when beta updates are disabled', async () => {
axiosGetMock.mockResolvedValueOnce({
data: [
{ tag_name: 'v2.6.0-beta.1', prerelease: true, draft: false },
{ tag_name: 'v2.5.2', prerelease: false, draft: false }
]
})
const version = await getLatestVersion(false)
expect(version).toBe('2.5.2')
})
it('fallback compares stable and beta backup metadata when beta updates are enabled', async () => {
axiosGetMock.mockRejectedValueOnce(new Error('network down'))
axiosGetMock.mockResolvedValueOnce({
data: 'version: 2.5.2'
})
axiosGetMock.mockResolvedValueOnce({
data: 'version: 2.4.2-beta.0'
})
const version = await getLatestVersion(true)
expect(version).toBe('2.5.2')
expect(axiosGetMock).toHaveBeenNthCalledWith(2, `${RELEASE_URL_BACKUP}/latest.yml`, {
headers: {
Referer: 'https://github.com'
}
})
expect(axiosGetMock).toHaveBeenNthCalledWith(3, `${RELEASE_URL_BACKUP}/latest.beta.yml`, {
headers: {
Referer: 'https://github.com'
}
})
})
it('fallback uses only stable backup metadata when beta updates are disabled', async () => {
axiosGetMock.mockRejectedValueOnce(new Error('network down'))
axiosGetMock.mockResolvedValueOnce({
data: 'version: 2.5.2'
})
const version = await getLatestVersion(false)
expect(version).toBe('2.5.2')
expect(axiosGetMock).toHaveBeenCalledTimes(2)
expect(axiosGetMock).toHaveBeenNthCalledWith(2, `${RELEASE_URL_BACKUP}/latest.yml`, {
headers: {
Referer: 'https://github.com'
}
})
})
})
@@ -1,93 +0,0 @@
import { describe, expect, it } from 'vitest'
import { createSchemaOnlyUploaderContext } from '~/main/utils/schemaOnlyUploaderContext'
type ConfigRecord = Record<string, unknown>
function isConfigRecord(value: unknown): value is ConfigRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function getByPath(value: unknown, path: string): unknown {
return path.split('.').reduce<unknown>((current, key) => {
if (!isConfigRecord(current)) {
return undefined
}
return current[key]
}, value)
}
describe('createSchemaOnlyUploaderContext', () => {
it('hides only the target uploader configuration without mutating the source', () => {
const config = {
picBed: {
current: 'tcyun',
tcyun: {
secretId: 'existing-secret-id',
secretKey: 'existing-secret-key'
},
github: {
token: 'github-token'
}
},
uploader: {
tcyun: {
defaultId: 'config-1'
},
github: {
defaultId: 'config-2'
}
},
settings: {
proxy: 'http://localhost:7890'
}
}
const context = {
marker: 'original-context',
getConfig<T>(name?: string): T {
return (name ? getByPath(config, name) : config) as T
}
}
const schemaContext = createSchemaOnlyUploaderContext(context, 'tcyun')
expect(schemaContext.getConfig('picBed.tcyun')).toBeUndefined()
expect(schemaContext.getConfig('picBed.tcyun.secretKey')).toBeUndefined()
expect(schemaContext.getConfig('uploader.tcyun')).toBeUndefined()
expect(schemaContext.getConfig('settings.proxy')).toBe('http://localhost:7890')
expect(schemaContext.marker).toBe('original-context')
expect(schemaContext.getConfig<ConfigRecord>('picBed')).toEqual({
current: 'tcyun',
github: {
token: 'github-token'
}
})
expect(schemaContext.getConfig<ConfigRecord>('uploader')).toEqual({
github: {
defaultId: 'config-2'
}
})
expect(schemaContext.getConfig<ConfigRecord>()).toEqual({
picBed: {
current: 'tcyun',
github: {
token: 'github-token'
}
},
uploader: {
github: {
defaultId: 'config-2'
}
},
settings: {
proxy: 'http://localhost:7890'
}
})
expect(context.getConfig('picBed.tcyun')).toEqual({
secretId: 'existing-secret-id',
secretKey: 'existing-secret-key'
})
})
})
-188
View File
@@ -1,188 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
type ServerConfig = {
port: number | string
host: string
enable: boolean
}
type ServerUploadAdapter = {
uploadClipboard: () => Promise<ImgInfo[] | Error>
uploadPaths: (paths: string[]) => Promise<ImgInfo[] | Error>
getTempDir: () => string
}
const isRecord = (value: unknown): value is Record<string, unknown> => {
return typeof value === 'object' && value !== null
}
let serverConfig: ServerConfig | undefined
let installedUploadAdapter: ServerUploadAdapter | undefined
const getConfigMock = vi.fn((key?: string) => {
if (key === 'settings.server') return serverConfig
return undefined
})
const saveConfigMock = vi.fn((patch: unknown) => {
if (!isRecord(patch)) return
const next = patch['settings.server']
if (isRecord(next) && 'port' in next && 'host' in next && 'enable' in next) {
serverConfig = next as unknown as ServerConfig
}
})
const setUploadAdapterMock = vi.fn((adapter?: ServerUploadAdapter) => {
installedUploadAdapter = adapter
})
const registerPostMock = vi.fn()
const listenMock = vi.fn()
const shutdownMock = vi.fn()
const loggerMock = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn()
}
const getAvailableWindowMock = vi.fn()
const uploadClipboardFilesWithInfoMock = vi.fn()
const uploadSelectedFilesWithInfoMock = vi.fn()
const getFormImageFolderPathMock = vi.fn(() => '/tmp/picgo-form-images')
vi.mock('@core/picgo', () => {
return {
default: {
getConfig: getConfigMock,
saveConfig: saveConfigMock,
server: {
setUploadAdapter: setUploadAdapterMock,
registerPost: registerPostMock,
listen: listenMock,
shutdown: shutdownMock
}
}
}
})
vi.mock('@core/picgo/logger', () => {
return { default: loggerMock }
})
vi.mock('apis/app/window/windowManager', () => {
return {
default: {
getAvailableWindow: getAvailableWindowMock
}
}
})
vi.mock('apis/app/uploader/apis', () => {
return {
uploadClipboardFilesWithInfo: uploadClipboardFilesWithInfoMock,
uploadSelectedFilesWithInfo: uploadSelectedFilesWithInfoMock
}
})
vi.mock('apis/core/datastore/dbChecker', () => {
return {
getFormImageFolderPath: getFormImageFolderPathMock
}
})
describe('main/server (GUI adapter to picgo-core)', () => {
beforeEach(() => {
serverConfig = undefined
installedUploadAdapter = undefined
vi.clearAllMocks()
vi.resetModules()
})
it('backfills default settings.server when missing', async () => {
serverConfig = undefined
await import('../../main/server')
expect(saveConfigMock).toHaveBeenCalledWith({
'settings.server': {
port: 36677,
host: '127.0.0.1',
enable: true
}
})
expect(serverConfig).toEqual({
port: 36677,
host: '127.0.0.1',
enable: true
})
})
it('does not install upload adapter or listen when settings.server.enable is false', async () => {
serverConfig = { port: 36677, host: '127.0.0.1', enable: false }
const mod = await import('../../main/server')
const server = mod.default
server.startup()
expect(setUploadAdapterMock).not.toHaveBeenCalled()
expect(registerPostMock).not.toHaveBeenCalled()
expect(listenMock).not.toHaveBeenCalled()
})
it('installs GUI upload adapter once and delegates listen/shutdown to picgo.server', async () => {
serverConfig = { port: '36677', host: '127.0.0.1', enable: true }
listenMock.mockResolvedValue(36677)
const mod = await import('../../main/server')
const server = mod.default
server.startup()
server.startup()
expect(setUploadAdapterMock).toHaveBeenCalledTimes(1)
expect(registerPostMock).not.toHaveBeenCalled()
expect(listenMock).toHaveBeenCalledTimes(2)
expect(listenMock).toHaveBeenCalledWith(36677, '127.0.0.1')
expect(installedUploadAdapter).toBeDefined()
server.shutdown()
expect(shutdownMock).toHaveBeenCalledTimes(1)
})
it('GUI upload adapter preserves side-effect upload helpers and returns raw ImgInfo[] for Core responses', async () => {
serverConfig = { port: 36677, host: '127.0.0.1', enable: true }
const webContents = { send: vi.fn() }
const clipboardImage: ImgInfo = {
imgUrl: 'https://raw.example/clipboard image.png',
fileName: 'clipboard image.png',
extname: '.png',
size: 123
}
const selectedImages: ImgInfo[] = [{
imgUrl: 'https://raw.example/a image.png',
fileName: 'a image.png',
extname: '.png',
origin: '/tmp/a.png',
width: 10
}]
getAvailableWindowMock.mockReturnValue({ webContents })
uploadClipboardFilesWithInfoMock.mockResolvedValue([clipboardImage])
uploadSelectedFilesWithInfoMock.mockResolvedValue(selectedImages)
const mod = await import('../../main/server')
const server = mod.default
server.startup()
expect(installedUploadAdapter).toBeDefined()
const adapter = installedUploadAdapter!
await expect(adapter.uploadClipboard()).resolves.toEqual([clipboardImage])
await expect(adapter.uploadPaths(['/tmp/a.png'])).resolves.toEqual(selectedImages)
expect(uploadClipboardFilesWithInfoMock).toHaveBeenCalledTimes(1)
expect(getAvailableWindowMock).toHaveBeenCalledTimes(1)
expect(uploadSelectedFilesWithInfoMock).toHaveBeenCalledWith(webContents, [{ path: '/tmp/a.png' }])
expect(adapter.getTempDir()).toBe('/tmp/picgo-form-images')
expect(getFormImageFolderPathMock).toHaveBeenCalledTimes(1)
})
})
-272
View File
@@ -1,272 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { WebContents } from 'electron'
import { IPasteStyle, IRPCActionType, IWindowList } from '#/types/enum'
import {
uploadClipboardFiles,
uploadClipboardFilesWithInfo,
uploadSelectedFiles,
uploadSelectedFilesWithInfo
} from '../../main/apis/app/uploader/apis'
type WebContentsStub = {
send: ReturnType<typeof vi.fn>
}
type WindowStub = {
webContents: WebContentsStub
}
const mocks = vi.hoisted(() => {
const configValues: Record<string, unknown> = {}
return {
configValues,
getConfigMock: vi.fn(),
loggerInfoMock: vi.fn(),
setWebContentsMock: vi.fn(),
uploadMock: vi.fn(),
uploadWithBuildInClipboardMock: vi.fn(),
getAvailableWindowMock: vi.fn(),
windowManagerGetMock: vi.fn(),
windowManagerHasMock: vi.fn(),
pasteTemplateMock: vi.fn(),
albumInsertMock: vi.fn(),
handleCopyUrlMock: vi.fn(),
handleUrlEncodeWithSettingMock: vi.fn(),
showNotificationMock: vi.fn(),
TMock: vi.fn()
}
})
vi.mock('@core/picgo', () => ({
default: {
getConfig: mocks.getConfigMock
}
}))
vi.mock('@core/picgo/logger', () => ({
default: {
info: mocks.loggerInfoMock
}
}))
vi.mock('../../main/apis/app/uploader', () => ({
default: {
setWebContents: mocks.setWebContentsMock
}
}))
vi.mock('apis/app/window/windowManager', () => ({
default: {
getAvailableWindow: mocks.getAvailableWindowMock,
get: mocks.windowManagerGetMock,
has: mocks.windowManagerHasMock
}
}))
vi.mock('~/main/utils/pasteTemplate', () => ({
default: mocks.pasteTemplateMock
}))
vi.mock('~/main/apis/core/datastore', () => ({
AlbumDB: {
getInstance: () => ({
insert: mocks.albumInsertMock
})
}
}))
vi.mock('~/main/utils/common', () => ({
handleCopyUrl: mocks.handleCopyUrlMock,
handleUrlEncodeWithSetting: mocks.handleUrlEncodeWithSettingMock,
showNotification: mocks.showNotificationMock
}))
vi.mock('~/main/i18n/index', () => ({
T: mocks.TMock
}))
const createWebContents = (): WebContentsStub => ({
send: vi.fn()
})
const asWebContents = (webContents: WebContentsStub): WebContents => {
return webContents as unknown as WebContents
}
describe('main uploader API helpers', () => {
let availableWindow: WindowStub
let trayWindow: WindowStub
let settingWindow: WindowStub
beforeEach(() => {
vi.useFakeTimers()
vi.clearAllMocks()
Object.keys(mocks.configValues).forEach((key) => {
delete mocks.configValues[key]
})
Object.assign(mocks.configValues, {
'settings.pasteStyle': IPasteStyle.MARKDOWN,
'settings.customLink': '$url',
'settings.useBuiltinClipboard': false
})
availableWindow = { webContents: createWebContents() }
trayWindow = { webContents: createWebContents() }
settingWindow = { webContents: createWebContents() }
mocks.getConfigMock.mockImplementation(<T>(key: string): T => mocks.configValues[key] as T)
mocks.setWebContentsMock.mockReturnValue({
upload: mocks.uploadMock,
uploadWithBuildInClipboard: mocks.uploadWithBuildInClipboardMock
})
mocks.getAvailableWindowMock.mockReturnValue(availableWindow)
mocks.windowManagerGetMock.mockImplementation((name: IWindowList) => {
if (name === IWindowList.TRAY_WINDOW) return trayWindow
if (name === IWindowList.SETTING_WINDOW) return settingWindow
return undefined
})
mocks.windowManagerHasMock.mockImplementation((name: IWindowList) => name === IWindowList.SETTING_WINDOW)
mocks.pasteTemplateMock.mockImplementation((style: IPasteStyle, item: ImgInfo, customLink?: string) => {
return `paste:${style}:${item.imgUrl ?? ''}:${customLink ?? ''}`
})
mocks.handleUrlEncodeWithSettingMock.mockImplementation((url: string) => `encoded:${url}`)
mocks.TMock.mockImplementation((key: string) => `t:${key}`)
})
afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
})
it('preserves clipboard upload side effects and returns raw image info for server adapters', async () => {
const image: ImgInfo = {
imgUrl: 'https://raw.example/clipboard image.png',
fileName: 'clipboard image.png',
extname: '.png',
origin: 'clipboard',
size: 123
}
mocks.uploadMock.mockResolvedValue([image])
const result = await uploadClipboardFilesWithInfo()
vi.runOnlyPendingTimers()
expect(result).toEqual([image])
expect(mocks.loggerInfoMock).toHaveBeenCalledWith('upload clipboard file')
expect(mocks.setWebContentsMock).toHaveBeenCalledWith(availableWindow.webContents)
expect(mocks.uploadMock).toHaveBeenCalledWith()
expect(mocks.handleCopyUrlMock).toHaveBeenCalledWith(`paste:${IPasteStyle.MARKDOWN}:${image.imgUrl}:$url`)
expect(mocks.showNotificationMock).toHaveBeenCalledWith({
title: 't:UPLOAD_SUCCEED',
body: image.imgUrl
})
expect(mocks.albumInsertMock).toHaveBeenCalledWith(image)
expect(trayWindow.webContents.send).toHaveBeenCalledWith('clipboardFiles', [])
expect(trayWindow.webContents.send).toHaveBeenCalledWith('uploadFiles', [image])
expect(settingWindow.webContents.send).toHaveBeenCalledWith(IRPCActionType.UPDATE_ALBUM)
})
it('keeps legacy clipboard helper return value URL-encoded while preserving raw side effects', async () => {
const image: ImgInfo = {
imgUrl: 'https://raw.example/clipboard image.png',
fileName: 'clipboard image.png',
extname: '.png'
}
mocks.uploadMock.mockResolvedValue([image])
const result = await uploadClipboardFiles()
vi.runOnlyPendingTimers()
expect(result).toBe(`encoded:${image.imgUrl}`)
expect(mocks.handleUrlEncodeWithSettingMock).toHaveBeenCalledWith(image.imgUrl)
expect(mocks.handleCopyUrlMock).toHaveBeenCalledWith(`paste:${IPasteStyle.MARKDOWN}:${image.imgUrl}:$url`)
expect(mocks.albumInsertMock).toHaveBeenCalledWith(image)
expect(trayWindow.webContents.send).toHaveBeenCalledWith('uploadFiles', [image])
})
it('uses the builtin clipboard upload path when configured', async () => {
const image: ImgInfo = {
imgUrl: 'https://raw.example/builtin.png',
fileName: 'builtin.png',
extname: '.png'
}
mocks.configValues['settings.useBuiltinClipboard'] = true
mocks.uploadWithBuildInClipboardMock.mockResolvedValue([image])
const result = await uploadClipboardFilesWithInfo()
vi.runOnlyPendingTimers()
expect(result).toEqual([image])
expect(mocks.uploadWithBuildInClipboardMock).toHaveBeenCalledWith()
expect(mocks.uploadMock).not.toHaveBeenCalled()
expect(mocks.albumInsertMock).toHaveBeenCalledWith(image)
})
it('preserves selected-file upload side effects and returns raw image info for server adapters', async () => {
const webContents = createWebContents()
const files: IFileWithPath[] = [{ path: '/tmp/a.png' }, { path: '/tmp/b.png' }]
const images: ImgInfo[] = [
{
imgUrl: 'https://raw.example/a image.png',
fileName: 'a image.png',
extname: '.png',
origin: '/tmp/a.png',
width: 10
},
{
imgUrl: 'https://raw.example/b image.png',
fileName: 'b image.png',
extname: '.png',
origin: '/tmp/b.png',
height: 20
}
]
mocks.uploadMock.mockResolvedValue(images)
const result = await uploadSelectedFilesWithInfo(asWebContents(webContents), files)
vi.runOnlyPendingTimers()
expect(result).toEqual(images)
expect(mocks.setWebContentsMock).toHaveBeenCalledWith(webContents)
expect(mocks.uploadMock).toHaveBeenCalledWith(['/tmp/a.png', '/tmp/b.png'])
expect(mocks.handleCopyUrlMock).toHaveBeenCalledWith([
`paste:${IPasteStyle.MARKDOWN}:${images[0].imgUrl}:$url`,
`paste:${IPasteStyle.MARKDOWN}:${images[1].imgUrl}:$url`
].join('\n'))
expect(mocks.showNotificationMock).toHaveBeenCalledWith({
title: 't:UPLOAD_SUCCEED',
body: images[0].imgUrl
})
expect(mocks.showNotificationMock).toHaveBeenCalledWith({
title: 't:UPLOAD_SUCCEED',
body: images[1].imgUrl
})
expect(mocks.albumInsertMock).toHaveBeenNthCalledWith(1, images[0])
expect(mocks.albumInsertMock).toHaveBeenNthCalledWith(2, images[1])
expect(trayWindow.webContents.send).toHaveBeenCalledWith('uploadFiles', images)
expect(settingWindow.webContents.send).toHaveBeenCalledWith(IRPCActionType.UPDATE_ALBUM)
})
it('keeps legacy selected-file helper return values URL-encoded', async () => {
const webContents = createWebContents()
const files: IFileWithPath[] = [{ path: '/tmp/a.png' }, { path: '/tmp/b.png' }]
const images: ImgInfo[] = [
{ imgUrl: 'https://raw.example/a image.png', fileName: 'a image.png' },
{ imgUrl: 'https://raw.example/b image.png', fileName: 'b image.png' }
]
mocks.uploadMock.mockResolvedValue(images)
const result = await uploadSelectedFiles(asWebContents(webContents), files)
vi.runOnlyPendingTimers()
expect(result).toEqual([
`encoded:${images[0].imgUrl}`,
`encoded:${images[1].imgUrl}`
])
expect(mocks.handleUrlEncodeWithSettingMock).toHaveBeenNthCalledWith(1, images[0].imgUrl)
expect(mocks.handleUrlEncodeWithSettingMock).toHaveBeenNthCalledWith(2, images[1].imgUrl)
expect(mocks.albumInsertMock).toHaveBeenNthCalledWith(1, images[0])
expect(mocks.albumInsertMock).toHaveBeenNthCalledWith(2, images[1])
})
})
@@ -1,166 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { SHOW_PLUGIN_PAGE_MENU } from '#/events/constants'
import { IRPCActionType } from '~/universal/types/enum'
vi.mock('@/utils/dataSender', () => ({
getConfig: vi.fn(),
invokeRPC: vi.fn(),
openURL: vi.fn(),
saveConfig: vi.fn(),
sendRPC: vi.fn(),
sendToMain: vi.fn()
}))
import { pluginsAdapter } from '@/adapters/plugins'
import {
getConfig,
invokeRPC,
openURL,
saveConfig,
sendRPC,
sendToMain
} from '@/utils/dataSender'
describe('renderer/adapters plugins', () => {
const getConfigMock = vi.mocked(getConfig)
const invokeRPCMock = vi.mocked(invokeRPC)
const saveConfigMock = vi.mocked(saveConfig)
const sendRPCMock = vi.mocked(sendRPC)
const sendToMainMock = vi.mocked(sendToMain)
beforeEach(() => {
vi.clearAllMocks()
})
it('opens plugin page menu through ipc', () => {
const plugin = {
name: 'demo',
fullName: 'picgo-plugin-demo',
author: 'author',
description: 'desc',
logo: '',
version: '1.0.0',
gui: true,
config: {},
homepage: '',
ing: false
} as IPicGoPlugin
pluginsAdapter.openPluginMenu(plugin)
expect(sendToMainMock).toHaveBeenCalledWith(SHOW_PLUGIN_PAGE_MENU, plugin)
})
it('installs plugin through install RPC', async () => {
invokeRPCMock.mockResolvedValue({
success: true,
data: 'picgo-plugin-demo'
})
const result = await pluginsAdapter.installPlugin('picgo-plugin-demo')
expect(invokeRPCMock).toHaveBeenCalledWith(
IRPCActionType.INSTALL_PLUGIN,
'picgo-plugin-demo'
)
expect(result).toEqual({
success: true,
body: 'picgo-plugin-demo',
errMsg: ''
})
})
it('imports local plugin through import RPC', async () => {
invokeRPCMock.mockResolvedValue({
success: true,
data: 'picgo-plugin-local'
})
const result = await pluginsAdapter.importLocalPlugin()
expect(invokeRPCMock).toHaveBeenCalledWith(IRPCActionType.IMPORT_LOCAL_PLUGIN)
expect(result).toEqual({
success: true,
data: 'picgo-plugin-local'
})
})
it('toggles plugin enabled through enable and disable RPC', async () => {
invokeRPCMock.mockResolvedValue({
success: true,
data: 'picgo-plugin-demo'
})
await pluginsAdapter.togglePluginEnabled('picgo-plugin-demo', true)
await pluginsAdapter.togglePluginEnabled('picgo-plugin-demo', false)
expect(invokeRPCMock).toHaveBeenNthCalledWith(
1,
IRPCActionType.ENABLE_PLUGIN,
'picgo-plugin-demo'
)
expect(invokeRPCMock).toHaveBeenNthCalledWith(
2,
IRPCActionType.DISABLE_PLUGIN,
'picgo-plugin-demo'
)
})
it('updates and uninstalls plugin through RPC', async () => {
invokeRPCMock.mockResolvedValue({
success: true,
data: 'picgo-plugin-demo'
})
await pluginsAdapter.updatePlugin('picgo-plugin-demo')
await pluginsAdapter.uninstallPlugin('picgo-plugin-demo')
expect(invokeRPCMock).toHaveBeenNthCalledWith(
1,
IRPCActionType.UPDATE_PLUGIN,
'picgo-plugin-demo'
)
expect(invokeRPCMock).toHaveBeenNthCalledWith(
2,
IRPCActionType.UNINSTALL_PLUGIN,
'picgo-plugin-demo'
)
})
it('persists transformer and needReload via config helpers', async () => {
await pluginsAdapter.saveTransformer('path')
await pluginsAdapter.setNeedReload(true)
expect(saveConfigMock).toHaveBeenNthCalledWith(1, {
'picBed.transformer': 'path'
})
expect(saveConfigMock).toHaveBeenNthCalledWith(2, {
needReload: true
})
})
it('reads needReload via config helper', async () => {
getConfigMock.mockResolvedValue(true)
const result = await pluginsAdapter.getNeedReload()
expect(getConfigMock).toHaveBeenCalledWith('needReload')
expect(result).toBe(true)
})
it('reloads app through system RPC', () => {
pluginsAdapter.reloadApp()
expect(sendRPCMock).toHaveBeenCalledWith(IRPCActionType.RELOAD_APP)
})
it('opens homepage and awesome list through openURL', () => {
const openURLMock = vi.mocked(openURL)
pluginsAdapter.openPluginHomepage('https://example.com')
pluginsAdapter.openAwesomeList()
expect(openURLMock).toHaveBeenNthCalledWith(1, 'https://example.com')
expect(openURLMock).toHaveBeenNthCalledWith(2, 'https://github.com/PicGo/Awesome-PicGo')
})
})
@@ -1,131 +0,0 @@
// @vitest-environment jsdom
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen, waitFor } from '@testing-library/react'
import type { PropsWithChildren } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AlbumSource } from '~/universal/types/cloudAlbum'
import { NavType } from '@/components/main/album/utils'
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key })
}))
vi.mock('@/adapters/cloud-album', () => ({
cloudAlbumAdapter: {
getStats: vi.fn()
}
}))
vi.mock('@/components/common/album-source-switcher', () => ({
AlbumSourceSwitcher: () => null
}))
vi.mock('@/components/common/cloud-feature-highlights', () => ({
CloudFeatureHighlights: () => null
}))
vi.mock('@/components/common/cloud-refresh-button', () => ({
CloudRefreshButton: () => null
}))
vi.mock('@/components/main/album/cloud-loading', () => ({
CloudSidebarSkeleton: () => <div data-testid="cloud-sidebar-skeleton" />
}))
vi.mock('@/store', () => ({
useAppStore: {
use: {
picBeds: () => [
{ type: 'picgo-cloud', name: 'PicGo Cloud', visible: true }
]
}
}
}))
import { cloudAlbumAdapter } from '@/adapters/cloud-album'
import { AlbumSidebar } from '@/components/main/album/album-sidebar'
const buildWrapper = () => {
const client = new QueryClient({
defaultOptions: { queries: { retry: false, gcTime: 0 } }
})
const Wrapper = ({ children }: PropsWithChildren) => (
<QueryClientProvider client={client}>{children}</QueryClientProvider>
)
return { client, Wrapper }
}
const baseProps = {
images: [],
providers: [],
navContext: { type: NavType.All, value: 'all' },
albumSource: AlbumSource.CLOUD,
isCloudAvailable: true,
onFilterChange: () => {}
}
describe('AlbumSidebar (cloud mode)', () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.clearAllMocks()
})
it('renders All Photos count and provider count from stats query', async () => {
vi.mocked(cloudAlbumAdapter.getStats).mockResolvedValue({
success: true,
data: { total: 7, types: [{ type: 'picgo-cloud', count: 7 }] }
} as unknown as Awaited<ReturnType<typeof cloudAlbumAdapter.getStats>>)
const { Wrapper } = buildWrapper()
render(<AlbumSidebar {...baseProps} />, { wrapper: Wrapper })
await waitFor(() => {
expect(screen.getByText('ALBUM_ALL_PHOTOS')).toBeTruthy()
expect(screen.getAllByText('7').length).toBeGreaterThanOrEqual(1)
expect(screen.getByText('PicGo Cloud')).toBeTruthy()
})
expect(cloudAlbumAdapter.getStats).toHaveBeenCalledOnce()
})
it('shows "—" for All Photos count when stats query errors out', async () => {
vi.mocked(cloudAlbumAdapter.getStats).mockResolvedValue({
success: false,
error: 'boom'
} as unknown as Awaited<ReturnType<typeof cloudAlbumAdapter.getStats>>)
const { Wrapper } = buildWrapper()
render(<AlbumSidebar {...baseProps} />, { wrapper: Wrapper })
await waitFor(() => {
expect(screen.getByText('—')).toBeTruthy()
})
})
it('does not call stats adapter when isCloudAvailable=false', async () => {
const { Wrapper } = buildWrapper()
render(
<AlbumSidebar {...baseProps} isCloudAvailable={false} />,
{ wrapper: Wrapper }
)
// give react-query a microtask
await Promise.resolve()
expect(cloudAlbumAdapter.getStats).not.toHaveBeenCalled()
})
it('does not call stats adapter when albumSource is LOCAL', async () => {
const { Wrapper } = buildWrapper()
render(
<AlbumSidebar {...baseProps} albumSource={AlbumSource.LOCAL} />,
{ wrapper: Wrapper }
)
await Promise.resolve()
expect(cloudAlbumAdapter.getStats).not.toHaveBeenCalled()
})
})
@@ -1,179 +0,0 @@
// @vitest-environment jsdom
import { act, render } from '@testing-library/react'
import { useEffect, useState } from 'react'
import { describe, expect, it } from 'vitest'
import { evaluatePluginConfig } from 'picgo'
import {
usePluginConfigRefresh,
type RefreshConfigSchemaTarget,
} from '@/components/common/use-plugin-config-refresh'
import type { ProviderPluginConfig } from '@/components/main/providers/types'
import {
REGION_ENDPOINTS,
ENDPOINT_BUCKETS,
buildCascadeRawSchema,
} from '../fixtures/cascade-fixture'
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
const DEBOUNCE = 20
const flushCascade = () => act(async () => { await wait(DEBOUNCE + 30) })
/**
* Hook-level cascade behavior. The "page-refresh saved-config preservation"
* tests previously here were moved to `provider-config-panel.spec.tsx` —
* they're now covered by mounting the real component, so the harness
* duplicates were removed. The tests below isolate cascade semantics that
* don't depend on the panel: schema diffing, default propagation, and the
* merger's stale-clearing rules.
*/
const buildInitialSchema = (): ProviderPluginConfig[] => {
return evaluatePluginConfig(
buildCascadeRawSchema() as Parameters<typeof evaluatePluginConfig>[0]
) as unknown as ProviderPluginConfig[]
}
interface HarnessState {
schema: ProviderPluginConfig[]
values: Record<string, unknown>
}
function Harness({
schemaRef,
setValuesRef,
}: {
schemaRef: { current: HarnessState | null }
setValuesRef: {
current: ((values: Record<string, unknown>) => void) | null
}
}) {
const initialSchema = buildInitialSchema()
const initialValues: Record<string, unknown> = {}
for (const field of initialSchema) {
if (field.default !== undefined) {
initialValues[field.name] = field.default
}
}
const [schema, setSchema] = useState<ProviderPluginConfig[]>(initialSchema)
const [values, setValues] = useState<Record<string, unknown>>(initialValues)
useEffect(() => {
schemaRef.current = { schema, values }
setValuesRef.current = setValues
}, [schema, values, schemaRef, setValuesRef])
const target: RefreshConfigSchemaTarget = {
target: 'uploader',
uploaderName: 'picgo-plugin-test',
}
usePluginConfigRefresh({
enabled: true,
target,
currentSchema: schema,
currentValues: values,
fetchSchema: async (_target, draftValues) =>
evaluatePluginConfig(
buildCascadeRawSchema() as Parameters<typeof evaluatePluginConfig>[0],
draftValues
) as unknown as ProviderPluginConfig[],
onSchemaUpdate: (nextSchema, nextValues) => {
setSchema(nextSchema)
setValues(nextValues)
},
debounceMs: DEBOUNCE,
})
return null
}
function setupHarness() {
const schemaRef: { current: HarnessState | null } = { current: null }
const setValuesRef: {
current: ((values: Record<string, unknown>) => void) | null
} = { current: null }
render(<Harness schemaRef={schemaRef} setValuesRef={setValuesRef} />)
return { schemaRef, setValuesRef }
}
describe('cascade hook behavior (region -> endpoint -> bucket)', () => {
it('seeds initial form with the first defaults from each cascade level', () => {
const { schemaRef } = setupHarness()
const state = schemaRef.current!
expect(state.values.region).toBe('us')
expect(state.values.endpoint).toBe('s3.us-east-1')
const bucketField = state.schema.find((f) => f.name === 'bucket')!
expect(bucketField.choices).toEqual(ENDPOINT_BUCKETS['s3.us-east-1'])
})
it('cascades both endpoint and bucket when region changes (untouched values)', async () => {
const { schemaRef, setValuesRef } = setupHarness()
// Pretend the user picked the initial defaults
await act(async () => {
setValuesRef.current!({
region: 'us',
endpoint: 's3.us-east-1',
bucket: 'us-east-prod',
pathPrefix: '',
})
})
await flushCascade()
// Switch region to 'asia' — endpoint/bucket are stale
await act(async () => {
setValuesRef.current!({
region: 'asia',
endpoint: 's3.us-east-1',
bucket: 'us-east-prod',
pathPrefix: '',
})
})
await flushCascade()
const state = schemaRef.current!
expect(state.values.endpoint).toBe(REGION_ENDPOINTS.asia[0])
expect(state.values.bucket).toBeUndefined() // not in new bucket choices
const endpointField = state.schema.find((f) => f.name === 'endpoint')!
expect(endpointField.choices).toEqual(REGION_ENDPOINTS.asia)
const bucketField = state.schema.find((f) => f.name === 'bucket')!
expect(bucketField.choices).toEqual(ENDPOINT_BUCKETS[REGION_ENDPOINTS.asia[0]])
})
it('clears user-edited values when they no longer match new choices', async () => {
const { schemaRef, setValuesRef } = setupHarness()
// User explicitly picks the second us endpoint and matching bucket
await act(async () => {
setValuesRef.current!({
region: 'us',
endpoint: 's3.us-west-2',
bucket: 'us-west-prod',
pathPrefix: '',
})
})
await flushCascade()
// Switch region to 'asia' — both endpoint and bucket are now invalid
await act(async () => {
setValuesRef.current!({
region: 'asia',
endpoint: 's3.us-west-2',
bucket: 'us-west-prod',
pathPrefix: '',
})
})
await flushCascade()
const state = schemaRef.current!
expect(state.values.endpoint).toBeUndefined()
expect(state.values.bucket).toBeUndefined()
})
})
@@ -1,171 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
filterValuesBySchema,
mergePluginSchema,
} from '@/components/common/merge-plugin-schema'
import type { ProviderPluginConfig } from '@/components/main/providers/types'
const inputField = (name: string, defaultValue?: string): ProviderPluginConfig => ({
name,
type: 'input',
required: false,
default: defaultValue,
})
const listField = (
name: string,
choices: string[],
defaultValue?: string
): ProviderPluginConfig => ({
name,
type: 'list',
required: false,
default: defaultValue,
choices,
})
const checkboxField = (
name: string,
choices: string[]
): ProviderPluginConfig => ({
name,
type: 'checkbox',
required: false,
choices,
})
describe('mergePluginSchema', () => {
it('keeps the old value when the list field is present and the value is still valid', () => {
const oldSchema = [listField('uploader', ['github', 'gitee'])]
const newSchema = [listField('uploader', ['github', 'gitee', 'qiniu'])]
const result = mergePluginSchema(oldSchema, newSchema, { uploader: 'github' })
expect(result.values.uploader).toBe('github')
})
it('clears the value when the field is present but the value no longer matches new choices', () => {
const oldSchema = [listField('repo', ['a', 'b'])]
const newSchema = [listField('repo', ['c', 'd'])]
const result = mergePluginSchema(oldSchema, newSchema, { repo: 'a' })
expect(result.values.repo).toBeUndefined()
})
it('resets to new default when the field type changes', () => {
const oldSchema = [listField('mode', ['a', 'b'], 'a')]
const newSchema: ProviderPluginConfig[] = [inputField('mode', 'fresh')]
const result = mergePluginSchema(oldSchema, newSchema, { mode: 'a' })
expect(result.values.mode).toBe('fresh')
})
it('keeps the orphan value in mergedValues when the field disappears', () => {
const oldSchema = [listField('uploader', ['github']), inputField('branch')]
const newSchema = [listField('uploader', ['gitee'])]
const result = mergePluginSchema(oldSchema, newSchema, {
uploader: 'github',
branch: 'dev',
})
expect(result.values.branch).toBe('dev')
})
it('restores the orphan when the field re-appears after a later refresh', () => {
const stage1OldSchema = [listField('uploader', ['github']), inputField('branch')]
const stage1NewSchema = [listField('uploader', ['gitee'])]
const intermediate = mergePluginSchema(stage1OldSchema, stage1NewSchema, {
uploader: 'gitee',
branch: 'dev',
})
const stage2OldSchema = stage1NewSchema
const stage2NewSchema = [listField('uploader', ['github']), inputField('branch')]
const final = mergePluginSchema(stage2OldSchema, stage2NewSchema, intermediate.values)
expect(final.values.branch).toBe('dev')
})
it('replaces an input field value with the new default when the old value equals the old default (untouched)', () => {
const oldSchema: ProviderPluginConfig[] = [
inputField('apiVersion', 'v1'),
]
const newSchema: ProviderPluginConfig[] = [
inputField('apiVersion', 'v2'),
]
const result = mergePluginSchema(oldSchema, newSchema, { apiVersion: 'v1' })
expect(result.values.apiVersion).toBe('v2')
})
it('keeps an input field value when the user has clearly typed something different from the default', () => {
const oldSchema: ProviderPluginConfig[] = [
inputField('apiVersion', 'v1'),
]
const newSchema: ProviderPluginConfig[] = [
inputField('apiVersion', 'v2'),
]
const result = mergePluginSchema(oldSchema, newSchema, { apiVersion: 'custom' })
expect(result.values.apiVersion).toBe('custom')
})
it('seeds new fields with their default value', () => {
const oldSchema = [listField('uploader', ['github'])]
const newSchema = [listField('uploader', ['github']), inputField('branch', 'main')]
const result = mergePluginSchema(oldSchema, newSchema, { uploader: 'github' })
expect(result.values.branch).toBe('main')
})
it('handles a mixed scenario that triggers every rule at once', () => {
const oldSchema = [
// uploader: value 'github' was edited by the user (differs from old default 'gitee')
listField('uploader', ['github', 'gitee'], 'gitee'),
// repo: value 'a' equals old default — untouched-default rule should apply on refresh
listField('repo', ['a', 'b'], 'a'),
inputField('branch', 'main'),
checkboxField('flags', ['x', 'y', 'z']),
]
const newSchema = [
listField('uploader', ['github'], 'github'),
listField('repo', ['c', 'd'], 'c'),
// 'branch' field gone, becoming an orphan
checkboxField('flags', ['x', 'z']),
listField('newField', ['p', 'q'], 'p'),
]
const result = mergePluginSchema(oldSchema, newSchema, {
uploader: 'github',
repo: 'a',
branch: 'dev',
flags: ['x', 'y'],
})
// uploader: user-edited value still in new choices -> kept
expect(result.values.uploader).toBe('github')
// repo: value matched old default, defaults changed -> reset to new default
expect(result.values.repo).toBe('c')
// branch: orphan, kept in memory
expect(result.values.branch).toBe('dev')
// flags: 'y' no longer valid -> cleared
expect(result.values.flags).toBeUndefined()
// newField: brand new, seeded with new default
expect(result.values.newField).toBe('p')
})
})
describe('filterValuesBySchema', () => {
it('drops orphan keys that are not in the current schema', () => {
const schema = [listField('uploader', ['github']), inputField('branch')]
const filtered = filterValuesBySchema(schema, {
uploader: 'github',
branch: 'main',
legacy: 'should-be-dropped',
})
expect(filtered).toEqual({ uploader: 'github', branch: 'main' })
})
})
@@ -1,178 +0,0 @@
// @vitest-environment jsdom
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
ensureExpanded: vi.fn(),
ensureHydrated: vi.fn(async () => {}),
navigate: vi.fn(),
refreshConfigSchema: vi.fn(),
setHydrating: vi.fn()
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key })
}))
vi.mock('sonner', () => ({
toast: {
error: vi.fn(),
success: vi.fn(),
warning: vi.fn()
}
}))
vi.mock('@tanstack/react-router', () => ({
useNavigate: () => mocks.navigate,
useSearch: () => ({
uploader: 'tcyun',
configId: 'config-1'
})
}))
vi.mock('@/adapters/plugins', () => ({
pluginsAdapter: {
refreshConfigSchema: mocks.refreshConfigSchema
}
}))
vi.mock('@/store', () => ({
appActions: {
ensureHydrated: mocks.ensureHydrated
},
providerStoreActions: {
ensureExpanded: mocks.ensureExpanded,
setHydrating: mocks.setHydrating
},
useAppStore: {
use: {
appConfig: () => ({
picBed: {
uploader: 'tcyun'
},
uploader: {
tcyun: {
defaultId: 'config-1',
configList: [
{
_id: 'config-1',
_configName: 'Existing Config',
_createdAt: 1700000000000,
_updatedAt: 1700000000000,
secretKey: 'existing-secret-key'
}
]
}
}
}),
providers: () => [
{
id: 'tcyun',
name: 'Tencent Cloud',
visible: true,
isDefaultUploader: true
}
],
hasHydrated: () => true
}
},
useProviderStore: {
use: {
isHydrating: () => false
}
}
}))
vi.mock('@/components/main/providers/provider-sidebar', () => ({
ProviderSidebar: ({
onCreateIntent
}: {
onCreateIntent: (uploaderId: string) => void
}) => (
<button type='button' onClick={() => onCreateIntent('tcyun')}>
Open create dialog
</button>
)
}))
vi.mock('@/components/main/providers/provider-config-panel', () => ({
ProviderConfigPanel: ({
draftConfigMap
}: {
draftConfigMap: Record<string, unknown>
}) => (
<output data-testid='draft-config'>{JSON.stringify(draftConfigMap)}</output>
)
}))
vi.mock('@/components/main/providers/provider-config-name-dialog', () => ({
ProviderConfigNameDialog: ({
state,
onSubmit
}: {
state: { name: string } | null
onSubmit: () => Promise<void>
}) => state
? (
<button type='button' onClick={async () => await onSubmit()}>
Submit create dialog
</button>
)
: null
}))
vi.mock('@/components/main/providers/provider-delete-config-dialog', () => ({
ProviderDeleteConfigDialog: () => null
}))
import { PicGoProviders } from '@/components/main/providers/picgo-providers'
describe('PicGoProviders create config', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.refreshConfigSchema.mockResolvedValue([
{
name: 'version',
type: 'list',
choices: ['v4', 'v5'],
default: 'v5',
required: false
},
{
name: 'secretKey',
type: 'password',
default: '',
required: true
}
])
})
it('requests schema-only defaults and creates an empty credential draft', async () => {
render(<PicGoProviders />)
fireEvent.click(screen.getByRole('button', { name: 'Open create dialog' }))
fireEvent.click(screen.getByRole('button', { name: 'Submit create dialog' }))
await waitFor(() => {
expect(mocks.refreshConfigSchema).toHaveBeenCalledWith({
target: 'uploader',
uploaderName: 'tcyun',
draftValues: {},
schemaOnly: true
})
})
await waitFor(() => {
const draftMap = JSON.parse(
screen.getByTestId('draft-config').textContent ?? '{}'
) as Record<string, Record<string, unknown>>
expect(draftMap.tcyun).toMatchObject({
_configName: 'New Config',
_isDraft: true,
version: 'v5',
secretKey: ''
})
})
})
})
@@ -1,73 +0,0 @@
// Regression spec for the plugin config save→read path consistency.
//
// picgo-core stores per-plugin configs at the root of the config object via
// `picgo.saveConfig({ [fullName]: values })` (same convention the CLI uses).
// The renderer's `normalizeAppConfig` rolls those root entries up into
// `appConfig.plugins[fullName]` by cross-referencing the `picgoPlugins` install
// map, so the panel and CLI read the same place.
import { describe, expect, it } from 'vitest'
import { set as lodashSet } from 'lodash'
import { normalizeAppConfig } from '@/store/utils'
describe('plugin config save path', () => {
it('rolls up root-level plugin entries into appConfig.plugins via picgoPlugins install map', () => {
const rawConfig: Record<string, unknown> = {
picBed: { uploader: 'smms', current: 'smms', transformer: 'path' },
picgoPlugins: { 'picgo-plugin-test': true },
settings: {},
uploader: {},
transformer: {},
needReload: false
}
// Simulates picgo.saveConfig({ 'picgo-plugin-test': { ... } })
lodashSet(rawConfig, 'picgo-plugin-test', {
mode: 'advanced',
verbosity: 'debug',
apiVersion: 'v2'
})
const normalized = normalizeAppConfig(rawConfig as never, [])
expect(normalized?.plugins['picgo-plugin-test']).toEqual({
mode: 'advanced',
verbosity: 'debug',
apiVersion: 'v2'
})
})
it('ignores root entries that do not correspond to an installed plugin', () => {
const rawConfig: Record<string, unknown> = {
picBed: { uploader: 'smms', current: 'smms', transformer: 'path' },
picgoPlugins: {},
settings: {},
uploader: {},
transformer: {},
needReload: false
}
lodashSet(rawConfig, 'picgo-plugin-test', { mode: 'advanced' })
const normalized = normalizeAppConfig(rawConfig as never, [])
// Not installed → not rolled up.
expect(normalized?.plugins['picgo-plugin-test']).toBeUndefined()
})
it('falls back to a pre-existing nested plugins entry when no root entry exists', () => {
const rawConfig: Record<string, unknown> = {
picBed: { uploader: 'smms', current: 'smms', transformer: 'path' },
picgoPlugins: { 'picgo-plugin-test': true },
settings: {},
uploader: {},
transformer: {},
plugins: {
'picgo-plugin-test': { mode: 'legacy' }
},
needReload: false
}
const normalized = normalizeAppConfig(rawConfig as never, [])
expect(normalized?.plugins['picgo-plugin-test']).toEqual({ mode: 'legacy' })
})
})
@@ -1,332 +0,0 @@
// @vitest-environment jsdom
import { act, render, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { evaluatePluginConfig } from 'picgo'
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key })
}))
vi.mock('@/adapters/plugins', () => ({
pluginsAdapter: {
refreshConfigSchema: vi.fn(async () => []),
savePluginConfig: vi.fn(async () => {}),
getInstalledPlugins: vi.fn(async () => [])
}
}))
vi.mock('@/store/app-actions', () => ({
appActions: {
refreshAppConfig: vi.fn(async () => {}),
hydrateAppState: vi.fn(async () => {})
}
}))
import { PluginDetailPanel } from '@/components/main/plugins/plugin-detail-panel'
import { useAppStore } from '@/store/app-store'
import { usePluginStore } from '@/store/plugins/store'
import { pluginStoreActions } from '@/store/plugins/actions'
import { pluginsAdapter } from '@/adapters/plugins'
import { appActions } from '@/store/app-actions'
import { normalizePluginConfigSchema } from '@/components/common/normalize-plugin-schema'
import { pluginDetailTab, type PluginInstalledItem } from '@/components/main/plugins/types'
import { IPasteStyle, IStartupMode } from '~/universal/types/enum'
// Raw plugin config schema mirroring the picgo-plugin-test test plugin.
// mode → verbosity (default debug/info), apiVersion (default v2/v1).
const buildRawPluginSchema = (): unknown[] => [
{
name: 'mode',
type: 'list',
required: true,
alias: 'Mode',
default: 'basic',
choices: ['basic', 'advanced']
},
{
name: 'verbosity',
type: 'list',
required: false,
alias: 'Verbosity',
dependsOn: ['mode'],
default: (answers: Record<string, unknown>) =>
(answers.mode === 'advanced' ? 'debug' : 'info'),
choices: (answers: Record<string, unknown>) =>
(answers.mode === 'advanced'
? ['silent', 'info', 'debug', 'trace']
: ['silent', 'info'])
},
{
name: 'apiVersion',
type: 'list',
required: false,
alias: 'API version',
dependsOn: ['mode'],
default: (answers: Record<string, unknown>) =>
(answers.mode === 'advanced' ? 'v2' : 'v1'),
choices: (answers: Record<string, unknown>) =>
(answers.mode === 'advanced' ? ['v2', 'v2-beta'] : ['v1', 'v1-legacy'])
}
]
const baseAppConfig = {
picBed: {
uploader: 'smms',
current: 'smms',
transformer: 'path',
proxy: '',
list: []
},
uploader: {} as Record<string, unknown>,
settings: {
appearance: 'auto',
pasteStyle: IPasteStyle.MARKDOWN,
showUpdateTip: false,
autoStart: false,
rename: false,
autoRename: false,
uploadNotification: false,
notificationSound: true,
miniWindowOnTop: false,
logLevel: ['all'],
autoCopyUrl: true,
checkBetaUpdate: true,
useBuiltinClipboard: false,
language: 'en',
logFileSizeLimit: 10,
encodeOutputURL: false,
showDockIcon: true,
showMenubarIcon: true,
customLink: '$url',
npmProxy: '',
npmRegistry: '',
server: { port: 36677, host: '127.0.0.1', enable: true },
startupMode: IStartupMode.HIDE,
shortKey: {},
urlRewrite: { rules: [] }
},
picgoPlugins: {},
plugins: {} as Record<string, Record<string, unknown>>,
transformer: {},
needReload: false
}
const FULL_NAME = 'picgo-plugin-test'
function buildInstalledPlugin(): PluginInstalledItem {
const schemaEvaluated = evaluatePluginConfig(
buildRawPluginSchema() as Parameters<typeof evaluatePluginConfig>[0]
) as unknown[]
return {
name: 'test',
fullName: FULL_NAME,
author: 'tester',
description: 'for test',
logo: '',
version: '1.0.0',
gui: false,
homepage: '',
enabled: true,
hasInstall: true,
guiMenu: [],
config: {
plugin: {
name: 'test',
fullName: FULL_NAME,
config: normalizePluginConfigSchema(schemaEvaluated)
},
transformer: { name: '', fullName: undefined, config: [] }
},
uploader: undefined
}
}
function setupStore(pluginConfigInAppConfig: Record<string, unknown> | undefined) {
const installedPlugin = buildInstalledPlugin()
useAppStore.setState({
defaultPicBed: 'smms',
appConfig: {
...baseAppConfig,
plugins: pluginConfigInAppConfig
? { [FULL_NAME]: pluginConfigInAppConfig }
: {}
} as never,
picBeds: [],
providers: [],
providerSchemas: {},
pluginsInstalled: [installedPlugin],
settingsVersion: { currentVersion: '2.5.3', latestVersion: null },
hasHydrated: true,
hasSettingsHydrated: true,
picgoCloud: {
loginStatus: 'IDLE',
loginError: null,
hasAgreedToTermsAndPrivacy: false
}
} as never)
usePluginStore.setState({
searchValue: '',
exactMatch: false,
rawSearchResults: [],
searchResults: [],
isSearching: false,
isImportingLocal: false,
isMutatingByPlugin: {},
readmeByPlugin: {}
})
return installedPlugin
}
function getFieldSelectTrigger(label: string) {
const labelEl = screen.getByText(label)
const fieldContainer = labelEl.closest('[data-slot="field"]')
if (!fieldContainer) throw new Error(`No field container for "${label}"`)
const trigger = fieldContainer.querySelector('[role="combobox"]')
if (!trigger) throw new Error(`No combobox trigger inside "${label}" field`)
return trigger as HTMLElement
}
function mountPanel(plugin: PluginInstalledItem) {
const appConfig = useAppStore.getState().appConfig
return render(
<PluginDetailPanel
appConfig={appConfig}
selectedItem={{
name: plugin.name,
fullName: plugin.fullName,
author: plugin.author,
description: plugin.description,
logo: plugin.logo,
version: plugin.version,
homepage: plugin.homepage,
hasInstall: true,
installedPlugin: plugin
} as never}
plugin={plugin}
activeTab={pluginDetailTab.Config}
availableTabs={[pluginDetailTab.Readme, pluginDetailTab.Config]}
readmeState={null}
isMutating={false}
onTabChange={vi.fn()}
/>
)
}
describe('PluginDetailPanel — config tab persistence (issue diagnostic)', () => {
beforeEach(() => {
vi.clearAllMocks()
// Schema RPC default: re-evaluate with given draftValues (simulates main).
vi.mocked(pluginsAdapter.refreshConfigSchema).mockImplementation(
async (payload) => {
const draftValues = (payload as { draftValues: Record<string, unknown> })
.draftValues
const evaluated = evaluatePluginConfig(
buildRawPluginSchema() as Parameters<typeof evaluatePluginConfig>[0],
draftValues
) as unknown[]
return normalizePluginConfigSchema(evaluated)
}
)
})
it('shows saved advanced/debug/v2 when appConfig.plugins[fullName] has those values', async () => {
// Simulates the desired post-save state: appConfig.plugins[fullName] has the
// values the user just saved.
const plugin = setupStore({
mode: 'advanced',
verbosity: 'debug',
apiVersion: 'v2'
})
mountPanel(plugin)
// No initial sync wired yet in the panel — schema stays basic-state.
// But form values come from appConfig.plugins[fullName], so mode should
// at least show 'advanced'.
await waitFor(() => {
const modeTrigger = getFieldSelectTrigger('Mode')
expect(modeTrigger.textContent).toContain('advanced')
})
// Without the schema sync, verbosity/apiVersion values are saved but
// the basic-state choices don't include them, so they render as
// placeholder (field.name). This documents the current broken state
// and gives us a failing assertion to fix.
const verbosityTrigger = getFieldSelectTrigger('Verbosity')
const apiVersionTrigger = getFieldSelectTrigger('API version')
// After fix: these should contain 'debug' / 'v2'
expect(verbosityTrigger.textContent).toContain('debug')
expect(apiVersionTrigger.textContent).toContain('v2')
})
it('shows saved values end-to-end: user toggles mode, clicks Confirm, form retains new values', async () => {
// Step 1: empty starting state — no saved plugin config.
const plugin = setupStore(undefined)
const { rerender } = mountPanel(plugin)
// Step 2: simulate the cascade — user changed mode to 'advanced',
// hook fetched new schema, form values updated to advanced/debug/v2.
// We can't drive Radix Select interactions in jsdom reliably, so
// we simulate the save call directly with the post-cascade values.
vi.mocked(pluginsAdapter.savePluginConfig).mockResolvedValueOnce(undefined)
vi.mocked(pluginsAdapter.getInstalledPlugins).mockResolvedValueOnce([
plugin as never
])
vi.mocked(appActions.refreshAppConfig).mockImplementationOnce(async () => {
// Simulate what refreshAppConfig SHOULD do after picgo-core writes the
// plugin config: appConfig.plugins[fullName] becomes the saved values.
useAppStore.setState((state) => {
if (!state.appConfig) return
state.appConfig.plugins[FULL_NAME] = {
mode: 'advanced',
verbosity: 'debug',
apiVersion: 'v2'
}
})
})
await act(async () => {
await pluginStoreActions.savePluginConfig(FULL_NAME, 'config', {
mode: 'advanced',
verbosity: 'debug',
apiVersion: 'v2'
})
})
// Re-render so the panel picks up new appConfig reference.
const nextAppConfig = useAppStore.getState().appConfig
rerender(
<PluginDetailPanel
appConfig={nextAppConfig}
selectedItem={{
name: plugin.name,
fullName: plugin.fullName,
author: plugin.author,
description: plugin.description,
logo: plugin.logo,
version: plugin.version,
homepage: plugin.homepage,
hasInstall: true,
installedPlugin: plugin
} as never}
plugin={plugin}
activeTab={pluginDetailTab.Config}
availableTabs={[pluginDetailTab.Readme, pluginDetailTab.Config]}
readmeState={null}
isMutating={false}
onTabChange={vi.fn()}
/>
)
// Verify saved values are visible after save.
await waitFor(() => {
const modeTrigger = getFieldSelectTrigger('Mode')
expect(modeTrigger.textContent).toContain('advanced')
})
expect(getFieldSelectTrigger('Verbosity').textContent).toContain('debug')
expect(getFieldSelectTrigger('API version').textContent).toContain('v2')
})
})
@@ -1,191 +0,0 @@
// @vitest-environment jsdom
import { render, screen , fireEvent } from '@testing-library/react'
import { describe, expect, it, vi, beforeEach } from 'vitest'
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key
})
}))
vi.mock('@/adapters/plugins', () => ({
pluginsAdapter: {
reloadApp: vi.fn()
}
}))
import { AppReloadBar } from '@/components/common/app-reload-bar'
import { PluginSidebar, type PluginSidebarListItem } from '@/components/main/plugins/plugin-sidebar'
import { useAppStore } from '@/store/app-store'
import { usePluginStore } from '@/store/plugins/store'
import { pluginsAdapter } from '@/adapters/plugins'
import { IPasteStyle, IStartupMode } from '~/universal/types/enum'
function resetStores () {
useAppStore.setState({
defaultPicBed: 'smms',
appConfig: {
picBed: {
uploader: 'smms',
current: 'smms',
transformer: 'path',
proxy: '',
list: []
},
uploader: {},
settings: {
appearance: 'auto',
pasteStyle: IPasteStyle.MARKDOWN,
showUpdateTip: false,
autoStart: false,
rename: false,
autoRename: false,
uploadNotification: false,
notificationSound: true,
miniWindowOnTop: false,
logLevel: ['all'],
autoCopyUrl: true,
checkBetaUpdate: true,
useBuiltinClipboard: false,
language: 'en',
logFileSizeLimit: 10,
encodeOutputURL: false,
showDockIcon: true,
showMenubarIcon: true,
customLink: '$url',
npmProxy: '',
npmRegistry: '',
server: {
port: 36677,
host: '127.0.0.1',
enable: true
},
startupMode: IStartupMode.HIDE,
shortKey: {},
urlRewrite: {
rules: []
}
},
picgoPlugins: {},
plugins: {},
transformer: {},
needReload: false
},
picBeds: [],
providers: [],
providerSchemas: {},
pluginsInstalled: [],
settingsVersion: {
currentVersion: '2.5.3',
latestVersion: null
},
hasHydrated: true,
hasSettingsHydrated: true,
picgoCloud: {
loginStatus: 'IDLE',
loginError: null,
hasAgreedToTermsAndPrivacy: false
}
})
usePluginStore.setState({
searchValue: '',
exactMatch: false,
rawSearchResults: [],
searchResults: [],
isSearching: false,
isImportingLocal: false,
isMutatingByPlugin: {},
readmeByPlugin: {}
})
}
function createSidebarItems (): PluginSidebarListItem[] {
return [
{
fullName: 'picgo-plugin-cloudflare-r2-xqv',
name: 'cloudflare-r2-xqv',
description: 'picgo for cloudflare-r2 storage',
author: 'xiaoqinvar',
version: '1.0.4',
logo: '',
homepage: '',
hasInstall: true,
installedPlugin: {
name: 'cloudflare-r2-xqv',
fullName: 'picgo-plugin-cloudflare-r2-xqv',
author: 'xiaoqinvar',
description: 'picgo for cloudflare-r2 storage',
logo: '',
version: '1.0.4',
gui: true,
homepage: '',
enabled: true,
hasInstall: true,
guiMenu: [],
config: {
plugin: { name: 'x', fullName: 'x', config: [] },
transformer: { name: 'path', fullName: 'path', config: [] }
}
}
}
]
}
describe('renderer/plugins components', () => {
beforeEach(() => {
vi.clearAllMocks()
resetStores()
})
it('shows reload bar only when needReload is true and triggers reload action', async () => {
const { rerender } = render(<AppReloadBar />)
expect(screen.queryByText('TIPS_NEED_RELOAD')).not.toBeInTheDocument()
useAppStore.setState((state) => {
if (state.appConfig) {
state.appConfig.needReload = true
}
})
rerender(<AppReloadBar />)
const button = await screen.findByRole('button', { name: 'TIPS_NEED_RELOAD' })
fireEvent.click(button)
expect(vi.mocked(pluginsAdapter.reloadApp)).toHaveBeenCalled()
})
it('shows import loading state and clears search input through store actions', async () => {
const onImportLocalPlugin = vi.fn()
usePluginStore.setState({
searchValue: 'cloudflare',
isImportingLocal: true
})
render(
<PluginSidebar
items={createSidebarItems()}
selectedPluginFullName={null}
onSelectPlugin={vi.fn()}
onInstallPlugin={vi.fn()}
onOpenAwesomeList={vi.fn()}
onImportLocalPlugin={onImportLocalPlugin}
onOpenPluginMenu={vi.fn()}
/>
)
expect(screen.getByRole('textbox', { name: 'SEARCH' })).toHaveValue('cloudflare')
const clearButton = screen.getByRole('button', { name: 'ALBUM_CLEAR_SELECTION' })
fireEvent.click(clearButton)
expect(usePluginStore.getState().searchValue).toBe('')
const importButtons = screen.getAllByRole('button')
const disabledImportButton = importButtons.find((button) => button.hasAttribute('disabled'))
expect(disabledImportButton).toBeDefined()
})
})
@@ -1,497 +0,0 @@
// @vitest-environment jsdom
import { act, render, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { evaluatePluginConfig } from 'picgo'
const routerMocks = vi.hoisted(() => ({
search: {
uploader: 'picgo-plugin-test',
configId: 'config-1'
}
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key })
}))
vi.mock('@tanstack/react-router', async () => {
const actual = await vi.importActual<typeof import('@tanstack/react-router')>(
'@tanstack/react-router'
)
return {
...actual,
useNavigate: () => vi.fn(),
useSearch: () => routerMocks.search
}
})
vi.mock('@/adapters/plugins', () => ({
pluginsAdapter: {
refreshConfigSchema: vi.fn(async () => [])
}
}))
vi.mock('@/store/providers/actions', () => ({
providerStoreActions: {
ensureSchema: vi.fn(async () => {}),
setDefaultConfig: vi.fn(),
setLoadingByProvider: vi.fn(),
saveConfig: vi.fn(),
createConfig: vi.fn()
}
}))
import { ProviderConfigPanel } from '@/components/main/providers/provider-config-panel'
import type { ProviderDraftConfigItem } from '@/components/main/providers/types'
import { useAppStore } from '@/store/app-store'
import { useProviderStoreBase as useProviderStore } from '@/store/providers/store'
import { pluginsAdapter } from '@/adapters/plugins'
import { normalizePluginConfigSchema } from '@/components/common/normalize-plugin-schema'
import { IPasteStyle, IStartupMode } from '~/universal/types/enum'
import { buildCascadeRawSchema } from '../fixtures/cascade-fixture'
beforeEach(() => {
routerMocks.search.uploader = 'picgo-plugin-test'
routerMocks.search.configId = 'config-1'
})
const baseAppConfig = {
picBed: {
uploader: 'picgo-plugin-test',
current: 'picgo-plugin-test',
transformer: 'path',
proxy: '',
list: []
},
uploader: {} as Record<string, unknown>,
settings: {
appearance: 'auto',
pasteStyle: IPasteStyle.MARKDOWN,
showUpdateTip: false,
autoStart: false,
rename: false,
autoRename: false,
uploadNotification: false,
notificationSound: true,
miniWindowOnTop: false,
logLevel: ['all'],
autoCopyUrl: true,
checkBetaUpdate: true,
useBuiltinClipboard: false,
language: 'en',
logFileSizeLimit: 10,
encodeOutputURL: false,
showDockIcon: true,
showMenubarIcon: true,
customLink: '$url',
npmProxy: '',
npmRegistry: '',
server: { port: 36677, host: '127.0.0.1', enable: true },
startupMode: IStartupMode.HIDE,
shortKey: {},
urlRewrite: { rules: [] }
},
picgoPlugins: {},
plugins: {},
transformer: {},
needReload: false
}
function setupStoreWithSavedConfig(
savedConfig: Record<string, unknown>,
schemaOverride?: unknown[]
) {
// Mimics what the main process sends at startup: schema evaluated with
// empty answers, so the plugin's `default(answers)` for downstream fields
// computes against synthAnswers defaults — NOT the user's saved values.
// Provider-config-panel then has to issue an initial sync to bring the
// schema in line with the saved values.
const initialSchema = schemaOverride ??
evaluatePluginConfig(
buildCascadeRawSchema(savedConfig.region as string) as Parameters<
typeof evaluatePluginConfig
>[0]
) as unknown[]
useAppStore.setState({
defaultPicBed: 'picgo-plugin-test',
appConfig: {
...baseAppConfig,
uploader: {
'picgo-plugin-test': {
defaultId: 'config-1',
configList: [
{
_id: 'config-1',
_configName: 'New Config',
_createdAt: 1700000000000,
_updatedAt: 1700000000000,
...savedConfig
}
]
}
}
} as never,
picBeds: [],
providers: [
{
id: 'picgo-plugin-test',
name: 'PicGo Test',
visible: true,
isDefaultUploader: true
}
],
providerSchemas: {
'picgo-plugin-test': {
id: 'picgo-plugin-test',
name: 'PicGo Test',
config: normalizePluginConfigSchema(initialSchema)
}
},
pluginsInstalled: [],
settingsVersion: { currentVersion: '2.5.3', latestVersion: null },
hasHydrated: true,
hasSettingsHydrated: true,
picgoCloud: {
loginStatus: 'IDLE',
loginError: null,
hasAgreedToTermsAndPrivacy: false
}
} as never)
useProviderStore.setState({
isHydrating: false,
isLoadingByProvider: {},
expandedProviderIds: [],
searchValue: ''
})
}
// Mock fetchSchema to mirror what the main side would compute when given
// `draftValues` — i.e. re-evaluate the raw schema with those answers so
// downstream choices/defaults reflect them.
function mockMainSideEvaluator() {
vi.mocked(pluginsAdapter.refreshConfigSchema).mockImplementation(
async (payload) => {
const draftValues = (payload as { draftValues: Record<string, unknown> })
.draftValues
const evaluated = evaluatePluginConfig(
buildCascadeRawSchema(draftValues.region as string | undefined) as Parameters<
typeof evaluatePluginConfig
>[0],
draftValues
) as unknown[]
return normalizePluginConfigSchema(evaluated)
}
)
}
function getFieldSelectTrigger(label: string) {
const labelEl = screen.getByText(label)
const fieldContainer = labelEl.closest('[data-slot="field"]')
if (!fieldContainer) throw new Error(`No field container for "${label}"`)
const trigger = fieldContainer.querySelector('[role="combobox"]')
if (!trigger) throw new Error(`No combobox trigger inside "${label}" field`)
return trigger as HTMLElement
}
function renderPanel(
draftConfigMap: Record<string, ProviderDraftConfigItem | undefined> = {}
) {
return render(
<ProviderConfigPanel
draftConfigMap={draftConfigMap}
setDraftConfigMap={vi.fn()}
onCreateConfigIntent={vi.fn()}
onDeleteConfigIntent={vi.fn()}
/>
)
}
describe('ProviderConfigPanel — page-refresh saved config rendering', () => {
beforeEach(() => {
vi.clearAllMocks()
mockMainSideEvaluator()
})
it('renders the saved bucket value in the Select trigger after page refresh', async () => {
setupStoreWithSavedConfig({
region: 'eu',
endpoint: 's3.eu-central-1',
bucket: 'eu-central-prod',
pathPrefix: ''
})
renderPanel()
await waitFor(
() => {
const bucketTrigger = getFieldSelectTrigger('Bucket')
expect(bucketTrigger.textContent).toContain('eu-central-prod')
},
{ timeout: 1000 }
)
expect(getFieldSelectTrigger('Region').textContent).toContain('eu')
expect(getFieldSelectTrigger('Endpoint').textContent).toContain(
's3.eu-central-1'
)
})
it('triggers an initial schema sync with the saved values when mounting', async () => {
setupStoreWithSavedConfig({
region: 'eu',
endpoint: 's3.eu-central-1',
bucket: 'eu-central-prod',
pathPrefix: ''
})
renderPanel()
await waitFor(() => {
expect(pluginsAdapter.refreshConfigSchema).toHaveBeenCalled()
})
const firstCall = vi.mocked(pluginsAdapter.refreshConfigSchema).mock
.calls[0]?.[0] as {
target: string
uploaderName: string
draftValues: Record<string, unknown>
}
expect(firstCall?.target).toBe('uploader')
expect(firstCall?.uploaderName).toBe('picgo-plugin-test')
expect(firstCall?.draftValues).toMatchObject({
region: 'eu',
endpoint: 's3.eu-central-1',
bucket: 'eu-central-prod'
})
})
it('does not clear saved values when the initial schema sync resolves', async () => {
setupStoreWithSavedConfig({
region: 'eu',
endpoint: 's3.eu-central-1',
bucket: 'eu-central-prod',
pathPrefix: ''
})
renderPanel()
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 200))
})
expect(getFieldSelectTrigger('Region').textContent).toContain('eu')
expect(getFieldSelectTrigger('Endpoint').textContent).toContain(
's3.eu-central-1'
)
expect(getFieldSelectTrigger('Bucket').textContent).toContain(
'eu-central-prod'
)
})
})
// User-driven cascade (region change → endpoint/bucket update) is verified
// at the hook level in `cascade-end-to-end.spec.tsx` — Radix Select in jsdom
// requires `@testing-library/user-event` (not installed) to reliably open
// its portal dropdown, and the panel here is a thin wrapper around the
// same hook. Adding a panel-level cascade test would duplicate coverage
// without adding signal.
describe('ProviderConfigPanel — editor field rendering', () => {
beforeEach(() => {
vi.clearAllMocks()
mockMainSideEvaluator()
})
it('renders a textarea for a type: editor field and binds its saved value', async () => {
// Re-setup the store with a schema that includes a type: editor field.
// We bypass the cascade fixture here because editor type is orthogonal
// to the region/endpoint/bucket cascade and we just want to verify the
// SchemaFormFields editor branch surfaces through the panel render path.
useAppStore.setState({
defaultPicBed: 'picgo-plugin-test',
appConfig: {
...baseAppConfig,
uploader: {
'picgo-plugin-test': {
defaultId: 'config-1',
configList: [
{
_id: 'config-1',
_configName: 'New Config',
_createdAt: 1700000000000,
_updatedAt: 1700000000000,
script: 'line-one\nline-two\nline-three'
}
]
}
}
} as never,
picBeds: [],
providers: [
{
id: 'picgo-plugin-test',
name: 'PicGo Test',
visible: true,
isDefaultUploader: true
}
],
providerSchemas: {
'picgo-plugin-test': {
id: 'picgo-plugin-test',
name: 'PicGo Test',
config: normalizePluginConfigSchema([
{
name: 'script',
type: 'editor',
alias: 'Compression script',
required: true,
message: 'Enter your compression script'
}
])
}
},
pluginsInstalled: [],
settingsVersion: { currentVersion: '2.5.3', latestVersion: null },
hasHydrated: true,
hasSettingsHydrated: true,
picgoCloud: {
loginStatus: 'IDLE',
loginError: null,
hasAgreedToTermsAndPrivacy: false
}
} as never)
useProviderStore.setState({
isHydrating: false,
isLoadingByProvider: {},
expandedProviderIds: [],
searchValue: ''
})
renderPanel()
await waitFor(() => {
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
expect(textarea.tagName).toBe('TEXTAREA')
expect(textarea.value).toBe('line-one\nline-two\nline-three')
})
})
})
describe('ProviderConfigPanel — saved password rendering', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('keeps saved password fields masked without a reveal button', async () => {
const passwordSchema = [
{
name: 'secret',
type: 'password',
alias: 'Secret',
required: true,
message: 'Enter secret'
}
]
setupStoreWithSavedConfig(
{ secret: 'saved-secret' },
passwordSchema
)
vi.mocked(pluginsAdapter.refreshConfigSchema).mockResolvedValue(
normalizePluginConfigSchema(passwordSchema)
)
renderPanel()
await waitFor(() => {
const label = screen.getByText('Secret')
const field = label.closest('[data-slot="field"]')
if (!field) throw new Error('No field container for password field')
const input = field.querySelector('input')
expect(input?.value).toBe('saved-secret')
expect(input?.type).toBe('password')
expect(field.querySelector('button')).toBeNull()
})
})
})
describe('ProviderConfigPanel — draft schema rendering', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('keeps schema refreshes isolated from persisted uploader defaults', async () => {
const draftId = 'draft:picgo-plugin-test'
const cachedSchema = [
{
name: 'version',
type: 'list',
choices: ['v4', 'v5'],
default: 'v5',
required: false
},
{
name: 'secret',
type: 'password',
alias: 'Secret',
default: 'persisted-secret',
required: true
}
]
const cleanSchema = [
cachedSchema[0],
{
...cachedSchema[1],
default: ''
}
]
setupStoreWithSavedConfig(
{
version: 'v5',
secret: 'persisted-secret'
},
cachedSchema
)
routerMocks.search.configId = draftId
vi.mocked(pluginsAdapter.refreshConfigSchema).mockResolvedValue(
normalizePluginConfigSchema(cleanSchema)
)
renderPanel({
'picgo-plugin-test': {
_id: draftId,
_configName: 'New Config',
_createdAt: 1700000000001,
_updatedAt: 1700000000001,
_isDraft: true,
version: 'v5',
secret: ''
}
})
await waitFor(() => {
expect(pluginsAdapter.refreshConfigSchema).toHaveBeenCalledWith(
expect.objectContaining({
target: 'uploader',
uploaderName: 'picgo-plugin-test',
schemaOnly: true
})
)
})
await waitFor(() => {
const label = screen.getByText('Secret')
const field = label.closest('[data-slot="field"]')
if (!field) throw new Error('No field container for draft password field')
const input = field.querySelector('input')
expect(input?.value).toBe('')
expect(input?.type).toBe('password')
expect(field.querySelector('button')).not.toBeNull()
})
})
})
@@ -1,219 +0,0 @@
// @vitest-environment jsdom
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key })
}))
import { SchemaFormFields } from '@/components/common/schema-form-fields'
import type { ProviderPluginConfig } from '@/components/main/providers/types'
const renderSchema = (
schema: ProviderPluginConfig[],
values: Record<string, unknown> = {},
fieldErrors: Record<string, string | undefined> = {}
) => {
const onValueChange = vi.fn()
const utils = render(
<SchemaFormFields
schema={schema}
values={values}
fieldErrors={fieldErrors}
onValueChange={onValueChange}
/>
)
return { ...utils, onValueChange }
}
describe('SchemaFormFields editor field', () => {
it('renders a textarea for type: editor', () => {
renderSchema([
{ name: 'script', type: 'editor', required: false }
])
const textarea = screen.getByRole('textbox')
expect(textarea).toBeTruthy()
expect(textarea.tagName).toBe('TEXTAREA')
expect(textarea.getAttribute('data-slot')).toBe('textarea')
})
it('binds the textarea value from values map', () => {
renderSchema(
[{ name: 'script', type: 'editor', required: false }],
{ script: 'line1\nline2\nline3' }
)
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
expect(textarea.value).toBe('line1\nline2\nline3')
})
it('fires onValueChange with the full multi-line value on input', () => {
const { onValueChange } = renderSchema([
{ name: 'script', type: 'editor', required: false }
])
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
fireEvent.change(textarea, { target: { value: 'a\nb\nc' } })
expect(onValueChange).toHaveBeenCalledWith('script', 'a\nb\nc')
})
it('uses field.message as placeholder, falling back to field.name', () => {
const { rerender } = renderSchema([
{
name: 'script',
type: 'editor',
required: false,
message: 'Enter multi-line script'
}
])
let textarea = screen.getByRole('textbox') as HTMLTextAreaElement
expect(textarea.placeholder).toBe('Enter multi-line script')
rerender(
<SchemaFormFields
schema={[{ name: 'script', type: 'editor', required: false }]}
values={{}}
fieldErrors={{}}
onValueChange={vi.fn()}
/>
)
textarea = screen.getByRole('textbox') as HTMLTextAreaElement
expect(textarea.placeholder).toBe('script')
})
it('marks textarea as invalid when fieldErrors contains the field', () => {
renderSchema(
[{ name: 'script', type: 'editor', required: true }],
{},
{ script: 'required' }
)
const textarea = screen.getByRole('textbox')
expect(textarea.getAttribute('aria-invalid')).toBe('true')
})
it('renders a tooltip trigger when field.tips is provided', () => {
renderSchema([
{
name: 'script',
type: 'editor',
required: false,
alias: 'Compression script',
tips: 'Supports **markdown** in the tooltip.'
}
])
const tooltipTrigger = screen.getByRole('button', {
name: /Compression script tip/i
})
expect(tooltipTrigger).toBeTruthy()
})
})
describe('SchemaFormFields password field', () => {
const passwordSchema: ProviderPluginConfig[] = [
{
name: 'secret',
type: 'password',
required: true,
alias: 'Secret'
}
]
it('allows password reveal by default', () => {
renderSchema(passwordSchema, { secret: 'saved-secret' })
const input = screen.getByDisplayValue('saved-secret') as HTMLInputElement
expect(input.type).toBe('password')
fireEvent.click(screen.getByRole('button'))
expect(input.type).toBe('text')
})
it('keeps the password masked and removes the reveal button when disabled', () => {
const onValueChange = vi.fn()
render(
<SchemaFormFields
schema={passwordSchema}
values={{ secret: 'saved-secret' }}
allowPasswordReveal={false}
onValueChange={onValueChange}
/>
)
const input = screen.getByDisplayValue('saved-secret') as HTMLInputElement
expect(input.type).toBe('password')
expect(screen.queryByRole('button')).toBeNull()
fireEvent.change(input, { target: { value: 'replacement-secret' } })
expect(onValueChange).toHaveBeenCalledWith('secret', 'replacement-secret')
})
it('clears visible password state when reveal becomes disabled', () => {
const onValueChange = vi.fn()
const { rerender } = render(
<SchemaFormFields
schema={passwordSchema}
values={{ secret: 'saved-secret' }}
allowPasswordReveal
onValueChange={onValueChange}
/>
)
fireEvent.click(screen.getByRole('button'))
expect(
(screen.getByDisplayValue('saved-secret') as HTMLInputElement).type
).toBe('text')
rerender(
<SchemaFormFields
schema={passwordSchema}
values={{ secret: 'saved-secret' }}
allowPasswordReveal={false}
onValueChange={onValueChange}
/>
)
expect(
(screen.getByDisplayValue('saved-secret') as HTMLInputElement).type
).toBe('password')
rerender(
<SchemaFormFields
schema={passwordSchema}
values={{ secret: 'saved-secret' }}
allowPasswordReveal
onValueChange={onValueChange}
/>
)
expect(
(screen.getByDisplayValue('saved-secret') as HTMLInputElement).type
).toBe('password')
})
})
describe('SchemaFormFields unknown field type', () => {
it('does not render any input control for an unrecognized type, label only', () => {
renderSchema([
{
name: 'mystery',
type: 'unknown-widget' as never,
required: false,
alias: 'Mystery field'
}
])
// The label is still rendered via FieldLabel
expect(screen.getByText('Mystery field')).toBeTruthy()
// None of the input controls render: no textbox, no combobox, no switch
expect(screen.queryByRole('textbox')).toBeNull()
expect(screen.queryByRole('combobox')).toBeNull()
expect(screen.queryByRole('switch')).toBeNull()
})
})
@@ -1,251 +0,0 @@
// @vitest-environment jsdom
import { act, render } from '@testing-library/react'
import { useEffect, useState } from 'react'
import { describe, expect, it, vi } from 'vitest'
import {
usePluginConfigRefresh,
type RefreshConfigSchemaTarget,
} from '@/components/common/use-plugin-config-refresh'
import type { ProviderPluginConfig } from '@/components/main/providers/types'
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
const inputField = (name: string): ProviderPluginConfig => ({
name,
type: 'input',
required: false,
})
const listField = (
name: string,
choices: string[],
dependsOn?: string[]
): ProviderPluginConfig => ({
name,
type: 'list',
required: false,
choices,
dependsOn,
})
interface HarnessProps {
initialSchema: ProviderPluginConfig[]
initialValues: Record<string, unknown>
target: RefreshConfigSchemaTarget
fetchSchema: (
target: RefreshConfigSchemaTarget,
draftValues: Record<string, unknown>
) => Promise<ProviderPluginConfig[]>
schemaRef: { current: ProviderPluginConfig[] }
valuesRef: { current: Record<string, unknown> }
setValuesRef: { current: ((values: Record<string, unknown>) => void) | null }
setSchemaRef: { current: ((schema: ProviderPluginConfig[]) => void) | null }
debounceMs?: number
}
function Harness({
initialSchema,
initialValues,
target,
fetchSchema,
schemaRef,
valuesRef,
setValuesRef,
setSchemaRef,
debounceMs,
}: HarnessProps) {
const [schema, setSchema] = useState<ProviderPluginConfig[]>(initialSchema)
const [values, setValues] = useState<Record<string, unknown>>(initialValues)
useEffect(() => {
schemaRef.current = schema
valuesRef.current = values
setSchemaRef.current = setSchema
setValuesRef.current = setValues
}, [schema, values, schemaRef, valuesRef, setSchemaRef, setValuesRef])
usePluginConfigRefresh({
enabled: true,
target,
currentSchema: schema,
currentValues: values,
fetchSchema,
onSchemaUpdate: (nextSchema, nextValues) => {
setSchema(nextSchema)
setValues(nextValues)
},
debounceMs,
})
return null
}
const DEBOUNCE_TICK = 20
describe('usePluginConfigRefresh', () => {
const makeRefs = () => ({
schemaRef: { current: [] as ProviderPluginConfig[] },
valuesRef: { current: {} as Record<string, unknown> },
setSchemaRef: {
current: null as ((schema: ProviderPluginConfig[]) => void) | null,
},
setValuesRef: {
current: null as ((values: Record<string, unknown>) => void) | null,
},
})
it('does not call fetchSchema when the change does not hit any dependsOn', async () => {
const fetchSchema = vi.fn(async () => [] as ProviderPluginConfig[])
const refs = makeRefs()
render(
<Harness
initialSchema={[
listField('uploader', ['github']),
inputField('proxy'), // proxy is not referenced by anyone's dependsOn
]}
initialValues={{ uploader: 'github', proxy: '' }}
target={{ target: 'uploader', uploaderName: 'imgur' }}
fetchSchema={fetchSchema}
debounceMs={DEBOUNCE_TICK}
{...refs}
/>
)
await act(async () => {
refs.setValuesRef.current!({ uploader: 'github', proxy: 'http://proxy' })
})
await act(async () => {
await wait(DEBOUNCE_TICK + 30)
})
expect(fetchSchema).not.toHaveBeenCalled()
})
it('calls fetchSchema after debounce when a dependsOn target changes', async () => {
const fetchSchema = vi.fn(async () => [
listField('uploader', ['github', 'gitee']),
listField('repo', ['x', 'y'], ['uploader']),
])
const refs = makeRefs()
render(
<Harness
initialSchema={[
listField('uploader', ['github', 'gitee']),
listField('repo', ['a', 'b'], ['uploader']),
]}
initialValues={{ uploader: 'github', repo: 'a' }}
target={{ target: 'uploader', uploaderName: 'imgur' }}
fetchSchema={fetchSchema}
debounceMs={DEBOUNCE_TICK}
{...refs}
/>
)
await act(async () => {
refs.setValuesRef.current!({ uploader: 'gitee', repo: 'a' })
})
await act(async () => {
await wait(DEBOUNCE_TICK + 30)
})
expect(fetchSchema).toHaveBeenCalledTimes(1)
// 'repo' is a transitive dependent of the changed 'uploader' field, so it is
// stripped from the payload to let main re-cascade its default.
expect(fetchSchema).toHaveBeenCalledWith(
{ target: 'uploader', uploaderName: 'imgur' },
{ uploader: 'gitee' }
)
})
it('coalesces multiple changes inside the debounce window into one call', async () => {
const fetchSchema = vi.fn(async () => [] as ProviderPluginConfig[])
const refs = makeRefs()
render(
<Harness
initialSchema={[
listField('uploader', ['github', 'gitee']),
listField('repo', ['a'], ['uploader']),
]}
initialValues={{ uploader: 'github', repo: 'a' }}
target={{ target: 'uploader', uploaderName: 'imgur' }}
fetchSchema={fetchSchema}
debounceMs={DEBOUNCE_TICK}
{...refs}
/>
)
await act(async () => {
refs.setValuesRef.current!({ uploader: 'gitee', repo: 'a' })
})
await act(async () => {
refs.setValuesRef.current!({ uploader: 'github', repo: 'a' })
})
await act(async () => {
refs.setValuesRef.current!({ uploader: 'gitee', repo: 'a' })
})
await act(async () => {
await wait(DEBOUNCE_TICK + 30)
})
expect(fetchSchema).toHaveBeenCalledTimes(1)
expect(fetchSchema).toHaveBeenLastCalledWith(
{ target: 'uploader', uploaderName: 'imgur' },
{ uploader: 'gitee' }
)
})
it('passes the appropriate payload shape for each refresh target', async () => {
const initialSchema = [
listField('uploader', ['github', 'gitee']),
listField('repo', ['a'], ['uploader']),
]
// Return a schema with the same dependsOn structure so subsequent edits still fire the hook.
const fetchSchema = vi.fn(async () => initialSchema)
const runWith = async (target: RefreshConfigSchemaTarget) => {
const refs = makeRefs()
render(
<Harness
initialSchema={initialSchema}
initialValues={{ uploader: 'github', repo: 'a' }}
target={target}
fetchSchema={fetchSchema}
debounceMs={DEBOUNCE_TICK}
{...refs}
/>
)
await act(async () => {
refs.setValuesRef.current!({ uploader: 'gitee', repo: 'a' })
})
await act(async () => {
await wait(DEBOUNCE_TICK + 30)
})
}
await runWith({ target: 'plugin', pluginFullName: 'picgo-plugin-x' })
expect(fetchSchema).toHaveBeenLastCalledWith(
{ target: 'plugin', pluginFullName: 'picgo-plugin-x' },
{ uploader: 'gitee' }
)
await runWith({ target: 'transformer', pluginFullName: 'picgo-plugin-x' })
expect(fetchSchema).toHaveBeenLastCalledWith(
{ target: 'transformer', pluginFullName: 'picgo-plugin-x' },
{ uploader: 'gitee' }
)
await runWith({ target: 'uploader', uploaderName: 'imgur' })
expect(fetchSchema).toHaveBeenLastCalledWith(
{ target: 'uploader', uploaderName: 'imgur' },
{ uploader: 'gitee' }
)
})
})
@@ -1,64 +0,0 @@
/**
* Shared cascade-test fixture: a region → endpoint → bucket schema that
* mirrors the picgo-plugin-test reactive plugin. Used by both the hook-level
* cascade tests and the panel-level real-component tests.
*/
export const REGION_ENDPOINTS: Record<string, string[]> = {
us: ['s3.us-east-1', 's3.us-west-2'],
eu: ['s3.eu-west-1', 's3.eu-central-1'],
asia: ['s3.ap-southeast-1', 's3.ap-northeast-1'],
}
export const ENDPOINT_BUCKETS: Record<string, string[]> = {
's3.us-east-1': ['us-east-prod', 'us-east-staging'],
's3.us-west-2': ['us-west-prod'],
's3.eu-west-1': ['eu-west-prod', 'eu-west-archive'],
's3.eu-central-1': ['eu-central-prod'],
's3.ap-southeast-1': ['asia-sg-prod', 'asia-sg-staging'],
's3.ap-northeast-1': ['asia-tk-prod'],
}
export const buildCascadeRawSchema = (storedRegion?: string): unknown[] => [
{
name: 'region',
type: 'list',
required: true,
alias: 'Region',
default: storedRegion || 'us',
choices: ['us', 'eu', 'asia'],
},
{
name: 'endpoint',
type: 'list',
required: true,
alias: 'Endpoint',
dependsOn: ['region'],
default: (answers: Record<string, unknown>) => {
const region = (answers.region as string) || 'us'
return REGION_ENDPOINTS[region]?.[0]
},
choices: (answers: Record<string, unknown>) => {
const region = (answers.region as string) || 'us'
return REGION_ENDPOINTS[region] || []
},
},
{
name: 'bucket',
type: 'list',
required: true,
alias: 'Bucket',
dependsOn: ['endpoint'],
choices: (answers: Record<string, unknown>) => {
const endpoint = answers.endpoint as string
return ENDPOINT_BUCKETS[endpoint] || []
},
},
{
name: 'pathPrefix',
type: 'input',
required: false,
alias: 'Path prefix',
default: '',
},
]
@@ -1,139 +0,0 @@
// @vitest-environment jsdom
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { renderHook, waitFor } from '@testing-library/react'
import type { PropsWithChildren } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { CloudAlbumStatsResponse } from '~/universal/types/cloudAlbum'
vi.mock('@/adapters/cloud-album', () => ({
cloudAlbumAdapter: {
getStats: vi.fn()
}
}))
vi.mock('@/queries/query-client', () => ({
rendererQueryClient: {
invalidateQueries: vi.fn()
}
}))
import { cloudAlbumAdapter } from '@/adapters/cloud-album'
import { rendererQueryClient } from '@/queries/query-client'
import {
invalidateCloudAlbumStatsQuery,
PicGoCloudAlbumStatsQueryKeys,
useCloudAlbumStatsQuery
} from '@/queries/picgo-cloud-album-stats'
const SAMPLE_STATS: CloudAlbumStatsResponse = {
total: 12,
types: [
{ type: 'picgo-cloud', count: 8 },
{ type: 'github', count: 4 }
]
}
const buildWrapper = () => {
const client = new QueryClient({
defaultOptions: {
queries: { retry: false, gcTime: 0 }
}
})
const Wrapper = ({ children }: PropsWithChildren) => (
<QueryClientProvider client={client}>{children}</QueryClientProvider>
)
return { client, Wrapper }
}
describe('useCloudAlbumStatsQuery', () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.clearAllMocks()
})
it('returns parsed stats when adapter resolves with success', async () => {
vi.mocked(cloudAlbumAdapter.getStats).mockResolvedValue({
success: true,
data: SAMPLE_STATS
} as unknown as Awaited<ReturnType<typeof cloudAlbumAdapter.getStats>>)
const { Wrapper } = buildWrapper()
const { result } = renderHook(() => useCloudAlbumStatsQuery(), {
wrapper: Wrapper
})
await waitFor(() => {
expect(result.current.isSuccess).toBe(true)
})
expect(cloudAlbumAdapter.getStats).toHaveBeenCalledOnce()
expect(result.current.data).toEqual(SAMPLE_STATS)
})
it('surfaces error state when adapter resolves with success: false', async () => {
vi.mocked(cloudAlbumAdapter.getStats).mockResolvedValue({
success: false,
error: 'stats-fetch-failed'
} as unknown as Awaited<ReturnType<typeof cloudAlbumAdapter.getStats>>)
const { Wrapper } = buildWrapper()
const { result } = renderHook(() => useCloudAlbumStatsQuery(), {
wrapper: Wrapper
})
await waitFor(() => {
expect(result.current.isError).toBe(true)
})
expect(result.current.error).toBeInstanceOf(Error)
expect((result.current.error as Error).message).toBe('stats-fetch-failed')
})
it('does not call adapter when enabled is false', async () => {
const { Wrapper } = buildWrapper()
const { result } = renderHook(() => useCloudAlbumStatsQuery({ enabled: false }), {
wrapper: Wrapper
})
// give react-query a microtask to schedule (it shouldn't)
await Promise.resolve()
expect(cloudAlbumAdapter.getStats).not.toHaveBeenCalled()
expect(result.current.fetchStatus).toBe('idle')
})
it('calls adapter when enabled is omitted (default true)', async () => {
vi.mocked(cloudAlbumAdapter.getStats).mockResolvedValue({
success: true,
data: SAMPLE_STATS
} as unknown as Awaited<ReturnType<typeof cloudAlbumAdapter.getStats>>)
const { Wrapper } = buildWrapper()
renderHook(() => useCloudAlbumStatsQuery(), { wrapper: Wrapper })
await waitFor(() => {
expect(cloudAlbumAdapter.getStats).toHaveBeenCalledOnce()
})
})
})
describe('invalidateCloudAlbumStatsQuery', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('invalidates the canonical stats query key', async () => {
vi.mocked(rendererQueryClient.invalidateQueries).mockResolvedValue(undefined)
await invalidateCloudAlbumStatsQuery()
expect(rendererQueryClient.invalidateQueries).toHaveBeenCalledOnce()
expect(rendererQueryClient.invalidateQueries).toHaveBeenCalledWith({
queryKey: PicGoCloudAlbumStatsQueryKeys.stats
})
})
})
@@ -1,175 +0,0 @@
// @vitest-environment jsdom
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { renderHook, waitFor } from '@testing-library/react'
import type { PropsWithChildren } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
IPicGoCloudConfigSyncSessionStatus,
IPicGoCloudEncryptionMethod,
type IPicGoCloudConfigSyncState
} from '~/universal/types/cloudConfigSync'
vi.mock('@/adapters/cloud', () => ({
cloudAdapter: {
getConfigSyncState: vi.fn()
}
}))
vi.mock('@/queries/picgo-cloud', () => ({
usePicGoCloudUserInfo: vi.fn()
}))
vi.mock('@/queries/query-client', () => ({
rendererQueryClient: {
setQueryData: vi.fn(),
getQueryData: vi.fn()
}
}))
import { cloudAdapter } from '@/adapters/cloud'
import { usePicGoCloudUserInfo } from '@/queries/picgo-cloud'
import { rendererQueryClient } from '@/queries/query-client'
import {
PicGoCloudConfigSyncQueryKeys,
setCloudConfigSyncStateQueryData,
updateCloudConfigSyncStateQueryData,
useCloudConfigSyncStateQuery
} from '@/queries/picgo-cloud-config-sync'
const SAMPLE_STATE: IPicGoCloudConfigSyncState = {
sessionStatus: IPicGoCloudConfigSyncSessionStatus.IDLE,
encryptionMethod: IPicGoCloudEncryptionMethod.AUTO,
lastSyncedAt: '2026-05-05T10:35:09.000Z'
}
const buildWrapper = () => {
const client = new QueryClient({
defaultOptions: {
queries: { retry: false, gcTime: 0 }
}
})
const Wrapper = ({ children }: PropsWithChildren) => (
<QueryClientProvider client={client}>{children}</QueryClientProvider>
)
return { client, Wrapper }
}
describe('picgo-cloud-config-sync helpers', () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.clearAllMocks()
})
describe('setCloudConfigSyncStateQueryData', () => {
it('writes state to query cache under the canonical key', () => {
setCloudConfigSyncStateQueryData(SAMPLE_STATE)
expect(rendererQueryClient.setQueryData).toHaveBeenCalledOnce()
expect(rendererQueryClient.setQueryData).toHaveBeenCalledWith(
PicGoCloudConfigSyncQueryKeys.state,
SAMPLE_STATE
)
})
})
describe('updateCloudConfigSyncStateQueryData', () => {
it('passes the updater function to setQueryData', () => {
const updater = vi.fn(
(prev: IPicGoCloudConfigSyncState | undefined): IPicGoCloudConfigSyncState => ({
...(prev ?? { sessionStatus: IPicGoCloudConfigSyncSessionStatus.IDLE }),
sessionStatus: IPicGoCloudConfigSyncSessionStatus.SYNCING
})
)
updateCloudConfigSyncStateQueryData(updater)
expect(rendererQueryClient.setQueryData).toHaveBeenCalledOnce()
const callArgs = vi.mocked(rendererQueryClient.setQueryData).mock.calls[0]
expect(callArgs[0]).toEqual(PicGoCloudConfigSyncQueryKeys.state)
expect(callArgs[1]).toBe(updater)
})
})
})
describe('useCloudConfigSyncStateQuery', () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.clearAllMocks()
})
it('does not call adapter when user is not logged in', async () => {
vi.mocked(usePicGoCloudUserInfo).mockReturnValue({
userInfo: undefined,
isPaid: false,
isFetched: true
} as unknown as ReturnType<typeof usePicGoCloudUserInfo>)
const { Wrapper } = buildWrapper()
const { result } = renderHook(() => useCloudConfigSyncStateQuery(), {
wrapper: Wrapper
})
// 等一个 microtask 让 react-query 有机会 schedule fetch
await Promise.resolve()
expect(cloudAdapter.getConfigSyncState).not.toHaveBeenCalled()
expect(result.current.fetchStatus).toBe('idle')
})
it('returns parsed state when adapter resolves with success', async () => {
vi.mocked(usePicGoCloudUserInfo).mockReturnValue({
userInfo: { user: 'tester', plan: 1 },
isPaid: true,
isFetched: true
} as unknown as ReturnType<typeof usePicGoCloudUserInfo>)
vi.mocked(cloudAdapter.getConfigSyncState).mockResolvedValue({
success: true,
data: SAMPLE_STATE
} as unknown as Awaited<ReturnType<typeof cloudAdapter.getConfigSyncState>>)
const { Wrapper } = buildWrapper()
const { result } = renderHook(() => useCloudConfigSyncStateQuery(), {
wrapper: Wrapper
})
await waitFor(() => {
expect(result.current.isSuccess).toBe(true)
})
expect(cloudAdapter.getConfigSyncState).toHaveBeenCalledOnce()
expect(result.current.data).toEqual(SAMPLE_STATE)
})
it('surfaces error state when adapter resolves with success: false', async () => {
vi.mocked(usePicGoCloudUserInfo).mockReturnValue({
userInfo: { user: 'tester', plan: 0 },
isPaid: false,
isFetched: true
} as unknown as ReturnType<typeof usePicGoCloudUserInfo>)
vi.mocked(cloudAdapter.getConfigSyncState).mockResolvedValue({
success: false,
error: 'boom'
} as unknown as Awaited<ReturnType<typeof cloudAdapter.getConfigSyncState>>)
const { Wrapper } = buildWrapper()
const { result } = renderHook(() => useCloudConfigSyncStateQuery(), {
wrapper: Wrapper
})
await waitFor(() => {
expect(result.current.isError).toBe(true)
})
expect(result.current.error).toBeInstanceOf(Error)
expect((result.current.error as Error).message).toBe('boom')
})
})
@@ -1,125 +0,0 @@
// @vitest-environment jsdom
import { createMemoryHistory } from '@tanstack/history'
import {
Outlet,
RouterProvider,
createRootRoute,
createRoute,
createRouter,
redirect,
useMatchRoute
} from '@tanstack/react-router'
import { render, screen } from '@testing-library/react'
import { describe, expect, test } from 'vitest'
function AppShellMock () {
const matchRoute = useMatchRoute()
const isActive = (to: string) => {
return Boolean(matchRoute({ to, fuzzy: true, pending: false }))
}
return (
<div>
<button aria-current={isActive('/main/dashboard') ? 'page' : undefined}>SIDEBAR_DASHBOARD</button>
<button aria-current={isActive('/main/providers') ? 'page' : undefined}>ALBUM_PROVIDERS</button>
<button aria-current={isActive('/main/plugins') ? 'page' : undefined}>SIDEBAR_PLUGINS</button>
<button aria-current={isActive('/main/settings/settings') ? 'page' : undefined}>SETTINGS</button>
<button>MORE</button>
<Outlet />
</div>
)
}
function createTestRouter (initialEntry: string) {
const rootRoute = createRootRoute({
component: Outlet
})
const mainRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/main',
component: AppShellMock
})
const mainIndexRoute = createRoute({
getParentRoute: () => mainRoute,
path: '/',
beforeLoad: () => {
throw redirect({ to: '/main/dashboard', replace: true })
}
})
const dashboardRoute = createRoute({
getParentRoute: () => mainRoute,
path: '/dashboard',
component: () => <div title='DASHBOARD_HISTORY_PANEL_TITLE'>dashboard</div>
})
const providersRoute = createRoute({
getParentRoute: () => mainRoute,
path: '/providers',
component: () => <div>providers</div>
})
const pluginsRoute = createRoute({
getParentRoute: () => mainRoute,
path: '/plugins',
component: () => <div>plugins</div>
})
const settingsRoute = createRoute({
getParentRoute: () => mainRoute,
path: '/settings/settings',
component: () => <div>settings</div>
})
const routeTree = rootRoute.addChildren([
mainRoute.addChildren([
mainIndexRoute,
dashboardRoute,
providersRoute,
pluginsRoute,
settingsRoute
])
])
return createRouter({
routeTree,
history: createMemoryHistory({
initialEntries: [initialEntry]
})
})
}
function renderRoute (initialEntry: string) {
const router = createTestRouter(initialEntry)
render(<RouterProvider router={router} />)
return router
}
describe('renderer app shell routing', () => {
test('redirects /main/ to dashboard and renders dashboard-only history panel', async () => {
renderRoute('/main/')
expect(await screen.findByRole('button', { name: 'SIDEBAR_DASHBOARD' })).toHaveAttribute('aria-current', 'page')
expect(screen.getByTitle('DASHBOARD_HISTORY_PANEL_TITLE')).toBeInTheDocument()
})
test('drives active sidebar state from the current URL', async () => {
renderRoute('/main/providers')
expect(await screen.findByRole('button', { name: 'ALBUM_PROVIDERS' })).toHaveAttribute('aria-current', 'page')
expect(screen.getByRole('button', { name: 'SIDEBAR_DASHBOARD' })).not.toHaveAttribute('aria-current')
})
test('renders the v3-style shell navigation groups on desktop routes', async () => {
renderRoute('/main/dashboard')
expect(await screen.findByRole('button', { name: 'SIDEBAR_PLUGINS' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'SETTINGS' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'MORE' })).toBeInTheDocument()
})
})
@@ -1,70 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { IConfig } from 'picgo'
import { getConfig, getPicBeds } from '@/utils/dataSender'
import {
appActions,
PicGoCloudLoginStatusValues,
useStore
} from '@/store'
vi.mock('@/utils/dataSender', () => {
return {
getConfig: vi.fn(),
getPicBeds: vi.fn(),
saveConfig: vi.fn()
}
})
const resetStore = () => {
useStore.setState({
defaultPicBed: 'smms',
appConfig: null,
picBeds: [],
picgoCloud: {
loginStatus: PicGoCloudLoginStatusValues.Idle,
loginError: null,
hasAgreedToTermsAndPrivacy: false
}
})
}
describe('renderer/store appConfig', () => {
const getConfigMock = vi.mocked(getConfig)
const getPicBedsMock = vi.mocked(getPicBeds)
beforeEach(() => {
vi.clearAllMocks()
resetStore()
})
it('refreshAppConfig updates appConfig and defaultPicBed', async () => {
const config: IConfig = {
picBed: {
uploader: 'github',
current: 'smms'
},
picgoPlugins: {}
}
getConfigMock.mockResolvedValue(config)
await appActions.refreshAppConfig()
const nextState = useStore.getState()
expect(nextState.appConfig?.picBed.uploader).toBe('github')
expect(nextState.appConfig?.picBed.current).toBe('smms')
expect(nextState.appConfig?.settings.autoCopyUrl).toBe(true)
expect(nextState.defaultPicBed).toBe('github')
})
it('refreshPicBeds updates picBeds', async () => {
const picBeds: IPicBedType[] = [
{ type: 'smms', name: 'SM.MS', visible: true },
{ type: 'github', name: 'GitHub', visible: true }
]
getPicBedsMock.mockResolvedValue(picBeds)
await appActions.refreshPicBeds()
expect(useStore.getState().picBeds).toEqual(picBeds)
})
})
@@ -1,341 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@/adapters/plugins', () => ({
pluginsAdapter: {
installPlugin: vi.fn(),
importLocalPlugin: vi.fn(),
uninstallPlugin: vi.fn(),
updatePlugin: vi.fn(),
togglePluginEnabled: vi.fn(),
saveTransformer: vi.fn(),
setNeedReload: vi.fn(),
getInstalledPlugins: vi.fn(),
savePluginConfig: vi.fn(),
fetchPluginReadme: vi.fn(),
searchPlugins: vi.fn()
}
}))
vi.mock('@/store/app-actions', () => ({
appActions: {
hydrateAppState: vi.fn(),
refreshAppConfig: vi.fn()
}
}))
vi.mock('sonner', () => ({
toast: {
success: vi.fn()
}
}))
import { pluginsAdapter } from '@/adapters/plugins'
import { pluginReadmeStatus, type PluginInstalledItem } from '@/components/main/plugins/types'
import { appActions } from '@/store/app-actions'
import { useAppStore } from '@/store/app-store'
import { pluginStoreActions } from '@/store/plugins/actions'
import { usePluginStore } from '@/store/plugins/store'
import { toast } from 'sonner'
import { IPasteStyle, IStartupMode } from '~/universal/types/enum'
function createInstalledPlugin (overrides?: Partial<PluginInstalledItem>): PluginInstalledItem {
return {
name: 'cloudflare-r2-xqv',
fullName: 'picgo-plugin-cloudflare-r2-xqv',
author: 'xiaoqinvar',
description: 'picgo for cloudflare-r2 storage',
logo: 'https://example.com/logo.png',
version: '1.0.4',
gui: true,
homepage: 'https://example.com',
enabled: true,
hasInstall: true,
guiMenu: [],
config: {
plugin: {
name: 'cloudflare-r2-xqv',
fullName: 'picgo-plugin-cloudflare-r2-xqv',
config: []
},
transformer: {
name: 'path',
fullName: 'path',
config: []
}
},
uploader: {
id: 'cloudflare',
name: 'cloudflare',
schema: [],
configState: {
configList: [],
defaultId: ''
}
},
...overrides
}
}
function resetStores () {
useAppStore.setState({
defaultPicBed: 'smms',
appConfig: {
picBed: {
uploader: 'smms',
current: 'smms',
transformer: 'path',
proxy: '',
list: []
},
uploader: {},
settings: {
appearance: 'auto',
pasteStyle: IPasteStyle.MARKDOWN,
showUpdateTip: false,
autoStart: false,
rename: false,
autoRename: false,
uploadNotification: false,
notificationSound: true,
miniWindowOnTop: false,
logLevel: ['all'],
autoCopyUrl: true,
checkBetaUpdate: true,
useBuiltinClipboard: false,
language: 'en',
logFileSizeLimit: 10,
encodeOutputURL: false,
showDockIcon: true,
showMenubarIcon: true,
customLink: '$url',
npmProxy: '',
npmRegistry: '',
server: {
port: 36677,
host: '127.0.0.1',
enable: true
},
startupMode: IStartupMode.HIDE,
shortKey: {},
urlRewrite: {
rules: []
}
},
picgoPlugins: {},
plugins: {},
transformer: {},
needReload: false
},
picBeds: [],
providers: [],
providerSchemas: {},
pluginsInstalled: [],
settingsVersion: {
currentVersion: '2.5.3',
latestVersion: null
},
hasHydrated: true,
hasSettingsHydrated: true,
picgoCloud: {
loginStatus: 'IDLE',
loginError: null,
hasAgreedToTermsAndPrivacy: false
}
})
usePluginStore.setState({
searchValue: '',
exactMatch: false,
rawSearchResults: [],
searchResults: [],
isSearching: false,
isImportingLocal: false,
isMutatingByPlugin: {},
readmeByPlugin: {}
})
}
describe('renderer/store plugins', () => {
const installPluginMock = vi.mocked(pluginsAdapter.installPlugin)
const importLocalPluginMock = vi.mocked(pluginsAdapter.importLocalPlugin)
const uninstallPluginMock = vi.mocked(pluginsAdapter.uninstallPlugin)
const updatePluginMock = vi.mocked(pluginsAdapter.updatePlugin)
const togglePluginEnabledMock = vi.mocked(pluginsAdapter.togglePluginEnabled)
const getInstalledPluginsMock = vi.mocked(pluginsAdapter.getInstalledPlugins)
const fetchPluginReadmeMock = vi.mocked(pluginsAdapter.fetchPluginReadme)
const hydrateAppStateMock = vi.mocked(appActions.hydrateAppState)
const refreshAppConfigMock = vi.mocked(appActions.refreshAppConfig)
const toastSuccessMock = vi.mocked(toast.success)
beforeEach(() => {
vi.clearAllMocks()
resetStores()
})
it('installs plugin and shows success toast', async () => {
const installedPlugin = createInstalledPlugin()
installPluginMock.mockResolvedValue({
success: true,
body: installedPlugin.fullName,
errMsg: ''
})
getInstalledPluginsMock.mockResolvedValue([installedPlugin as unknown as IPicGoPlugin])
await pluginStoreActions.installPlugin(installedPlugin.fullName)
expect(installPluginMock).toHaveBeenCalledWith(installedPlugin.fullName)
expect(hydrateAppStateMock).toHaveBeenCalled()
expect(useAppStore.getState().pluginsInstalled).toHaveLength(1)
expect(toastSuccessMock).toHaveBeenCalled()
})
it('toggles plugin enabled state and shows success toast', async () => {
const installedPlugin = createInstalledPlugin()
useAppStore.setState({ pluginsInstalled: [installedPlugin] })
togglePluginEnabledMock.mockResolvedValue({
success: true,
data: installedPlugin.fullName
})
getInstalledPluginsMock.mockResolvedValue([
{ ...installedPlugin, enabled: false } as unknown as IPicGoPlugin
])
await pluginStoreActions.setPluginEnabled(installedPlugin.fullName, false)
expect(togglePluginEnabledMock).toHaveBeenCalledWith(installedPlugin.fullName, false)
expect(useAppStore.getState().pluginsInstalled[0]?.enabled).toBe(false)
expect(toastSuccessMock).toHaveBeenCalled()
})
it('updates plugin and marks needReload', async () => {
const installedPlugin = createInstalledPlugin()
useAppStore.setState({ pluginsInstalled: [installedPlugin] })
updatePluginMock.mockResolvedValue({
success: true,
data: installedPlugin.fullName
})
getInstalledPluginsMock.mockResolvedValue([installedPlugin as unknown as IPicGoPlugin])
await pluginStoreActions.updatePlugin(installedPlugin.fullName)
expect(updatePluginMock).toHaveBeenCalledWith(installedPlugin.fullName)
expect(useAppStore.getState().appConfig?.needReload).toBe(true)
expect(toastSuccessMock).toHaveBeenCalled()
})
it('uninstalls plugin and marks needReload', async () => {
const installedPlugin = createInstalledPlugin()
useAppStore.setState({ pluginsInstalled: [installedPlugin] })
uninstallPluginMock.mockResolvedValue({
success: true,
data: installedPlugin.fullName
})
getInstalledPluginsMock.mockResolvedValue([])
await pluginStoreActions.uninstallPlugin(installedPlugin.fullName)
expect(uninstallPluginMock).toHaveBeenCalledWith(installedPlugin.fullName)
expect(useAppStore.getState().pluginsInstalled).toEqual([])
expect(useAppStore.getState().appConfig?.needReload).toBe(true)
expect(toastSuccessMock).toHaveBeenCalled()
})
it('imports local plugin via RPC result and selects installed plugin data', async () => {
const installedPlugin = createInstalledPlugin()
importLocalPluginMock.mockResolvedValue({
success: true,
data: installedPlugin.fullName
})
getInstalledPluginsMock.mockResolvedValue([installedPlugin as unknown as IPicGoPlugin])
const result = await pluginStoreActions.importLocalPlugin()
expect(importLocalPluginMock).toHaveBeenCalled()
expect(result?.installedPlugin.fullName).toBe(installedPlugin.fullName)
expect(usePluginStore.getState().isImportingLocal).toBe(false)
expect(toastSuccessMock).toHaveBeenCalled()
})
it('returns null when local import is cancelled', async () => {
importLocalPluginMock.mockResolvedValue({
success: true,
data: null
})
const result = await pluginStoreActions.importLocalPlugin()
expect(result).toBeNull()
expect(getInstalledPluginsMock).not.toHaveBeenCalled()
expect(usePluginStore.getState().isImportingLocal).toBe(false)
})
it('filters exact-match search results from cached raw results', () => {
usePluginStore.setState({
searchValue: 'cloudflare',
exactMatch: false,
rawSearchResults: [
{
name: 'cloudflare-r2-xqv',
fullName: 'picgo-plugin-cloudflare-r2-xqv',
author: 'foo',
description: 'foo',
logo: '',
version: '1.0.0',
homepage: '',
gui: true,
hasInstall: false
},
{
name: 'other',
fullName: 'picgo-plugin-other',
author: 'bar',
description: 'bar',
logo: '',
version: '1.0.0',
homepage: '',
gui: true,
hasInstall: false
}
],
searchResults: []
})
pluginStoreActions.toggleExactMatch()
expect(usePluginStore.getState().exactMatch).toBe(true)
expect(usePluginStore.getState().searchResults).toHaveLength(1)
expect(usePluginStore.getState().searchResults[0]?.fullName).toBe(
'picgo-plugin-cloudflare-r2-xqv'
)
})
it('fetches README into ready state', async () => {
fetchPluginReadmeMock.mockResolvedValue('# hello')
await pluginStoreActions.fetchPluginReadme('picgo-plugin-cloudflare-r2-xqv')
expect(usePluginStore.getState().readmeByPlugin['picgo-plugin-cloudflare-r2-xqv']).toEqual({
status: pluginReadmeStatus.Ready,
content: '# hello',
errorMessage: null
})
})
it('refreshes app config after saving plugin config', async () => {
const installedPlugin = createInstalledPlugin()
useAppStore.setState({ pluginsInstalled: [installedPlugin] })
vi.mocked(pluginsAdapter.savePluginConfig).mockResolvedValue(undefined)
getInstalledPluginsMock.mockResolvedValue([installedPlugin as unknown as IPicGoPlugin])
await pluginStoreActions.savePluginConfig(installedPlugin.fullName, 'config', {
token: 'abc'
})
expect(vi.mocked(pluginsAdapter.savePluginConfig)).toHaveBeenCalledWith(
'plugin',
installedPlugin.config.plugin.fullName,
{ token: 'abc' }
)
expect(refreshAppConfigMock).toHaveBeenCalled()
})
})
@@ -1,282 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AppConfig } from '@/components/main/providers/types'
import { IPasteStyle, IStartupMode } from '~/universal/types/enum'
vi.mock('@/adapters/providers', () => ({
providersAdapter: {
getProviderSchema: vi.fn(),
getProviderConfigList: vi.fn(),
selectProviderConfig: vi.fn(),
changeCurrentUploader: vi.fn(),
deleteProviderConfig: vi.fn(),
copyProviderConfig: vi.fn(),
saveProviderConfig: vi.fn()
}
}))
vi.mock('@/store/app-actions', () => ({
appActions: {
hydrateAppState: vi.fn(),
ensureHydrated: vi.fn(),
ensureSettingsHydrated: vi.fn(),
setDefaultPicBed: vi.fn()
}
}))
import { providersAdapter } from '@/adapters/providers'
import { appActions } from '@/store/app-actions'
import { useAppStore } from '@/store/app-store'
import { useProviderStore } from '@/store/providers/store'
import { providerStoreActions } from '@/store/providers/actions'
function createAppConfig (): AppConfig {
return {
picBed: {
uploader: 'github',
current: 'github',
transformer: 'path',
proxy: '',
list: [
{ type: 'github', name: 'GitHub', visible: true }
]
},
uploader: {
github: {
defaultId: 'cfg-1',
configList: [
{
_id: 'cfg-1',
_configName: 'Default',
_createdAt: 1,
_updatedAt: 1,
token: 'old-token'
}
]
}
},
settings: {
appearance: 'auto',
pasteStyle: IPasteStyle.MARKDOWN,
showUpdateTip: false,
autoStart: false,
rename: false,
autoRename: false,
uploadNotification: false,
notificationSound: true,
miniWindowOnTop: false,
logLevel: ['all'],
autoCopyUrl: true,
checkBetaUpdate: true,
useBuiltinClipboard: false,
language: 'en',
logFileSizeLimit: 10,
encodeOutputURL: false,
showDockIcon: true,
showMenubarIcon: true,
customLink: '$url',
npmProxy: '',
npmRegistry: '',
server: {
port: 36677,
host: '127.0.0.1',
enable: true
},
startupMode: IStartupMode.SHOW_MAIN_WINDOW,
shortKey: {
'picgo:upload': {
name: 'upload',
label: 'Quick Upload',
key: 'CommandOrControl+Shift+U',
enable: true
}
},
urlRewrite: {
rules: []
}
},
picgoPlugins: {},
plugins: {},
transformer: {},
needReload: false
}
}
function resetStores () {
useAppStore.setState((state) => {
state.appConfig = createAppConfig()
state.providers = [
{
id: 'github',
name: 'GitHub',
visible: true,
isDefaultUploader: true
}
]
state.providerSchemas = {}
state.hasHydrated = true
})
useProviderStore.setState((state) => {
state.isHydrating = false
state.isLoadingByProvider = {}
state.expandedProviderIds = []
state.searchValue = ''
})
}
describe('renderer/store providers', () => {
const getProviderSchemaMock = vi.mocked(providersAdapter.getProviderSchema)
const selectProviderConfigMock = vi.mocked(providersAdapter.selectProviderConfig)
const changeCurrentUploaderMock = vi.mocked(providersAdapter.changeCurrentUploader)
const deleteProviderConfigMock = vi.mocked(providersAdapter.deleteProviderConfig)
const copyProviderConfigMock = vi.mocked(providersAdapter.copyProviderConfig)
const saveProviderConfigMock = vi.mocked(providersAdapter.saveProviderConfig)
const hydrateAppStateMock = vi.mocked(appActions.hydrateAppState)
const setDefaultPicBedMock = vi.mocked(appActions.setDefaultPicBed)
beforeEach(() => {
vi.clearAllMocks()
resetStores()
})
it('loads provider schema into global store', async () => {
getProviderSchemaMock.mockResolvedValue({
name: 'GitHub',
config: [
{
name: 'token',
type: 'password',
required: true,
alias: 'Token'
}
]
})
const schema = await providerStoreActions.ensureSchema('github')
expect(schema).toEqual({
id: 'github',
name: 'GitHub',
config: [
{
name: 'token',
type: 'password',
required: true,
alias: 'Token',
default: undefined,
message: undefined,
prefix: undefined,
tips: undefined,
confirmText: undefined,
cancelText: undefined,
choices: undefined
}
]
})
expect(useAppStore.getState().providerSchemas.github).toEqual(schema)
expect(useProviderStore.getState().isLoadingByProvider.github).toBe(false)
})
it('maps config id to name for default/select dashboard actions', async () => {
selectProviderConfigMock.mockResolvedValue('cfg-1')
const selectedId = await providerStoreActions.setDefaultConfig('github', 'cfg-1')
expect(selectProviderConfigMock).toHaveBeenCalledWith('github', 'Default')
expect(hydrateAppStateMock).toHaveBeenCalled()
expect(selectedId).toBe('cfg-1')
await providerStoreActions.selectDashboardProviderConfig('github', 'cfg-1')
expect(changeCurrentUploaderMock).toHaveBeenCalledWith('github', 'Default')
})
it('creates, copies, deletes and saves provider configs with real adapter calls', async () => {
saveProviderConfigMock.mockImplementation(async (_type, configId, values) => {
if (!configId && values._configName === 'New Config') {
useAppStore.setState((state) => {
if (!state.appConfig) {
return
}
state.appConfig.uploader.github.configList.push({
_id: 'cfg-2',
_configName: 'New Config',
_createdAt: 2,
_updatedAt: 2
})
})
}
})
const createdId = await providerStoreActions.createConfig('github', 'New Config')
expect(saveProviderConfigMock).toHaveBeenCalledWith('github', '', {
_configName: 'New Config'
})
expect(createdId).toBe('cfg-2')
await providerStoreActions.saveConfig('github', 'cfg-2', {
token: 'new-token'
})
expect(saveProviderConfigMock).toHaveBeenLastCalledWith('github', 'cfg-2', {
token: 'new-token',
_configName: 'New Config'
})
copyProviderConfigMock.mockResolvedValue({
defaultId: 'cfg-1',
configList: [
{
_id: 'cfg-1',
_configName: 'Default',
_createdAt: 1,
_updatedAt: 1
},
{
_id: 'cfg-3',
_configName: 'Default - Copy',
_createdAt: 3,
_updatedAt: 3
}
]
})
const copiedId = await providerStoreActions.copyConfig(
'github',
'cfg-1',
'Default - Copy'
)
expect(copyProviderConfigMock).toHaveBeenCalledWith(
'github',
'Default',
'Default - Copy'
)
expect(copiedId).toBe('cfg-3')
deleteProviderConfigMock.mockResolvedValue({
defaultId: 'cfg-1',
configList: [
{
_id: 'cfg-1',
_configName: 'Default',
_createdAt: 1,
_updatedAt: 1
}
]
})
const fallbackId = await providerStoreActions.deleteConfig('github', 'cfg-2')
expect(deleteProviderConfigMock).toHaveBeenCalledWith('github', 'New Config')
expect(fallbackId).toBe('cfg-1')
})
it('forwards default provider changes to app actions', async () => {
await providerStoreActions.setDefaultProvider('github')
expect(setDefaultPicBedMock).toHaveBeenCalledWith('github')
})
})

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