mirror of
https://github.com/Molunerfinn/PicGo.git
synced 2026-09-20 19:17:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
905e34aefe | ||
|
|
592f9855f8 | ||
|
|
e2e05aef49 | ||
|
|
781cf30c55 | ||
|
|
27464dcf29 | ||
|
|
ef07c15085 | ||
|
|
8952c98351 | ||
|
|
5cabbbdb90 | ||
|
|
43447726ef | ||
|
|
678cb713a9 | ||
|
|
32f501cadb | ||
|
|
4fb3f2d488 | ||
|
|
5eb3755fa2 |
@@ -22,7 +22,7 @@ body:
|
||||
label: 前置阅读 | Pre-reading
|
||||
description: 我已经自行查找、阅读以下内容(阅读了请打勾) | I have searched and read the following on my own (Please tick after reading)
|
||||
options:
|
||||
- label: "[文档/Doc](https://picgo.github.io/PicGo-Doc/)"
|
||||
- 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
|
||||
@@ -65,4 +65,4 @@ body:
|
||||
最后,喜欢 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!
|
||||
Buy me a cup of coffee if you can? There is a sponsorship QR code on the homepage, thank you for your support!
|
||||
|
||||
@@ -22,7 +22,7 @@ body:
|
||||
label: 前置阅读 | Pre-reading
|
||||
description: 我已经自行查找、阅读以下内容(阅读了请打勾) | I have searched and read the following on my own (Please tick after reading)
|
||||
options:
|
||||
- label: "[文档/Doc](https://picgo.github.io/PicGo-Doc/)"
|
||||
- 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
|
||||
@@ -60,4 +60,4 @@ body:
|
||||
最后,喜欢 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!
|
||||
Buy me a cup of coffee if you can? There is a sponsorship QR code on the homepage, thank you for your support!
|
||||
|
||||
+276
-86
@@ -6,11 +6,6 @@ on:
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_upload_dist:
|
||||
description: "Test upload-dist.js script"
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
build_os:
|
||||
description: "Build for specific OS: Windows, macOS, Linux, All"
|
||||
required: true
|
||||
@@ -21,97 +16,303 @@ on:
|
||||
- macOS
|
||||
- Linux
|
||||
- All
|
||||
skip_notarize:
|
||||
description: "Skip Notarization (true/false)"
|
||||
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:
|
||||
# parallel build jobs
|
||||
build:
|
||||
name: Build on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
# ============== 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:
|
||||
# if one job fails, do not stop other jobs
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||
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
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- name: Clean workspace on Windows
|
||||
if: runner.os == 'Windows' && (github.event.inputs.build_os == 'Windows' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v'))
|
||||
|
||||
- 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
|
||||
|
||||
- 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: Clean workspace on macOS & Linux
|
||||
if: runner.os == 'macOS' || runner.os == 'Linux' && (github.event.inputs.build_os == 'macOS' || github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v'))
|
||||
run: |
|
||||
rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Ubuntu Update with sudo
|
||||
if: runner.os == 'Linux' && (github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v'))
|
||||
|
||||
# 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: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2
|
||||
- name: Build Windows x64 & ARM64 App
|
||||
if: runner.os == 'Windows' && (github.event.inputs.build_os == 'Windows' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v'))
|
||||
run: pnpm run build:win || true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
- name: Build macOS x64 & ARM64 App
|
||||
if: runner.os == 'macOS' && (github.event.inputs.build_os == 'macOS' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v'))
|
||||
run: pnpm run build:mac || true
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
# macOS Code Signing
|
||||
# p12 证书的 Base64 字符串
|
||||
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
|
||||
# p12 证书密码
|
||||
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
|
||||
# macOS Notarization (公证所需变量)
|
||||
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_notarize }}
|
||||
- name: Build Linux x64 & ARM64 App
|
||||
if: runner.os == 'Linux' && (github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v'))
|
||||
run: pnpm run build:linux || true
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
# Move signed artifacts to dist folder, overwriting existing ones
|
||||
Move-Item -Path "signed-artifact\*.exe" -Destination "dist\" -Force
|
||||
Write-Host "✅ Signed artifacts moved to dist folder."
|
||||
|
||||
# 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-${{ runner.os }}
|
||||
name: PicGo-Windows-${{ matrix.arch }}
|
||||
path: dist/*.*
|
||||
- name: Upload to release.picgo.app
|
||||
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.test_upload_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
|
||||
|
||||
- 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: |
|
||||
pnpm run upload-dist
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
- 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 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"
|
||||
|
||||
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 }}
|
||||
@@ -119,17 +320,7 @@ jobs:
|
||||
PICGO_ENV_S3_LEGACY_ACCOUNT_ID: ${{ secrets.PICGO_ENV_S3_LEGACY_ACCOUNT_ID }}
|
||||
PICGO_ENV_S3_LEGACY_SECRET_ID: ${{ secrets.PICGO_ENV_S3_LEGACY_SECRET_ID }}
|
||||
PICGO_ENV_S3_LEGACY_SECRET_KEY: ${{ secrets.PICGO_ENV_S3_LEGACY_SECRET_KEY }}
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Publish GitHub Dev Release
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: softprops/action-gh-release@v2
|
||||
@@ -140,16 +331,16 @@ jobs:
|
||||
draft: true
|
||||
prerelease: false
|
||||
files: |
|
||||
!artifacts/**/*-unpacked/**
|
||||
artifacts/**/*.exe
|
||||
artifacts/**/*.dmg
|
||||
artifacts/**/*.zip
|
||||
artifacts/**/*.AppImage
|
||||
artifacts/**/*.deb
|
||||
artifacts/**/*.snap
|
||||
artifacts/**/*.tar.gz
|
||||
artifacts/**/*.yml
|
||||
artifacts/**/*.blockmap
|
||||
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
|
||||
@@ -160,13 +351,12 @@ jobs:
|
||||
draft: true
|
||||
prerelease: false
|
||||
files: |
|
||||
!artifacts/**/*-unpacked/**
|
||||
artifacts/**/*.exe
|
||||
artifacts/**/*.dmg
|
||||
artifacts/**/*.zip
|
||||
artifacts/**/*.AppImage
|
||||
artifacts/**/*.deb
|
||||
artifacts/**/*.snap
|
||||
artifacts/**/*.tar.gz
|
||||
artifacts/**/*.yml
|
||||
artifacts/**/*.blockmap
|
||||
dist/*.exe
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
dist/*.snap
|
||||
dist/*.tar.gz
|
||||
dist/*.yml
|
||||
dist/*.blockmap
|
||||
|
||||
@@ -29,3 +29,4 @@ test.js
|
||||
specs/
|
||||
.cache/
|
||||
openspec/
|
||||
bug*
|
||||
@@ -12,12 +12,20 @@ PicGo is an Electron + Vue 3 desktop client. Source lives in `src/`: `src/main`
|
||||
- `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.
|
||||
- `pnpm gen-i18n` — regenerate typed locales after touching `public/i18n/*.yml`.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
Follow ESLint Standard defaults: two-space indentation, single quotes, trailing commas where allowed, and no stray semicolons. Author new modules in TypeScript. Keep renderer files browser-safe; route Node APIs through IPC helpers such as `src/main/events/picgoCoreIPC.ts`. Name Vue components in PascalCase (`UploadPanel.vue`) and use camelCase for utilities. Centralize IPC event names inside `src/universal/events/constants.ts`, and store enums/types under `src/universal/types/` so they stay reusable. Static assets are served from `public/` and resolved via `getStaticPath`/`getStaticFileUrl` (`src/universal/utils/staticPath.ts`); avoid using `__static` directly.
|
||||
Static assets are served from `public/`. In the main process use `getStaticPath`/`getStaticFileUrl` (`src/universal/utils/staticPath.ts`). In the renderer, place assets under `public/` and resolve them via `import.meta.env.BASE_URL + filename` (helper: `src/renderer/utils/static.ts`); do not rely on `__static` in renderer code.
|
||||
- Do not use `as any` under any circumstances; keep typings explicit and safe.
|
||||
- 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()`).
|
||||
- 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`).
|
||||
|
||||
## 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.
|
||||
@@ -27,6 +35,10 @@ Commits follow the PicGo conventional preset enforced by Husky (`pnpm lint:dpdm`
|
||||
|
||||
## Internationalization Tips
|
||||
Add locales by creating `public/i18n/<locale>.yml`, exposing its `LANG_DISPLAY_LABEL`, and registering it in `src/universal/i18n/index.ts`. After editing `public/i18n/*.yml`, run `pnpm gen-i18n` to regenerate TS typings and keep them in sync.
|
||||
- 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`) and run `pnpm gen-i18n`.
|
||||
|
||||
## Serena MCP & Context7 Tools
|
||||
When starting work or if you hit issues, try checking MCP for Serena or Context7 tooling. If available, use those tools to navigate, edit, or fetch docs efficiently.
|
||||
|
||||
@@ -1,3 +1,49 @@
|
||||
## :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)
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## 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://picgo.github.io/PicGo-Doc/guide/getting-started.html#%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B), this FAQ, and closed [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed) first — you will likely find the answer there.
|
||||
> 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://picgo.github.io/PicGo-Doc/guide/getting-started.html#%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B),这份 FAQ,以及那些被关闭的 [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed),应该能找到答案。
|
||||
> 在使用 PicGo 期间你会遇到很多问题,不过很多问题其实之前就有人提问过,也被解决,所以你可以先看看 [使用文档](https://docs.picgo.app/gui/guide/getting-started),这份 FAQ,以及那些被关闭的 [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed),应该能找到答案。
|
||||
|
||||
## 1. Qiniu image host: upload succeeds but images don’t show in Album, or the URL has no `http://` prefix / 七牛图床上传图片成功后,相册里无法显示或图片无`http://`前缀
|
||||
|
||||
@@ -148,106 +148,10 @@ If you run into upload issues with the Gitee image host, PicGo cannot help becau
|
||||
|
||||
## 12. On macOS, PicGo shows “App is damaged”, or it doesn’t respond after installation / macOS系统安装完PicGo显示「文件已损坏」或者安装完打开没有反应
|
||||
|
||||
Because PicGo is not signed, it may be blocked by macOS Gatekeeper.
|
||||
Please try the version >= v2.4.2, which has fixed this issue.
|
||||
|
||||
1. If you see “App is damaged” when opening after installation, do the following:
|
||||
请尝试使用 v2.4.2 及以上版本,已经修复该问题。
|
||||
|
||||
Trust the developer (password required):
|
||||
|
||||
```
|
||||
sudo spctl --master-disable
|
||||
```
|
||||
|
||||
Then remove quarantine attributes from PicGo:
|
||||
|
||||
```
|
||||
xattr -cr /Applications/PicGo.app
|
||||
```
|
||||
|
||||
If you see the following message:
|
||||
|
||||
```sh
|
||||
option -r not recognized
|
||||
|
||||
usage: xattr [-slz] file [file ...]
|
||||
xattr -p [-slz] attr_name file [file ...]
|
||||
xattr -w [-sz] attr_name attr_value file [file ...]
|
||||
xattr -d [-s] attr_name file [file ...]
|
||||
xattr -c [-s] file [file ...]
|
||||
|
||||
The first form lists the names of all xattrs on the given file(s).
|
||||
The second form (-p) prints the value of the xattr attr_name.
|
||||
The third form (-w) sets the value of the xattr attr_name to attr_value.
|
||||
The fourth form (-d) deletes the xattr attr_name.
|
||||
The fifth form (-c) deletes (clears) all xattrs.
|
||||
|
||||
options:
|
||||
-h: print this help
|
||||
-s: act on symbolic links themselves rather than their targets
|
||||
-l: print long format (attr_name: attr_value)
|
||||
-z: compress or decompress (if compressed) attribute value in zip format
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```
|
||||
sudo xattr -d com.apple.quarantine /Applications/PicGo.app/
|
||||
```
|
||||
|
||||
2. If PicGo doesn’t respond after installation, troubleshoot in this order:
|
||||
1. PicGo won’t automatically pop up a main window on macOS — it’s designed as a menu bar app. If you can see the PicGo icon in the menu bar, the installation succeeded; click it to open the menu bar window. See FAQ #7.
|
||||
2. If you’re on an Apple Silicon (M1) Mac and previously had the x64 build installed, then switched to the arm64 build and it doesn’t respond, reboot your Mac.
|
||||
|
||||
因为 PicGo 没有签名,所以会被 macOS 的安全检查所拦下。
|
||||
|
||||
1. 安装后打开遇到「文件已损坏」的情况,请按如下方式操作:
|
||||
|
||||
信任开发者,会要求输入密码:
|
||||
|
||||
```
|
||||
sudo spctl --master-disable
|
||||
```
|
||||
|
||||
然后放行 PicGo :
|
||||
|
||||
```
|
||||
xattr -cr /Applications/PicGo.app
|
||||
```
|
||||
|
||||
然后就能正常打开。
|
||||
|
||||
如果提示以下内容
|
||||
|
||||
```sh
|
||||
option -r not recognized
|
||||
|
||||
usage: xattr [-slz] file [file ...]
|
||||
xattr -p [-slz] attr_name file [file ...]
|
||||
xattr -w [-sz] attr_name attr_value file [file ...]
|
||||
xattr -d [-s] attr_name file [file ...]
|
||||
xattr -c [-s] file [file ...]
|
||||
|
||||
The first form lists the names of all xattrs on the given file(s).
|
||||
The second form (-p) prints the value of the xattr attr_name.
|
||||
The third form (-w) sets the value of the xattr attr_name to attr_value.
|
||||
The fourth form (-d) deletes the xattr attr_name.
|
||||
The fifth form (-c) deletes (clears) all xattrs.
|
||||
|
||||
options:
|
||||
-h: print this help
|
||||
-s: act on symbolic links themselves rather than their targets
|
||||
-l: print long format (attr_name: attr_value)
|
||||
-z: compress or decompress (if compressed) attribute value in zip format
|
||||
```
|
||||
执行命令
|
||||
|
||||
```
|
||||
sudo xattr -d com.apple.quarantine /Applications/PicGo.app/
|
||||
```
|
||||
|
||||
2. 如果安装打开后没有反应,请按下方顺序排查:
|
||||
1. macOS安装好之后,PicGo 是不会弹出主窗口的,因为 PicGo 在 macOS 系统里设计是个顶部栏应用。注意看你顶部栏的图标,如果有 PicGo 的图标,说明安装成功了,点击图标即可打开顶部栏窗口。参考上述第七点。
|
||||
2. 如果你是 M1 的系统,此前装过 PicGo 的 x64 版本,但是后来更新了 arm64 的版本发现打开后没反应,请重启电脑即可。
|
||||
|
||||
## 13. Are third-party plugins claiming to be “PicGo Official image host” trustworthy? / 所谓「PicGo 官方图床」的第三方插件是否可信
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ PicGo supports mainstream Image hosts out of the box, and can be extended indefi
|
||||
- **International / open platforms**: GitHub, SM.MS, Imgur
|
||||
- **More options via plugins**: AWS S3, Cloudflare R2, MinIO, and more
|
||||
|
||||
> **Note**: PicGo itself will no longer add new third-party Image hosts by default. You can build Image host plugins yourself—see [PicGo-Core](https://picgo.github.io/PicGo-Core-Doc/).
|
||||
> **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/).
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
@@ -66,7 +66,7 @@ PicGo is built around a fast, low-friction image upload experience:
|
||||
|
||||
### 🚀 Fast uploads
|
||||
- **Multiple ways to upload**: drag & drop, paste from clipboard, hotkeys, and even right-click context menu upload on macOS/Windows.
|
||||
- **Global hotkey**: press `Command+Shift+P` (macOS) / `Ctrl+Shift+P` (Windows/Linux) to open the upload window without leaving your current app. The global key can be customized.
|
||||
- **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.
|
||||
@@ -76,11 +76,11 @@ PicGo is built around a fast, low-friction image upload experience:
|
||||
### 🛠 Developer-friendly
|
||||
- **HTTP API**: upload via HTTP requests (v2.2.0+), making it easy to integrate with other tools.
|
||||
- **Open source**: fully open-source and transparent.
|
||||
- **Great documentation**: detailed docs help you get started quickly. For plugin development, see the [PicGo-Core docs](https://picgo.github.io/PicGo-Core-Doc/).
|
||||
- **Great documentation**: detailed docs help you get started quickly. For plugin development, see the [PicGo-Core docs](https://docs.picgo.app/core/).
|
||||
|
||||
> There’s more to discover—development progress is tracked in [Projects](https://github.com/Molunerfinn/PicGo/projects).
|
||||
|
||||
If you’re new to PicGo, start with the [User Guide](https://picgo.github.io/PicGo-Doc/guide/getting-started.html). If you run into issues, check the [FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md) and closed [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed).
|
||||
If you’re 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).
|
||||
|
||||
## Download & Install
|
||||
|
||||
|
||||
+4
-5
@@ -53,7 +53,7 @@ PicGo 原生支持主流图床平台,并可通过插件系统无限扩展:
|
||||
- **国际/开源平台**:GitHub、SM.MS、Imgur
|
||||
- **更多支持**:通过插件支持 AWS S3、Cloudflare R2、MinIO 等第三方图床
|
||||
|
||||
> **注意**:PicGo 本体不再增加默认的第三方图床支持。你可以自行开发第三方图床插件。详见 [PicGo-Core](https://picgo.github.io/PicGo-Core-Doc/)。
|
||||
> **注意**:PicGo 本体不再增加默认的第三方图床支持。你可以自行开发第三方图床插件。详见 [PicGo-Core](https://docs.picgo.app/core/)。
|
||||
|
||||
## ✨ 特色功能
|
||||
|
||||
@@ -67,7 +67,7 @@ PicGo 打造了全方位的上传体验,让“传图”这件事变得前所
|
||||
|
||||
### 🚀 极速上传体验
|
||||
- **多维上传方式**:支持拖拽图片、剪贴板粘贴、快捷键上传,甚至在 macOS/Windows 上支持右键菜单直接上传。
|
||||
- **全局快捷键**:默认 `Command+Shift+P` (macOS) / `Ctrl+Shift+P` (Windows/Linux) 即可唤起上传,无需离开当前窗口。 快捷键可自定义。
|
||||
- **全局快捷键**:默认 `Command+Shift+U` (macOS) / `Ctrl+Shift+U` (Windows/Linux) 即可唤起上传,无需离开当前窗口。 快捷键可自定义。
|
||||
|
||||
### 🧩 强大的插件生态
|
||||
- **高度可扩展**:PicGo 拥有丰富的插件系统,已有插件支持 AWS S3、Cloudflare R2、MinIO 等第三方图床。
|
||||
@@ -77,12 +77,11 @@ PicGo 打造了全方位的上传体验,让“传图”这件事变得前所
|
||||
### 🛠 开发者友好
|
||||
- **HTTP API**:支持通过 HTTP 请求调用 PicGo 上传 (v2.2.0+),方便与其他工具集成。
|
||||
- **开源透明**:代码完全开源,安全可靠。
|
||||
- **丰富的文档**:详尽的开发文档助你快速上手。插件开发请参考 [PicGo-Core 文档](https://picgo.github.io/PicGo-Core-Doc/)。
|
||||
- **丰富的文档**:详尽的开发文档助你快速上手。插件开发请参考 [PicGo-Core 文档](https://docs.picgo.app/core/)。
|
||||
|
||||
> 更多功能等你自己去发现,开发进度可以查看 [Projects](https://github.com/Molunerfinn/PicGo/projects)。
|
||||
|
||||
**如果第一次使用,请参考应用 [使用文档](https://picgo.github.io/PicGo-Doc/guide/getting-started.html)。遇到问题了还可以看看 [FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md) 以及被关闭的 [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed)。**
|
||||
|
||||
**如果第一次使用,请参考应用 [使用文档](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)。**
|
||||
|
||||
## 下载安装
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# 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 问题
|
||||
-216
@@ -1,216 +0,0 @@
|
||||
<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/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/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
|
||||
| ©2017 - {{ year }} #[a(href="https://github.com/Molunerfinn" target="_blank") Molunerfinn]
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'HomePage',
|
||||
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>
|
||||
@@ -1,10 +0,0 @@
|
||||
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')
|
||||
@@ -1,12 +0,0 @@
|
||||
<!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>
|
||||
@@ -3,7 +3,7 @@ import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const shouldNotarize = process.env.SKIP_NOTARIZE !== 'true';
|
||||
const shouldNotarize = process.env.SKIP_NOTARIZE !== 'true'
|
||||
|
||||
const config: Configuration = {
|
||||
appId: 'com.molunerfinn.picgo',
|
||||
|
||||
+2
-1
@@ -63,7 +63,8 @@ module.exports = [
|
||||
'test/unit/coverage/**',
|
||||
'test/unit/*.js',
|
||||
'test/e2e/*.js',
|
||||
'node_modules/**'
|
||||
'node_modules/**',
|
||||
'vitest.config.ts',
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+10
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "picgo",
|
||||
"version": "2.4.3",
|
||||
"version": "2.5.1",
|
||||
"private": true,
|
||||
"main": "dist_electron/main/index.js",
|
||||
"description": "A powerful & simple image uploader for creators.",
|
||||
@@ -9,13 +9,14 @@
|
||||
"email": "marksz@teamsz.xyz"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "electron-vite build && electron-builder --config electron-builder.config.ts --publish never",
|
||||
"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 && 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",
|
||||
@@ -26,8 +27,8 @@
|
||||
"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",
|
||||
"check": "pnpm run tsc && pnpm run lint",
|
||||
"test": "vitest run src/__tests__",
|
||||
"check": "pnpm run tsc && pnpm run vue-tsc && pnpm run lint",
|
||||
"test": "vitest run",
|
||||
"prepare": "husky",
|
||||
"commitlint": "commitlint --edit"
|
||||
},
|
||||
@@ -38,13 +39,14 @@
|
||||
"@picgo/video-duration": "^1.0.1",
|
||||
"axios": "^0.19.0",
|
||||
"clip-filepaths": "^0.3.0",
|
||||
"comment-json": "^4.5.1",
|
||||
"compare-versions": "^4.1.3",
|
||||
"core-js": "^3.27.1",
|
||||
"dayjs": "^1.11.19",
|
||||
"element-plus": "^2.3.7",
|
||||
"epipebomb": "^1.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"keycode": "^2.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-id": "^0.14.0",
|
||||
@@ -53,7 +55,7 @@
|
||||
"mime-types": "^3.0.2",
|
||||
"mitt": "^3.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"picgo": "^1.8.1",
|
||||
"picgo": "^2.0.1",
|
||||
"qrcode.vue": "^3.3.3",
|
||||
"semver": "^7.7.3",
|
||||
"shell-path": "2.1.0",
|
||||
@@ -112,7 +114,8 @@
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.6",
|
||||
"vitest": "^4.0.16"
|
||||
"vitest": "^4.0.16",
|
||||
"vue-tsc": "^3.2.3"
|
||||
},
|
||||
"commitlint": {
|
||||
"extends": [
|
||||
|
||||
Generated
+291
-22
@@ -26,6 +26,9 @@ importers:
|
||||
clip-filepaths:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0
|
||||
comment-json:
|
||||
specifier: ^4.5.1
|
||||
version: 4.5.1
|
||||
compare-versions:
|
||||
specifier: ^4.1.3
|
||||
version: 4.1.4
|
||||
@@ -45,7 +48,7 @@ importers:
|
||||
specifier: ^10.0.0
|
||||
version: 10.1.0
|
||||
js-yaml:
|
||||
specifier: ^4.1.0
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1
|
||||
keycode:
|
||||
specifier: ^2.2.0
|
||||
@@ -72,8 +75,8 @@ importers:
|
||||
specifier: ^1.4.5-lts.1
|
||||
version: 1.4.5-lts.2
|
||||
picgo:
|
||||
specifier: ^1.8.1
|
||||
version: 1.8.1
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
qrcode.vue:
|
||||
specifier: ^3.3.3
|
||||
version: 3.6.0(vue@3.5.25(typescript@5.9.3))
|
||||
@@ -246,6 +249,9 @@ importers:
|
||||
vitest:
|
||||
specifier: ^4.0.16
|
||||
version: 4.0.16(@types/node@20.19.26)(jiti@1.21.7)(stylus@0.54.8)
|
||||
vue-tsc:
|
||||
specifier: ^3.2.3
|
||||
version: 3.2.3(typescript@5.9.3)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -835,6 +841,12 @@ packages:
|
||||
'@floating-ui/utils@0.2.10':
|
||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||
|
||||
'@hono/node-server@1.19.9':
|
||||
resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
peerDependencies:
|
||||
hono: ^4
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@@ -1666,6 +1678,15 @@ packages:
|
||||
'@vitest/utils@4.0.16':
|
||||
resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==}
|
||||
|
||||
'@volar/language-core@2.4.27':
|
||||
resolution: {integrity: sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==}
|
||||
|
||||
'@volar/source-map@2.4.27':
|
||||
resolution: {integrity: sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==}
|
||||
|
||||
'@volar/typescript@2.4.27':
|
||||
resolution: {integrity: sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==}
|
||||
|
||||
'@vue/compiler-core@3.5.25':
|
||||
resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==}
|
||||
|
||||
@@ -1681,6 +1702,9 @@ packages:
|
||||
'@vue/devtools-api@6.6.4':
|
||||
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
||||
|
||||
'@vue/language-core@3.2.3':
|
||||
resolution: {integrity: sha512-VpN/GnYDzGLh44AI6i1OB/WsLXo6vwnl0EWHBelGc4TyC0yEq6azwNaed/+Tgr8anFlSdWYnMEkyHJDPe7ii7A==}
|
||||
|
||||
'@vue/reactivity@3.5.25':
|
||||
resolution: {integrity: sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==}
|
||||
|
||||
@@ -1747,6 +1771,9 @@ packages:
|
||||
ajv@8.17.1:
|
||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||
|
||||
alien-signals@3.1.2:
|
||||
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
|
||||
|
||||
ansi-escapes@3.2.0:
|
||||
resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1834,6 +1861,10 @@ packages:
|
||||
array-timsort@1.0.3:
|
||||
resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
|
||||
|
||||
array-union@2.1.0:
|
||||
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
array.prototype.findlastindex@1.2.6:
|
||||
resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1907,8 +1938,8 @@ packages:
|
||||
resolution: {integrity: sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==}
|
||||
deprecated: Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410
|
||||
|
||||
axios@1.13.2:
|
||||
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
|
||||
axios@1.13.3:
|
||||
resolution: {integrity: sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g==}
|
||||
|
||||
babel-polyfill@6.26.0:
|
||||
resolution: {integrity: sha512-F2rZGQnAdaHWQ8YAoeRbukc7HS9QgdgeyJ0rQDd485v9opwuPvjpPFcOOT/WmkKTdgy9ESgSPXDcTNpzrGr6iQ==}
|
||||
@@ -1984,6 +2015,10 @@ packages:
|
||||
builder-util@26.1.0:
|
||||
resolution: {integrity: sha512-BTUhmpkCuEAAUmc8EJkJOg7fMGsDSSRMPn1QTpoUpYpGp3SjyJV18LlCPTu3+UBfuQ/5ua7KqDLrsK9NAXO7fg==}
|
||||
|
||||
bundle-name@4.1.0:
|
||||
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
busboy@1.6.0:
|
||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||
engines: {node: '>=10.16.0'}
|
||||
@@ -2110,6 +2145,12 @@ packages:
|
||||
resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
citty@0.1.6:
|
||||
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
|
||||
|
||||
citty@0.2.0:
|
||||
resolution: {integrity: sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==}
|
||||
|
||||
cli-cursor@2.1.0:
|
||||
resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2236,10 +2277,6 @@ packages:
|
||||
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
|
||||
engines: {node: ^12.20.0 || >=14}
|
||||
|
||||
comment-json@4.4.1:
|
||||
resolution: {integrity: sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
comment-json@4.5.1:
|
||||
resolution: {integrity: sha512-taEtr3ozUmOB7it68Jll7s0Pwm+aoiHyXKrEC8SEodL4rNpdfDLqa7PfBlrgFoCNNdR8ImL+muti5IGvktJAAg==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -2269,6 +2306,10 @@ packages:
|
||||
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
|
||||
engines: {'0': node >= 0.8}
|
||||
|
||||
consola@3.4.2:
|
||||
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
|
||||
engines: {node: ^14.18.0 || >=16.10.0}
|
||||
|
||||
conventional-changelog-angular@1.6.6:
|
||||
resolution: {integrity: sha512-suQnFSqCxRwyBxY68pYTsFkG0taIdinHLNEAX5ivtw8bCRnIgnpvcHmlR/yjUyZIrNPYAoXlY1WiEKWgSE4BNg==}
|
||||
|
||||
@@ -2497,6 +2538,14 @@ packages:
|
||||
deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
default-browser-id@5.0.1:
|
||||
resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
default-browser@5.4.0:
|
||||
resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
default-shell@1.0.1:
|
||||
resolution: {integrity: sha512-/Os8tTMPSriNHCsVj3VLjMZblIl1sIg8EXz3qg7C5K+y9calfTA/qzlfPvCQ+LEgLWmtZ9wCnzE1w+S6TPPFyQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2512,10 +2561,17 @@ packages:
|
||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
define-lazy-prop@3.0.0:
|
||||
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
define-properties@1.2.1:
|
||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
defu@6.1.4:
|
||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@@ -2541,6 +2597,10 @@ packages:
|
||||
dir-compare@4.2.0:
|
||||
resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
dlv@1.1.3:
|
||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||
|
||||
@@ -3105,6 +3165,10 @@ packages:
|
||||
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
giget@2.0.0:
|
||||
resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
|
||||
hasBin: true
|
||||
|
||||
git-raw-commits@1.3.6:
|
||||
resolution: {integrity: sha512-svsK26tQ8vEKnMshTDatSIQSMDdz8CxIIqKsvPqbtV23Etmw6VNaFAitu8zwZ0VrOne7FztwPyRLxK7/DIUTQg==}
|
||||
hasBin: true
|
||||
@@ -3174,6 +3238,10 @@ packages:
|
||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
globby@11.1.0:
|
||||
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3229,6 +3297,10 @@ packages:
|
||||
resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
hono@4.11.6:
|
||||
resolution: {integrity: sha512-ofIiiHyl34SV6AuhE3YT2mhO5HRWokce+eUYE82TsP6z0/H3JeJcjVWEMSIAiw2QkjDOEpES/lYsg8eEbsLtdw==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
hosted-git-info@2.8.9:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
|
||||
@@ -3414,6 +3486,11 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
is-docker@3.0.0:
|
||||
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
hasBin: true
|
||||
|
||||
is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3438,6 +3515,15 @@ packages:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-in-ssh@1.0.0:
|
||||
resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
is-inside-container@1.0.0:
|
||||
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
|
||||
engines: {node: '>=14.16'}
|
||||
hasBin: true
|
||||
|
||||
is-interactive@1.0.0:
|
||||
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3532,6 +3618,10 @@ packages:
|
||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-wsl@3.1.0:
|
||||
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
@@ -3964,6 +4054,9 @@ packages:
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
muggle-string@0.4.1:
|
||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||
|
||||
multer@1.4.5-lts.2:
|
||||
resolution: {integrity: sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
@@ -4010,6 +4103,9 @@ packages:
|
||||
node-api-version@0.2.1:
|
||||
resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==}
|
||||
|
||||
node-fetch-native@1.6.7:
|
||||
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
|
||||
|
||||
node-gyp@11.5.0:
|
||||
resolution: {integrity: sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
@@ -4056,6 +4152,11 @@ packages:
|
||||
resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
nypm@0.6.4:
|
||||
resolution: {integrity: sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -4102,6 +4203,10 @@ packages:
|
||||
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
open@11.0.0:
|
||||
resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -4196,6 +4301,9 @@ packages:
|
||||
resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
|
||||
path-exists@3.0.0:
|
||||
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -4227,6 +4335,10 @@ packages:
|
||||
resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
path-type@4.0.0:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
@@ -4237,9 +4349,9 @@ packages:
|
||||
pend@1.2.0:
|
||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||
|
||||
picgo@1.8.1:
|
||||
resolution: {integrity: sha512-FDwo1iZN8kFtF1nzM+2BYtpAw4oXPlikvrJ75o7/qEaBazLApJ4DXVyAocK2OQ9exFWV2beKuavtxIcro3lTrg==}
|
||||
engines: {node: '>= 22.12.0'}
|
||||
picgo@2.0.1:
|
||||
resolution: {integrity: sha512-AC98GsS3zmDZzTAg55oavreBgJRwNnRtfqvauAAuTanMC0QU3sidBEYsOG2SWdDMlzN/RDxybOo7t9z6Asouaw==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
hasBin: true
|
||||
|
||||
picocolors@1.1.1:
|
||||
@@ -4344,6 +4456,10 @@ packages:
|
||||
engines: {node: '>=14.0.0'}
|
||||
hasBin: true
|
||||
|
||||
powershell-utils@0.1.0:
|
||||
resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -4554,6 +4670,10 @@ packages:
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
run-applescript@7.1.0:
|
||||
resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
run-async@2.4.1:
|
||||
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
@@ -4709,6 +4829,10 @@ packages:
|
||||
resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
slash@3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
slice-ansi@3.0.0:
|
||||
resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -5221,6 +5345,9 @@ packages:
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
vscode-uri@3.1.0:
|
||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||
|
||||
vue-demi@0.12.5:
|
||||
resolution: {integrity: sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -5254,6 +5381,12 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
vue-tsc@3.2.3:
|
||||
resolution: {integrity: sha512-1RdRB7rQXGFMdpo0aXf9spVzWEPGAk7PEb/ejHQwVrcuQA/HsGiixIc3uBQeqY2YjeEEgvr2ShQewBgcN4c1Cw==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: '>=5.0.0'
|
||||
|
||||
vue3-lazyload@0.3.8:
|
||||
resolution: {integrity: sha512-UiJHRT7mzry102WbhtrRgJh+f8Z8u4Z+H1RU4dvPmQeq7wFSDFxZB9iJOWGihH2FscXN/8rMGLDOQJAmjwqpCg==}
|
||||
peerDependencies:
|
||||
@@ -5348,6 +5481,10 @@ packages:
|
||||
resolution: {integrity: sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
|
||||
wsl-utils@0.3.1:
|
||||
resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
xml-name-validator@4.0.0:
|
||||
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -6374,6 +6511,10 @@ snapshots:
|
||||
|
||||
'@floating-ui/utils@0.2.10': {}
|
||||
|
||||
'@hono/node-server@1.19.9(hono@4.11.6)':
|
||||
dependencies:
|
||||
hono: 4.11.6
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
@@ -6621,7 +6762,7 @@ snapshots:
|
||||
'@types/bson': 4.2.4
|
||||
'@types/graceful-fs': 4.1.9
|
||||
'@types/lodash': 4.17.21
|
||||
comment-json: 4.4.1
|
||||
comment-json: 4.5.1
|
||||
fflate: 0.7.4
|
||||
lodash: 4.17.21
|
||||
lodash-id: 0.14.1
|
||||
@@ -7368,6 +7509,18 @@ snapshots:
|
||||
'@vitest/pretty-format': 4.0.16
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@volar/language-core@2.4.27':
|
||||
dependencies:
|
||||
'@volar/source-map': 2.4.27
|
||||
|
||||
'@volar/source-map@2.4.27': {}
|
||||
|
||||
'@volar/typescript@2.4.27':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.27
|
||||
path-browserify: 1.0.1
|
||||
vscode-uri: 3.1.0
|
||||
|
||||
'@vue/compiler-core@3.5.25':
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.5
|
||||
@@ -7400,6 +7553,16 @@ snapshots:
|
||||
|
||||
'@vue/devtools-api@6.6.4': {}
|
||||
|
||||
'@vue/language-core@3.2.3':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.27
|
||||
'@vue/compiler-dom': 3.5.25
|
||||
'@vue/shared': 3.5.25
|
||||
alien-signals: 3.1.2
|
||||
muggle-string: 0.4.1
|
||||
path-browserify: 1.0.1
|
||||
picomatch: 4.0.3
|
||||
|
||||
'@vue/reactivity@3.5.25':
|
||||
dependencies:
|
||||
'@vue/shared': 3.5.25
|
||||
@@ -7481,6 +7644,8 @@ snapshots:
|
||||
require-from-string: 2.0.2
|
||||
optional: true
|
||||
|
||||
alien-signals@3.1.2: {}
|
||||
|
||||
ansi-escapes@3.2.0: {}
|
||||
|
||||
ansi-escapes@4.3.2:
|
||||
@@ -7589,6 +7754,8 @@ snapshots:
|
||||
|
||||
array-timsort@1.0.3: {}
|
||||
|
||||
array-union@2.1.0: {}
|
||||
|
||||
array.prototype.findlastindex@1.2.6:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -7667,7 +7834,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
axios@1.13.2:
|
||||
axios@1.13.3:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
form-data: 4.0.5
|
||||
@@ -7775,6 +7942,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
bundle-name@4.1.0:
|
||||
dependencies:
|
||||
run-applescript: 7.1.0
|
||||
|
||||
busboy@1.6.0:
|
||||
dependencies:
|
||||
streamsearch: 1.1.0
|
||||
@@ -7909,6 +8080,12 @@ snapshots:
|
||||
|
||||
ci-info@4.3.1: {}
|
||||
|
||||
citty@0.1.6:
|
||||
dependencies:
|
||||
consola: 3.4.2
|
||||
|
||||
citty@0.2.0: {}
|
||||
|
||||
cli-cursor@2.1.0:
|
||||
dependencies:
|
||||
restore-cursor: 2.0.0
|
||||
@@ -8009,12 +8186,6 @@ snapshots:
|
||||
commander@9.5.0:
|
||||
optional: true
|
||||
|
||||
comment-json@4.4.1:
|
||||
dependencies:
|
||||
array-timsort: 1.0.3
|
||||
core-util-is: 1.0.3
|
||||
esprima: 4.0.1
|
||||
|
||||
comment-json@4.5.1:
|
||||
dependencies:
|
||||
array-timsort: 1.0.3
|
||||
@@ -8064,6 +8235,8 @@ snapshots:
|
||||
readable-stream: 2.3.8
|
||||
typedarray: 0.0.6
|
||||
|
||||
consola@3.4.2: {}
|
||||
|
||||
conventional-changelog-angular@1.6.6:
|
||||
dependencies:
|
||||
compare-func: 1.3.4
|
||||
@@ -8344,6 +8517,13 @@ snapshots:
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
default-browser-id@5.0.1: {}
|
||||
|
||||
default-browser@5.4.0:
|
||||
dependencies:
|
||||
bundle-name: 4.1.0
|
||||
default-browser-id: 5.0.1
|
||||
|
||||
default-shell@1.0.1: {}
|
||||
|
||||
defaults@1.0.4:
|
||||
@@ -8358,12 +8538,16 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
define-lazy-prop@3.0.0: {}
|
||||
|
||||
define-properties@1.2.1:
|
||||
dependencies:
|
||||
define-data-property: 1.1.4
|
||||
has-property-descriptors: 1.0.2
|
||||
object-keys: 1.1.1
|
||||
|
||||
defu@6.1.4: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
detect-file@1.0.0: {}
|
||||
@@ -8382,6 +8566,10 @@ snapshots:
|
||||
minimatch: 3.1.2
|
||||
p-limit: 3.1.0
|
||||
|
||||
dir-glob@3.0.1:
|
||||
dependencies:
|
||||
path-type: 4.0.0
|
||||
|
||||
dlv@1.1.3: {}
|
||||
|
||||
dmg-builder@26.1.0(electron-builder-squirrel-windows@26.1.0):
|
||||
@@ -9158,6 +9346,15 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
giget@2.0.0:
|
||||
dependencies:
|
||||
citty: 0.1.6
|
||||
consola: 3.4.2
|
||||
defu: 6.1.4
|
||||
node-fetch-native: 1.6.7
|
||||
nypm: 0.6.4
|
||||
pathe: 2.0.3
|
||||
|
||||
git-raw-commits@1.3.6:
|
||||
dependencies:
|
||||
dargs: 4.1.0
|
||||
@@ -9256,6 +9453,15 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
gopd: 1.2.0
|
||||
|
||||
globby@11.1.0:
|
||||
dependencies:
|
||||
array-union: 2.1.0
|
||||
dir-glob: 3.0.1
|
||||
fast-glob: 3.3.3
|
||||
ignore: 5.3.2
|
||||
merge2: 1.4.1
|
||||
slash: 3.0.0
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
got@11.8.6:
|
||||
@@ -9313,6 +9519,8 @@ snapshots:
|
||||
dependencies:
|
||||
parse-passwd: 1.0.0
|
||||
|
||||
hono@4.11.6: {}
|
||||
|
||||
hosted-git-info@2.8.9: {}
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
@@ -9526,6 +9734,8 @@ snapshots:
|
||||
|
||||
is-docker@2.2.1: {}
|
||||
|
||||
is-docker@3.0.0: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-finalizationregistry@1.1.1:
|
||||
@@ -9548,6 +9758,12 @@ snapshots:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-in-ssh@1.0.0: {}
|
||||
|
||||
is-inside-container@1.0.0:
|
||||
dependencies:
|
||||
is-docker: 3.0.0
|
||||
|
||||
is-interactive@1.0.0: {}
|
||||
|
||||
is-map@2.0.3: {}
|
||||
@@ -9624,6 +9840,10 @@ snapshots:
|
||||
dependencies:
|
||||
is-docker: 2.2.1
|
||||
|
||||
is-wsl@3.1.0:
|
||||
dependencies:
|
||||
is-inside-container: 1.0.0
|
||||
|
||||
isarray@1.0.0: {}
|
||||
|
||||
isarray@2.0.5: {}
|
||||
@@ -10044,6 +10264,8 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
muggle-string@0.4.1: {}
|
||||
|
||||
multer@1.4.5-lts.2:
|
||||
dependencies:
|
||||
append-field: 1.0.0
|
||||
@@ -10087,6 +10309,8 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 7.7.3
|
||||
|
||||
node-fetch-native@1.6.7: {}
|
||||
|
||||
node-gyp@11.5.0:
|
||||
dependencies:
|
||||
env-paths: 2.2.1
|
||||
@@ -10140,6 +10364,12 @@ snapshots:
|
||||
|
||||
number-is-nan@1.0.1: {}
|
||||
|
||||
nypm@0.6.4:
|
||||
dependencies:
|
||||
citty: 0.2.0
|
||||
pathe: 2.0.3
|
||||
tinyexec: 1.0.2
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-hash@3.0.0: {}
|
||||
@@ -10191,6 +10421,15 @@ snapshots:
|
||||
dependencies:
|
||||
mimic-fn: 2.1.0
|
||||
|
||||
open@11.0.0:
|
||||
dependencies:
|
||||
default-browser: 5.4.0
|
||||
define-lazy-prop: 3.0.0
|
||||
is-in-ssh: 1.0.0
|
||||
is-inside-container: 1.0.0
|
||||
powershell-utils: 0.1.0
|
||||
wsl-utils: 0.3.1
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -10291,6 +10530,8 @@ snapshots:
|
||||
|
||||
parse-passwd@1.0.0: {}
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
path-exists@3.0.0: {}
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
@@ -10312,24 +10553,32 @@ snapshots:
|
||||
dependencies:
|
||||
pify: 3.0.0
|
||||
|
||||
path-type@4.0.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
pe-library@0.4.1: {}
|
||||
|
||||
pend@1.2.0: {}
|
||||
|
||||
picgo@1.8.1:
|
||||
picgo@2.0.1:
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.9(hono@4.11.6)
|
||||
'@picgo/i18n': 1.0.0
|
||||
'@picgo/store': 2.1.0
|
||||
axios: 1.13.2
|
||||
axios: 1.13.3
|
||||
chalk: 2.4.2
|
||||
commander: 8.3.0
|
||||
comment-json: 4.5.1
|
||||
cross-spawn: 6.0.6
|
||||
dayjs: 1.11.19
|
||||
dotenv: 17.2.3
|
||||
ejs: 3.1.10
|
||||
form-data: 4.0.5
|
||||
fs-extra: 6.0.1
|
||||
giget: 2.0.0
|
||||
globby: 11.1.0
|
||||
hono: 4.11.6
|
||||
image-size: 0.8.3
|
||||
inquirer: 6.5.2
|
||||
is-wsl: 2.2.0
|
||||
@@ -10338,6 +10587,7 @@ snapshots:
|
||||
md5: 2.3.0
|
||||
mime-types: 3.0.2
|
||||
minimist: 1.2.8
|
||||
open: 11.0.0
|
||||
resolve: 1.22.11
|
||||
tunnel: 0.0.6
|
||||
transitivePeerDependencies:
|
||||
@@ -10424,6 +10674,8 @@ snapshots:
|
||||
commander: 9.5.0
|
||||
optional: true
|
||||
|
||||
powershell-utils@0.1.0: {}
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
proc-log@5.0.0: {}
|
||||
@@ -10662,6 +10914,8 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc': 4.53.3
|
||||
fsevents: 2.3.3
|
||||
|
||||
run-applescript@7.1.0: {}
|
||||
|
||||
run-async@2.4.1: {}
|
||||
|
||||
run-async@4.0.6: {}
|
||||
@@ -10816,6 +11070,8 @@ snapshots:
|
||||
|
||||
slash@2.0.0: {}
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
slice-ansi@3.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -11360,6 +11616,8 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vscode-uri@3.1.0: {}
|
||||
|
||||
vue-demi@0.12.5(vue@3.5.25(typescript@5.9.3)):
|
||||
dependencies:
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
@@ -11385,6 +11643,12 @@ snapshots:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
|
||||
vue-tsc@3.2.3(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.4.27
|
||||
'@vue/language-core': 3.2.3
|
||||
typescript: 5.9.3
|
||||
|
||||
vue3-lazyload@0.3.8(vue@3.5.25(typescript@5.9.3)):
|
||||
dependencies:
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
@@ -11503,6 +11767,11 @@ snapshots:
|
||||
imurmurhash: 0.1.4
|
||||
signal-exit: 4.1.0
|
||||
|
||||
wsl-utils@0.3.1:
|
||||
dependencies:
|
||||
is-wsl: 3.1.0
|
||||
powershell-utils: 0.1.0
|
||||
|
||||
xml-name-validator@4.0.0: {}
|
||||
|
||||
xmlbuilder@15.1.1: {}
|
||||
|
||||
+74
-33
@@ -3,7 +3,7 @@ ABOUT: About
|
||||
OPEN_MAIN_WINDOW: Open Main Window
|
||||
CHOOSE_DEFAULT_PICBED: Choose Default Picbed
|
||||
OPEN_UPDATE_HELPER: Open Update Helper
|
||||
PRIVACY_AGREEMENT: Privacy Agreement
|
||||
PRIVACY_TERMS_AGREEMENT: Privacy & Terms Agreement
|
||||
RELOAD_APP: Reload App
|
||||
UPLOAD_FAILED: Upload Failed
|
||||
UPLOAD_SUCCEED: Upload Succeed
|
||||
@@ -34,6 +34,75 @@ GALLERY: Gallery
|
||||
PICBEDS_SETTINGS: Picbeds Settings
|
||||
PICGO_SETTINGS: PicGo Settings
|
||||
PLUGIN_SETTINGS: Plugins Settings
|
||||
PICGO_CLOUD_TITLE: PicGo Cloud
|
||||
PICGO_CLOUD_ERROR_TITLE: PicGo Cloud Error
|
||||
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_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
|
||||
@@ -304,39 +373,11 @@ 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: >
|
||||
|
||||
This software respects and protects the personal privacy of all users who use the service. In order to provide you with more accurate and better services, this software will use and collect some of your behavioral information in accordance with the provisions of this Privacy Policy. When you agree to the software service use agreement, you are deemed to have agreed to the entire content of this privacy policy. This privacy policy is an integral part of the software service use agreement, and it will not be used if you do not agree.
|
||||
This Agreement will be updated periodically.
|
||||
|
||||
1. Scope of application
|
||||
|
||||
|
||||
a) When you use this software, this software will record some information about your operation behavior of this software, including but not limited to the time-consuming, type, quantity and other information of your use of this software to upload files.
|
||||
|
||||
|
||||
2. Use of Information
|
||||
|
||||
|
||||
a) After obtaining your usage data, the software will upload it to the data analysis server so as to provide you with better services after analyzing the data.
|
||||
|
||||
|
||||
3. Information disclosure
|
||||
|
||||
|
||||
a) This software will not disclose your information to untrusted third parties.
|
||||
|
||||
|
||||
b) In accordance with the relevant provisions of the law, or the requirements of administrative or judicial institutions, disclose to third parties or administrative or judicial institutions;
|
||||
|
||||
|
||||
|
||||
|
||||
4. Information Security
|
||||
|
||||
|
||||
a) This software does not collect your personal information, key information and other private information, and the collected information is only used for improving the software, optimizing the experience, and understanding the daily activities of the software.
|
||||
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
|
||||
|
||||
+74
-33
@@ -3,7 +3,7 @@ ABOUT: 关于
|
||||
OPEN_MAIN_WINDOW: 打开主窗口
|
||||
CHOOSE_DEFAULT_PICBED: 选择默认图床
|
||||
OPEN_UPDATE_HELPER: 打开更新助手
|
||||
PRIVACY_AGREEMENT: 隐私协议
|
||||
PRIVACY_TERMS_AGREEMENT: 隐私与条款协议
|
||||
RELOAD_APP: 重启应用
|
||||
UPLOAD_SUCCEED: 上传成功
|
||||
UPLOAD_FAILED: 上传失败
|
||||
@@ -34,6 +34,75 @@ GALLERY: 相册
|
||||
PICBEDS_SETTINGS: 图床设置
|
||||
PICGO_SETTINGS: PicGo设置
|
||||
PLUGIN_SETTINGS: 插件设置
|
||||
PICGO_CLOUD_TITLE: PicGo Cloud
|
||||
PICGO_CLOUD_ERROR_TITLE: 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_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: 微信支付
|
||||
@@ -304,39 +373,11 @@ 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: >
|
||||
|
||||
本软件尊重并保护所有使用服务用户的个人隐私权。为了给您提供更准确、更优质的服务,本软件会按照本隐私权政策的规定使用和收集您的一些行为信息。您在同意本软件服务使用协议之时,即视为您已经同意本隐私权政策全部内容。本隐私权政策属于本软件服务使用协议不可分割的一部分,如果不同意将无法使用。本协议会定期更新。
|
||||
|
||||
|
||||
1.适用范围
|
||||
|
||||
|
||||
a)在您使用本软件时,本软件会记录的您对本软件的一些操作行为信息,包括但不限于您使用本软件进行文件上传的耗时、类型、数量等信息。
|
||||
|
||||
|
||||
2.信息的使用
|
||||
|
||||
|
||||
a)在获得您的使用数据之后,本软件会将其上传至数据分析服务器,以便分析数据后,提供给您更好的服务。
|
||||
|
||||
|
||||
3.信息披露
|
||||
|
||||
|
||||
a)本软件不会将您的信息披露给不受信任的第三方。
|
||||
|
||||
|
||||
b)根据法律的有关规定,或者行政或司法机构的要求,向第三方或者行政、司法机构披露;
|
||||
|
||||
|
||||
|
||||
|
||||
4.信息安全
|
||||
|
||||
|
||||
a)本软件不会收集您的个人信息、密钥信息等隐私信息,所收集的信息仅仅作为改善软件、优化体验、了解软件日活等用途。
|
||||
PRIVACY: "使用前请阅读并同意隐私政策 ${privacyUrl} 与服务条款 ${termsUrl},同意后方可使用。"
|
||||
PRIVACY_TIPS: 请同意隐私协议,否则无法上传。
|
||||
QUIT: 退出
|
||||
|
||||
+74
-33
@@ -3,7 +3,7 @@ ABOUT: 關於
|
||||
OPEN_MAIN_WINDOW: 打開主視窗
|
||||
CHOOSE_DEFAULT_PICBED: 選擇預設圖床
|
||||
OPEN_UPDATE_HELPER: 開啟更新助手
|
||||
PRIVACY_AGREEMENT: 隱私協議
|
||||
PRIVACY_TERMS_AGREEMENT: 隱私與條款協議
|
||||
RELOAD_APP: 重啟程式
|
||||
UPLOAD_SUCCEED: 上傳成功
|
||||
UPLOAD_FAILED: 上傳失敗
|
||||
@@ -34,6 +34,75 @@ GALLERY: 相簿
|
||||
PICBEDS_SETTINGS: 圖床設定
|
||||
PICGO_SETTINGS: PicGo設定
|
||||
PLUGIN_SETTINGS: 插件設定
|
||||
PICGO_CLOUD_TITLE: PicGo Cloud
|
||||
PICGO_CLOUD_ERROR_TITLE: 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_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: 微信支付
|
||||
@@ -304,39 +373,11 @@ 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: >
|
||||
|
||||
本軟體尊重並保護所有使用服務用戶的個人隱私權。為了給您提供更準確、更優質的服務,本軟體會按照本隱私權政策的規定使用和收集您的一些行為信息。您在同意本軟體服務使用協議之時,即視為您已經同意本隱私權政策全部內容。本隱私權政策屬於本軟體服務使用協議不可分割的一部分,如果不同意將無法使用。本協議會定期更新。
|
||||
|
||||
|
||||
1.適用範圍
|
||||
|
||||
|
||||
a)在您使用本軟體時,本軟體會記錄的您對本軟體的一些操作行為信息,包括但不限於您使用本軟體進行文件上傳的耗時、類型、數量等信息。
|
||||
|
||||
|
||||
2.信息的使用
|
||||
|
||||
|
||||
a)在獲得您的使用數據之後,本軟體會將其上傳至數據分析服務器,以便分析數據後,提供給您更好的服務。
|
||||
|
||||
|
||||
3.信息披露
|
||||
|
||||
|
||||
a)本軟體不會將您的信息披露給不受信任的第三方。
|
||||
|
||||
|
||||
b)根據法律的有關規定,或者行政或司法機構的要求,向第三方或者行政、司法機構披露;
|
||||
|
||||
|
||||
|
||||
|
||||
4.信息安全
|
||||
|
||||
|
||||
a)本軟體不會收集您的個人信息、密鑰信息等隱私信息,所收集的信息僅僅作為改善軟體、優化體驗、了解軟體日活等用途。
|
||||
PRIVACY: "使用前請閱讀並同意隱私政策 ${privacyUrl} 與服務條款 ${termsUrl},同意後方可使用。"
|
||||
PRIVACY_TIPS: 請同意隱私協議,否則無法上傳。
|
||||
QUIT: 退出
|
||||
|
||||
+3
-3
@@ -16,15 +16,15 @@ const macos = [
|
||||
|
||||
const linux = {
|
||||
AppImage: [
|
||||
{ label: '64 bit', arch: '-x64', ext: '.AppImage' },
|
||||
{ label: '64 bit', arch: '-amd64', ext: '.AppImage' },
|
||||
{ label: 'ARM64', arch: '-arm64', ext: '.AppImage' }
|
||||
],
|
||||
Deb: [
|
||||
{ label: '64 bit', arch: '-x64', ext: '.deb' },
|
||||
{ label: '64 bit', arch: '-amd64', ext: '.deb' },
|
||||
{ label: 'ARM64', arch: '-arm64', ext: '.deb' }
|
||||
],
|
||||
Snap: [
|
||||
{ label: '64 bit', arch: '-x64', ext: '.snap' }
|
||||
{ label: '64 bit', arch: '-amd64', ext: '.snap' }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @stylistic/indent */
|
||||
const yaml = require('js-yaml')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
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.')
|
||||
}
|
||||
+118
-147
@@ -2,8 +2,6 @@
|
||||
// upload version file to cos
|
||||
|
||||
require('dotenv').config()
|
||||
// const crypto = require('crypto')
|
||||
// const axios = require('axios').default
|
||||
const fs = require('fs')
|
||||
const pkg = require('../package.json')
|
||||
const configList = require('./config')
|
||||
@@ -12,15 +10,13 @@ const path = require('path')
|
||||
const distPath = path.join(__dirname, '../dist')
|
||||
const S3Client = require('@aws-sdk/client-s3').S3Client
|
||||
const Upload = require('@aws-sdk/lib-storage').Upload
|
||||
// const BUCKET = 'picgo-1251750343'
|
||||
// const COS_SECRET_ID = process.env.PICGO_ENV_COS_SECRET_ID
|
||||
// const COS_SECRET_KEY = process.env.PICGO_ENV_COS_SECRET_KEY
|
||||
const uploadToDev = process.argv.includes('--dev')
|
||||
|
||||
const S3_BUCKET = 'release'
|
||||
const S3_LEGACY_BUCKET = 'picgo'
|
||||
// const AREA = 'ap-chengdu'
|
||||
const VERSION = pkg.version
|
||||
const FILE_PATH = `${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
|
||||
@@ -49,155 +45,130 @@ const S3LegacyOptions = {
|
||||
region: 'auto'
|
||||
}
|
||||
|
||||
// https://cloud.tencent.com/document/product/436/7778#signature
|
||||
// /**
|
||||
// * @param {string} fileName
|
||||
// * @returns
|
||||
// */
|
||||
// const generateSignature = (fileName, folder = FILE_PATH) => {
|
||||
// const secretKey = COS_SECRET_ID
|
||||
// // const area = AREA
|
||||
// const bucket = BUCKET
|
||||
// const path = folder
|
||||
// const today = Math.floor(new Date().getTime() / 1000)
|
||||
// const tomorrow = today + 86400
|
||||
// const signTime = `${today};${tomorrow}`
|
||||
// const signKey = crypto.createHmac('sha1', secretKey).update(signTime).digest('hex')
|
||||
// const httpString = `put\n/${path}${fileName}\n\nhost=${bucket}.cos.accelerate.myqcloud.com\n`
|
||||
// const sha1edHttpString = crypto.createHash('sha1').update(httpString).digest('hex')
|
||||
// const stringToSign = `sha1\n${signTime}\n${sha1edHttpString}\n`
|
||||
// const signature = crypto.createHmac('sha1', signKey).update(stringToSign).digest('hex')
|
||||
// return {
|
||||
// signature,
|
||||
// signTime
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// *
|
||||
// * @param {string} fileName
|
||||
// * @param {Buffer} fileBuffer
|
||||
// * @param {{ signature: string, signTime: string }} signature
|
||||
// * @returns
|
||||
// */
|
||||
// const getReqOptions = (fileName, fileBuffer, signature, folder = FILE_PATH) => {
|
||||
// return {
|
||||
// method: 'PUT',
|
||||
// url: `http://${BUCKET}.cos.accelerate.myqcloud.com/${encodeURI(folder)}${encodeURI(fileName)}`,
|
||||
// headers: {
|
||||
// Host: `${BUCKET}.cos.accelerate.myqcloud.com`,
|
||||
// Authorization: `q-sign-algorithm=sha1&q-ak=${COS_SECRET_KEY}&q-sign-time=${signature.signTime}&q-key-time=${signature.signTime}&q-header-list=host&q-url-param-list=&q-signature=${signature.signature}`,
|
||||
// contentType: mime.lookup(fileName),
|
||||
// useAgent: `PicGo;${pkg.version};null;null`
|
||||
// },
|
||||
// maxContentLength: Infinity,
|
||||
// maxBodyLength: Infinity,
|
||||
// data: fileBuffer,
|
||||
// resolveWithFullResponse: true
|
||||
// }
|
||||
// }
|
||||
/**
|
||||
* 检查是否使用 --all 参数(上传所有平台)
|
||||
*/
|
||||
function shouldUploadAll() {
|
||||
return process.argv.includes('--all')
|
||||
}
|
||||
|
||||
/**
|
||||
* a backup for version file
|
||||
* 获取要上传的配置列表
|
||||
*/
|
||||
// const uploadVersionFile = async () => {
|
||||
// try {
|
||||
// const platform = process.platform
|
||||
// if (configList[platform]) {
|
||||
// let versionFileHasUploaded = false
|
||||
// for (const [, config] of configList[platform].entries()) {
|
||||
// const versionFilePath = path.join(distPath, config['version-file'])
|
||||
// let versionFileName = config['version-file']
|
||||
// if (VERSION.toLocaleLowerCase().includes('beta')) {
|
||||
// versionFileName = versionFileName.replace('.yml', '.beta.yml')
|
||||
// }
|
||||
// // upload version file
|
||||
// if (!versionFileHasUploaded) {
|
||||
// const signature = generateSignature(versionFileName, '')
|
||||
// const reqOptions = getReqOptions(versionFileName, fs.readFileSync(versionFilePath), signature, '')
|
||||
// console.log('[PicGo Version File] Uploading...', versionFileName)
|
||||
// await axios.request(reqOptions)
|
||||
// versionFileHasUploaded = true
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// console.warn('platform not supported!', platform)
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.error(e)
|
||||
// }
|
||||
// }
|
||||
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 platform = process.platform
|
||||
if (configList[platform]) {
|
||||
const uploadedVersionFiles = new Set()
|
||||
for (const [index, config] of configList[platform].entries()) {
|
||||
const fileName = `${config.appNameWithPrefix}-${VERSION}-${config.arch}.${config.ext}`
|
||||
const filePath = path.join(distPath, fileName)
|
||||
const versionFilePath = path.join(distPath, config['version-file'])
|
||||
let versionFileName = config['version-file']
|
||||
if (VERSION.toLocaleLowerCase().includes('beta')) {
|
||||
versionFileName = versionFileName.replace('.yml', '.beta.yml')
|
||||
}
|
||||
console.log('[PicGo Dist] Preparing to upload', fileName)
|
||||
const client = new S3Client(S3Options)
|
||||
if (fs.existsSync(filePath)) {
|
||||
const uploadDistToS3 = new Upload({
|
||||
client,
|
||||
params: {
|
||||
Bucket: S3_BUCKET,
|
||||
Key: `${FILE_PATH}${fileName}`,
|
||||
Body: fs.createReadStream(filePath),
|
||||
ContentType: 'application/octet-stream'
|
||||
}
|
||||
})
|
||||
// upload dist file
|
||||
console.log('[PicGo Dist] Uploading...', fileName, `${index + 1}/${configList[platform].length}`)
|
||||
uploadDistToS3.on('httpUploadProgress', progress => {
|
||||
console.log(`[PicGo Dist] Uploading... ${progress.loaded}/${progress.total}`)
|
||||
})
|
||||
await uploadDistToS3.done()
|
||||
} else {
|
||||
console.warn('[PicGo Dist] File not found:', fileName)
|
||||
}
|
||||
const configs = getUploadConfigs()
|
||||
|
||||
// upload version file
|
||||
if (!uploadedVersionFiles.has(versionFileName) && fs.existsSync(versionFilePath)) {
|
||||
const uploadVersionFileToS3 = new Upload({
|
||||
client,
|
||||
params: {
|
||||
Bucket: S3_BUCKET,
|
||||
Key: `${versionFileName}`,
|
||||
Body: fs.createReadStream(versionFilePath),
|
||||
ContentType: mime.lookup(versionFileName)
|
||||
}
|
||||
})
|
||||
// upload to legacy bucket as well
|
||||
// will be deprecated in 2.5.0
|
||||
const legacyClient = new S3Client(S3LegacyOptions)
|
||||
const uploadVersionFileToLegacyS3 = new Upload({
|
||||
client: legacyClient,
|
||||
params: {
|
||||
Bucket: S3_LEGACY_BUCKET,
|
||||
Key: `${versionFileName}`,
|
||||
Body: fs.createReadStream(versionFilePath),
|
||||
ContentType: mime.lookup(versionFileName)
|
||||
}
|
||||
})
|
||||
console.log('[PicGo Version File] Uploading...', versionFileName)
|
||||
await uploadVersionFileToS3.done()
|
||||
await uploadVersionFileToLegacyS3.done()
|
||||
uploadedVersionFiles.add(versionFileName)
|
||||
console.log('[PicGo Version File] Upload successfully')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('platform not supported!', platform)
|
||||
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] 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 (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 {
|
||||
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(e)
|
||||
console.error('[PicGo] ❌ Upload error:', e)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
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'
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,309 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import fs from 'fs-extra'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
type ServerConfig = {
|
||||
port: number | string
|
||||
host: string
|
||||
enable: boolean
|
||||
}
|
||||
|
||||
type HonoContextLike = {
|
||||
req: {
|
||||
raw: Request
|
||||
formData: () => Promise<FormData>
|
||||
}
|
||||
json: (data: unknown, status?: number) => Response
|
||||
}
|
||||
|
||||
type UploadHandler = (c: HonoContextLike) => Promise<Response>
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
const createJsonContext = (req: Request): HonoContextLike => {
|
||||
return {
|
||||
req: {
|
||||
raw: req,
|
||||
formData: async () => new FormData()
|
||||
},
|
||||
json: (data: unknown, status: number = 200) => {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status,
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const readJson = async (res: Response): Promise<any> => {
|
||||
const text = await res.text()
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
let serverConfig: ServerConfig | undefined
|
||||
let registeredUploadHandler: UploadHandler | undefined
|
||||
let formImageDir: string
|
||||
|
||||
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 registerPostMock = vi.fn((routePath: string, handler: unknown, isInternal?: boolean) => {
|
||||
if (routePath === '/upload' && typeof handler === 'function') {
|
||||
registeredUploadHandler = handler as unknown as UploadHandler
|
||||
}
|
||||
return { routePath, isInternal }
|
||||
})
|
||||
|
||||
const listenMock = vi.fn()
|
||||
const shutdownMock = vi.fn()
|
||||
|
||||
const loggerMock = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn()
|
||||
}
|
||||
|
||||
const getAvailableWindowMock = vi.fn()
|
||||
const uploadClipboardFilesMock = vi.fn()
|
||||
const uploadSelectedFilesMock = vi.fn()
|
||||
|
||||
const dbPathDirMock = vi.fn(() => path.join(os.tmpdir(), 'picgo-gui-store'))
|
||||
const getFormImageFolderPathMock = vi.fn(() => formImageDir)
|
||||
|
||||
const cleanupFormUploaderFilesMock = vi.fn((fileInfoList?: unknown) => {
|
||||
if (!Array.isArray(fileInfoList)) return
|
||||
for (const item of fileInfoList) {
|
||||
if (typeof item === 'string') {
|
||||
try {
|
||||
fs.removeSync(item)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@core/picgo', () => {
|
||||
return {
|
||||
default: {
|
||||
getConfig: getConfigMock,
|
||||
saveConfig: saveConfigMock,
|
||||
server: {
|
||||
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 {
|
||||
uploadClipboardFiles: uploadClipboardFilesMock,
|
||||
uploadSelectedFiles: uploadSelectedFilesMock
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('apis/core/datastore/dbChecker', () => {
|
||||
return {
|
||||
dbPathDir: dbPathDirMock,
|
||||
getFormImageFolderPath: getFormImageFolderPathMock
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('~/main/utils/cleanupFormUploaderFiles', () => {
|
||||
return {
|
||||
cleanupFormUploaderFiles: cleanupFormUploaderFilesMock
|
||||
}
|
||||
})
|
||||
|
||||
describe('main/server (GUI adapter to picgo-core)', () => {
|
||||
beforeEach(async () => {
|
||||
serverConfig = undefined
|
||||
registeredUploadHandler = undefined
|
||||
formImageDir = await fs.mkdtemp(path.join(os.tmpdir(), 'picgo-gui-form-'))
|
||||
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.remove(formImageDir)
|
||||
})
|
||||
|
||||
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 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(registerPostMock).not.toHaveBeenCalled()
|
||||
expect(listenMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('registers internal /upload override 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()
|
||||
|
||||
expect(registerPostMock).toHaveBeenCalledTimes(1)
|
||||
expect(registerPostMock).toHaveBeenCalledWith('/upload', expect.any(Function), true)
|
||||
expect(listenMock).toHaveBeenCalledWith(36677, '127.0.0.1')
|
||||
|
||||
server.shutdown()
|
||||
expect(shutdownMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('implements GUI-compatible /upload JSON semantics with core-style status codes', async () => {
|
||||
serverConfig = { port: 36677, host: '127.0.0.1', enable: true }
|
||||
uploadClipboardFilesMock.mockResolvedValue('https://a.example/clipboard.png')
|
||||
uploadSelectedFilesMock.mockResolvedValue(['https://a.example/a.png'])
|
||||
getAvailableWindowMock.mockReturnValue({ webContents: {} })
|
||||
|
||||
const mod = await import('../../main/server')
|
||||
const server = mod.default
|
||||
server.startup()
|
||||
|
||||
expect(registeredUploadHandler).toBeDefined()
|
||||
const handler = registeredUploadHandler!
|
||||
|
||||
const resEmpty = await handler(createJsonContext(new Request('http://127.0.0.1/upload', { method: 'POST' })))
|
||||
expect(resEmpty.status).toBe(200)
|
||||
expect(await readJson(resEmpty)).toEqual({ success: true, result: ['https://a.example/clipboard.png'] })
|
||||
|
||||
const resObj = await handler(createJsonContext(new Request('http://127.0.0.1/upload', { method: 'POST', body: '{}' })))
|
||||
expect(resObj.status).toBe(200)
|
||||
expect(await readJson(resObj)).toEqual({ success: true, result: ['https://a.example/clipboard.png'] })
|
||||
|
||||
const resEmptyList = await handler(createJsonContext(new Request('http://127.0.0.1/upload', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ list: [] })
|
||||
})))
|
||||
expect(resEmptyList.status).toBe(200)
|
||||
expect(await readJson(resEmptyList)).toEqual({ success: true, result: ['https://a.example/clipboard.png'] })
|
||||
|
||||
const resList = await handler(createJsonContext(new Request('http://127.0.0.1/upload', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ list: ['/a.png'] })
|
||||
})))
|
||||
expect(resList.status).toBe(200)
|
||||
expect(await readJson(resList)).toEqual({ success: true, result: ['https://a.example/a.png'] })
|
||||
|
||||
const resInvalid = await handler(createJsonContext(new Request('http://127.0.0.1/upload', { method: 'POST', body: '{' })))
|
||||
expect(resInvalid.status).toBe(400)
|
||||
expect(await readJson(resInvalid)).toMatchObject({ success: false })
|
||||
})
|
||||
|
||||
it('writes multipart files to fixed temp folder and always cleans up (success/failure)', async () => {
|
||||
serverConfig = { port: 36677, host: '127.0.0.1', enable: true }
|
||||
getAvailableWindowMock.mockReturnValue({ webContents: {} })
|
||||
|
||||
const mod = await import('../../main/server')
|
||||
const server = mod.default
|
||||
server.startup()
|
||||
|
||||
const handler = registeredUploadHandler!
|
||||
|
||||
const makeMultipartContext = async (): Promise<{ ctx: HonoContextLike; expectedPath: string }> => {
|
||||
const fd = new FormData()
|
||||
fd.append('files', new Blob([Buffer.from('hello')], { type: 'image/png' }), 'a.png')
|
||||
|
||||
const req = new Request('http://127.0.0.1/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
const expectedPath = path.join(formImageDir, 'a.png')
|
||||
return {
|
||||
ctx: {
|
||||
req: {
|
||||
raw: req,
|
||||
formData: async () => fd
|
||||
},
|
||||
json: (data: unknown, status: number = 200) => new Response(JSON.stringify(data), { status })
|
||||
},
|
||||
expectedPath
|
||||
}
|
||||
}
|
||||
|
||||
// success case
|
||||
uploadSelectedFilesMock.mockImplementation(async (_webContents: unknown, list: Array<{ path: string }>) => {
|
||||
for (const item of list) {
|
||||
expect(await fs.pathExists(item.path)).toBe(true)
|
||||
}
|
||||
return ['https://a.example/form.png']
|
||||
})
|
||||
|
||||
const { ctx: ctxSuccess, expectedPath: pathSuccess } = await makeMultipartContext()
|
||||
const resSuccess = await handler(ctxSuccess)
|
||||
expect(resSuccess.status).toBe(200)
|
||||
expect(await readJson(resSuccess)).toEqual({ success: true, result: ['https://a.example/form.png'] })
|
||||
expect(await fs.pathExists(pathSuccess)).toBe(false)
|
||||
|
||||
// failure case still cleans up
|
||||
uploadSelectedFilesMock.mockRejectedValueOnce(new Error('fail'))
|
||||
|
||||
const { ctx: ctxFail, expectedPath: pathFail } = await makeMultipartContext()
|
||||
const resFail = await handler(ctxFail)
|
||||
expect(resFail.status).toBe(500)
|
||||
expect(await readJson(resFail)).toMatchObject({ success: false })
|
||||
expect(await fs.pathExists(pathFail)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp } from 'vue'
|
||||
import type { IConfig } from 'picgo'
|
||||
import { getConfig, getPicBeds } from '@/utils/dataSender'
|
||||
import { store, storeKey, type IStore } from '@/store'
|
||||
|
||||
vi.mock('@/utils/dataSender', () => {
|
||||
return {
|
||||
getConfig: vi.fn(),
|
||||
getPicBeds: vi.fn(),
|
||||
saveConfig: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
const buildStore = (): IStore => {
|
||||
const app = createApp({})
|
||||
store.install(app)
|
||||
return app._context.provides[storeKey as symbol] as IStore
|
||||
}
|
||||
|
||||
describe('renderer/store appConfig', () => {
|
||||
const getConfigMock = vi.mocked(getConfig)
|
||||
const getPicBedsMock = vi.mocked(getPicBeds)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('refreshAppConfig updates appConfig and defaultPicBed', async () => {
|
||||
const config: IConfig = {
|
||||
picBed: {
|
||||
uploader: 'github',
|
||||
current: 'smms'
|
||||
},
|
||||
picgoPlugins: {}
|
||||
}
|
||||
getConfigMock.mockResolvedValue(config)
|
||||
|
||||
const storeInstance = buildStore()
|
||||
await storeInstance.refreshAppConfig()
|
||||
|
||||
expect(storeInstance.state.appConfig).toStrictEqual(config)
|
||||
expect(storeInstance.state.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)
|
||||
|
||||
const storeInstance = buildStore()
|
||||
await storeInstance.refreshPicBeds()
|
||||
|
||||
expect(storeInstance.state.picBeds).toEqual(picBeds)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { saveConfig } from '@/utils/dataSender'
|
||||
import { PICGO_SAVE_CONFIG } from '#/events/constants'
|
||||
import { ipcRenderer } from 'electron'
|
||||
|
||||
vi.mock('electron', () => {
|
||||
return {
|
||||
ipcRenderer: {
|
||||
invoke: vi.fn(),
|
||||
on: vi.fn(),
|
||||
once: vi.fn(),
|
||||
send: vi.fn(),
|
||||
removeListener: vi.fn()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('renderer/utils/dataSender', () => {
|
||||
const ipcRendererMock = vi.mocked(ipcRenderer)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
ipcRendererMock.invoke.mockResolvedValue(true)
|
||||
})
|
||||
|
||||
it('invokes save config IPC after saveConfig', async () => {
|
||||
await saveConfig('settings.language', 'en')
|
||||
|
||||
expect(ipcRendererMock.invoke).toHaveBeenCalledWith(PICGO_SAVE_CONFIG, {
|
||||
'settings.language': 'en'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -66,7 +66,7 @@ export function createContextMenu () {
|
||||
}
|
||||
},
|
||||
{
|
||||
label: T('PRIVACY_AGREEMENT'),
|
||||
label: T('PRIVACY_TERMS_AGREEMENT'),
|
||||
click () {
|
||||
privacyManager.show(false)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import path from 'path'
|
||||
import { privacyManager } from '~/main/utils/privacyManager'
|
||||
import writeFile from 'write-file-atomic'
|
||||
import { CLIPBOARD_IMAGE_FOLDER } from '~/universal/utils/static'
|
||||
import { cleanupFormUploaderFiles } from '~/main/utils/cleanupFormUploaderFiles'
|
||||
import { IpcMainEvent } from 'electron/main'
|
||||
import { dataReportManager } from '~/main/utils/dataReport'
|
||||
|
||||
@@ -161,7 +160,6 @@ class Uploader {
|
||||
return false
|
||||
} finally {
|
||||
ipcMain.removeAllListeners(GET_RENAME_FILE_NAME)
|
||||
cleanupFormUploaderFiles(img)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { CREATE_APP_MENU } from '@core/bus/constants'
|
||||
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
|
||||
import { app } from 'electron'
|
||||
import { T } from '~/main/i18n'
|
||||
import { isLinux } from '~/universal/utils/common'
|
||||
import { isLinux, isWindows } from '~/universal/utils/common'
|
||||
import { getStaticPath } from '#/utils/staticPath'
|
||||
import picgo from '@core/picgo'
|
||||
// import { URLSearchParams } from 'url'
|
||||
@@ -37,7 +37,7 @@ const handleWindowParams = (windowURL: string) => {
|
||||
}
|
||||
|
||||
export const isWindowShouldShowOnStartup = (currentWindow: IWindowList) => {
|
||||
const startupMode = picgo.getConfig<IStartupMode | undefined>('settings.startupMode') || (isLinux ? IStartupMode.SHOW_MINI_WINDOW : IStartupMode.HIDE)
|
||||
const startupMode = picgo.getConfig<IStartupMode | undefined>('settings.startupMode') || (isLinux ? IStartupMode.SHOW_MINI_WINDOW : isWindows ? IStartupMode.SHOW_MAIN_WINDOW : IStartupMode.HIDE)
|
||||
switch (currentWindow) {
|
||||
case IWindowList.MINI_WINDOW: {
|
||||
return startupMode === IStartupMode.SHOW_MINI_WINDOW
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { dbChecker, dbPathChecker } from 'apis/core/datastore/dbChecker'
|
||||
import { shell } from 'electron'
|
||||
import pkg from 'root/package.json'
|
||||
import { PicGo } from 'picgo'
|
||||
|
||||
@@ -15,6 +16,10 @@ picgo.saveConfig({
|
||||
global.PICGO_GUI_VERSION = pkg.version
|
||||
picgo.GUI_VERSION = global.PICGO_GUI_VERSION
|
||||
|
||||
picgo.openUrl = (url: string) => {
|
||||
return shell.openExternal(url)
|
||||
}
|
||||
|
||||
// const originPicGoSaveConfig = picgo.saveConfig.bind(picgo)
|
||||
|
||||
// function flushDB () {
|
||||
|
||||
@@ -38,6 +38,7 @@ import { GalleryDB } from 'apis/core/datastore'
|
||||
import { IObject, IFilter } from '@picgo/store/dist/types'
|
||||
import pasteTemplate from '../utils/pasteTemplate'
|
||||
import { i18nManager, T } from '~/main/i18n'
|
||||
import { notifyAppConfigUpdated } from '~/main/utils/appConfigNotifier'
|
||||
import { rpcServer } from './rpc'
|
||||
|
||||
const STORE_PATH = path.dirname(dbPathChecker())
|
||||
@@ -259,6 +260,7 @@ const handleRemoveFiles = () => {
|
||||
const handlePicGoSaveConfig = () => {
|
||||
ipcMain.handle(PICGO_SAVE_CONFIG, (_event, data: IObj) => {
|
||||
picgo.saveConfig(data)
|
||||
notifyAppConfigUpdated()
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const buildMiniPageMenu = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
label: T('PRIVACY_AGREEMENT'),
|
||||
label: T('PRIVACY_TERMS_AGREEMENT'),
|
||||
click () {
|
||||
privacyManager.show(false)
|
||||
}
|
||||
@@ -110,7 +110,7 @@ const buildMainPageMenu = (win: BrowserWindow) => {
|
||||
}
|
||||
},
|
||||
{
|
||||
label: T('PRIVACY_AGREEMENT'),
|
||||
label: T('PRIVACY_TERMS_AGREEMENT'),
|
||||
click () {
|
||||
privacyManager.show(false)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Menu } from 'electron'
|
||||
import getPicBeds from '~/main/utils/getPicBeds'
|
||||
import picgo from '@core/picgo'
|
||||
import { T } from '~/main/i18n'
|
||||
import { changeCurrentUploader } from '~/main/utils/handleUploaderConfig'
|
||||
|
||||
export const buildPicBedListMenu = () => {
|
||||
const picBeds = getPicBeds()
|
||||
@@ -34,7 +33,11 @@ export const buildPicBedListMenu = () => {
|
||||
type: 'checkbox',
|
||||
checked: config._id === defaultId && (item.type === currentPicBed),
|
||||
click: function () {
|
||||
changeCurrentUploader(item.type, config, config._id)
|
||||
try {
|
||||
picgo.uploaderConfig.use(item.type, config._configName)
|
||||
} catch (e) {
|
||||
picgo.log.error(e instanceof Error ? e : new Error(String(e)))
|
||||
}
|
||||
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
|
||||
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ipcMain, IpcMainEvent } from 'electron'
|
||||
import { ipcMain, IpcMainEvent, IpcMainInvokeEvent } from 'electron'
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
import { RPC_ACTIONS } from '#/events/constants'
|
||||
import { configRouter } from './routes/config'
|
||||
@@ -6,11 +6,13 @@ import { versionRouter } from './routes/version'
|
||||
import { toolboxRouter } from './routes/toolbox'
|
||||
import { systemRouter } from './routes/system'
|
||||
import { galleryToolboxRouter } from './routes/galleryToolbox'
|
||||
import { cloudRouter } from './routes/cloud'
|
||||
import { fail, isIRPCResult, ok } from './utils'
|
||||
|
||||
class RPCServer implements IRPCServer {
|
||||
private routes: IRPCRoutes = new Map()
|
||||
|
||||
private rpcEventHandler = async (event: IpcMainEvent, action: IRPCActionType, args: any[], callbackId: string) => {
|
||||
private rpcEventHandler = async (event: IpcMainEvent, action: IRPCActionType, args: any[], callbackId?: string) => {
|
||||
try {
|
||||
const handler = this.routes.get(action)
|
||||
if (!handler) {
|
||||
@@ -23,11 +25,25 @@ class RPCServer implements IRPCServer {
|
||||
}
|
||||
}
|
||||
|
||||
private rpcInvokeHandler = async (_event: IpcMainInvokeEvent, action: IRPCActionType, args: any[] = []) => {
|
||||
const handler = this.routes.get(action)
|
||||
if (!handler) {
|
||||
return fail(new Error(`RPC action not supported: ${action}`))
|
||||
}
|
||||
try {
|
||||
const res = await handler(args, _event)
|
||||
// For invoke-based RPC, normalize the return value to IRPCResult.
|
||||
return isIRPCResult(res) ? res : ok(res)
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* if sendback data is null, then it means that the action is not supported or error occurs
|
||||
* if there is no callbackId, then do not send back
|
||||
*/
|
||||
private sendBack (event: IpcMainEvent, action: IRPCActionType, data: any, callbackId: string) {
|
||||
private sendBack (event: IpcMainEvent, action: IRPCActionType, data: any, callbackId?: string) {
|
||||
if (callbackId) {
|
||||
event.sender.send(RPC_ACTIONS, data, action, callbackId)
|
||||
}
|
||||
@@ -35,6 +51,7 @@ class RPCServer implements IRPCServer {
|
||||
|
||||
start () {
|
||||
ipcMain.on(RPC_ACTIONS, this.rpcEventHandler)
|
||||
ipcMain.handle(RPC_ACTIONS, this.rpcInvokeHandler)
|
||||
}
|
||||
|
||||
use (routes: IRPCRoutes) {
|
||||
@@ -45,6 +62,7 @@ class RPCServer implements IRPCServer {
|
||||
|
||||
stop () {
|
||||
ipcMain.off(RPC_ACTIONS, this.rpcEventHandler)
|
||||
ipcMain.removeHandler(RPC_ACTIONS)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +73,7 @@ rpcServer.use(versionRouter.routes())
|
||||
rpcServer.use(toolboxRouter.routes())
|
||||
rpcServer.use(systemRouter.routes())
|
||||
rpcServer.use(galleryToolboxRouter.routes())
|
||||
rpcServer.use(cloudRouter.routes())
|
||||
|
||||
export {
|
||||
rpcServer
|
||||
|
||||
@@ -0,0 +1,559 @@
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
import { RPCRouter } from '../router'
|
||||
import picgo from '@core/picgo'
|
||||
import type { IPicGoCloudUserInfo } from '#/types/cloud'
|
||||
import { T } from '~/main/i18n'
|
||||
import { fail, ok } from '../utils'
|
||||
import GuiApi from 'apis/gui'
|
||||
import fs from 'fs-extra'
|
||||
import { parse } from 'comment-json'
|
||||
import { cloneDeep, isPlainObject, set, unset } from 'lodash'
|
||||
import path from 'path'
|
||||
import logger from 'apis/core/picgo/logger'
|
||||
import {
|
||||
ConfigSyncManager,
|
||||
ConflictType,
|
||||
E2EAskPinReason,
|
||||
EncryptionMethod,
|
||||
SyncStatus,
|
||||
type IDiffNode,
|
||||
type IConfig
|
||||
} from 'picgo'
|
||||
import {
|
||||
IPicGoCloudConfigSyncConflictChoice,
|
||||
IPicGoCloudConfigSyncRunStatus,
|
||||
IPicGoCloudConfigSyncSessionStatus,
|
||||
IPicGoCloudConfigSyncToastType,
|
||||
IPicGoCloudEncryptionMethod,
|
||||
type IPicGoCloudConfigSyncConflictItem,
|
||||
type IPicGoCloudConfigSyncResolution,
|
||||
type IPicGoCloudConfigSyncRunResult,
|
||||
type IPicGoCloudConfigSyncState
|
||||
} from '#/types/cloudConfigSync'
|
||||
|
||||
const cloudRouter = new RPCRouter()
|
||||
|
||||
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000
|
||||
const USER_ABORTED_CODE = 'PICGO_CLOUD_CONFIG_SYNC_ABORTED'
|
||||
|
||||
/**
|
||||
* Config sync session state MUST live in the main process (memory only) so the UI can re-hydrate
|
||||
* after window hide/show without losing an in-progress/conflict state.
|
||||
*/
|
||||
let configSyncSessionStatus: IPicGoCloudConfigSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.IDLE
|
||||
let configSyncConflictDiffTree: IDiffNode | null = null
|
||||
let configSyncConflictItems: IPicGoCloudConfigSyncConflictItem[] = []
|
||||
let configSyncManager: ConfigSyncManager | null = null
|
||||
|
||||
const clearConfigSyncSession = (): void => {
|
||||
configSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.IDLE
|
||||
configSyncConflictDiffTree = null
|
||||
configSyncConflictItems = []
|
||||
}
|
||||
|
||||
const logConfigSyncOutcome = (
|
||||
stage: 'sync' | 'applyResolvedConfig',
|
||||
status: SyncStatus,
|
||||
message?: string,
|
||||
meta: { conflictCount?: number } = {}
|
||||
): void => {
|
||||
const prefix = `[PicGo Cloud][config-sync][${stage}]`
|
||||
if (status === SyncStatus.SUCCESS) {
|
||||
logger.info(`${prefix} success`)
|
||||
return
|
||||
}
|
||||
|
||||
if (status === SyncStatus.CONFLICT) {
|
||||
const count = typeof meta.conflictCount === 'number' ? meta.conflictCount : 0
|
||||
logger.warn(`${prefix} conflict`, `count=${count}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (message === USER_ABORTED_CODE || message === 'Invalid PIN input') {
|
||||
logger.warn(`${prefix} aborted`)
|
||||
return
|
||||
}
|
||||
|
||||
logger.error(`${prefix} failed`, message || '')
|
||||
}
|
||||
|
||||
const getLocalEncryptionMethod = (): IPicGoCloudEncryptionMethod | undefined => {
|
||||
const value = picgo.getConfig<unknown>('settings.picgoCloud.encryptionMethod')
|
||||
if (
|
||||
value === IPicGoCloudEncryptionMethod.AUTO
|
||||
|| value === IPicGoCloudEncryptionMethod.SSE
|
||||
|| value === IPicGoCloudEncryptionMethod.E2EE
|
||||
) {
|
||||
return value
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const toSyncEncryptionMethod = (method?: IPicGoCloudEncryptionMethod): EncryptionMethod | undefined => {
|
||||
if (method === IPicGoCloudEncryptionMethod.AUTO) return EncryptionMethod.AUTO
|
||||
if (method === IPicGoCloudEncryptionMethod.SSE) return EncryptionMethod.SSE
|
||||
if (method === IPicGoCloudEncryptionMethod.E2EE) return EncryptionMethod.E2EE
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getSnapshotUpdatedAt = async (): Promise<string | undefined> => {
|
||||
try {
|
||||
const snapshotPath = path.join(picgo.baseDir, 'config.snapshot.json')
|
||||
if (!(await fs.pathExists(snapshotPath))) return undefined
|
||||
const content = await fs.readFile(snapshotPath, 'utf8')
|
||||
const parsed: unknown = parse(content)
|
||||
if (!isPlainObject(parsed)) return undefined
|
||||
const updatedAt = (parsed as { updatedAt?: unknown }).updatedAt
|
||||
return typeof updatedAt === 'string' && updatedAt ? updatedAt : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const buildConfigSyncState = async (): Promise<IPicGoCloudConfigSyncState> => {
|
||||
return {
|
||||
sessionStatus: configSyncSessionStatus,
|
||||
encryptionMethod: getLocalEncryptionMethod(),
|
||||
lastSyncedAt: await getSnapshotUpdatedAt(),
|
||||
conflicts: configSyncSessionStatus === IPicGoCloudConfigSyncSessionStatus.CONFLICT ? configSyncConflictItems : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const readLocalConfigWithComments = async (): Promise<IConfig> => {
|
||||
if (!(await fs.pathExists(picgo.configPath))) {
|
||||
return picgo.getConfig<IConfig>()
|
||||
}
|
||||
const content = await fs.readFile(picgo.configPath, 'utf8')
|
||||
const parsed: unknown = parse(content)
|
||||
if (!isPlainObject(parsed)) {
|
||||
throw new Error(T('PICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID'))
|
||||
}
|
||||
return parsed as IConfig
|
||||
}
|
||||
|
||||
const extractConflictItems = (diffTree: IDiffNode): IPicGoCloudConfigSyncConflictItem[] => {
|
||||
const items: IPicGoCloudConfigSyncConflictItem[] = []
|
||||
|
||||
const walk = (node: IDiffNode, pathSegments: string[]) => {
|
||||
const nextSegments = node.key === 'root' ? pathSegments : [...pathSegments, node.key]
|
||||
|
||||
if (node.status === ConflictType.CONFLICT) {
|
||||
// If the conflict is an object-level aggregation, surface leaf conflicts instead.
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(child => walk(child, nextSegments))
|
||||
return
|
||||
}
|
||||
|
||||
items.push({
|
||||
path: nextSegments.join('.'),
|
||||
localValue: node.localValue,
|
||||
remoteValue: node.remoteValue
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(child => walk(child, nextSegments))
|
||||
}
|
||||
}
|
||||
|
||||
walk(diffTree, [])
|
||||
return items
|
||||
}
|
||||
|
||||
const localizeConfigSyncResult = (status: SyncStatus, message: string | undefined): { message: string, toastType: IPicGoCloudConfigSyncToastType } => {
|
||||
if (status === SyncStatus.SUCCESS) {
|
||||
return {
|
||||
message: T('PICGO_CLOUD_CONFIG_SYNC_SUCCESS'),
|
||||
toastType: IPicGoCloudConfigSyncToastType.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
if (status === SyncStatus.CONFLICT) {
|
||||
return {
|
||||
message: T('PICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED'),
|
||||
toastType: IPicGoCloudConfigSyncToastType.INFO
|
||||
}
|
||||
}
|
||||
|
||||
const raw = message || T('PICGO_CLOUD_CONFIG_SYNC_FAILED')
|
||||
|
||||
const isEncryptionSwitchCancelled = message === picgo.i18n.translate('CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED')
|
||||
if (raw === USER_ABORTED_CODE || raw === 'Invalid PIN input' || isEncryptionSwitchCancelled) {
|
||||
return {
|
||||
message: isEncryptionSwitchCancelled
|
||||
? T('PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED')
|
||||
: T('PICGO_CLOUD_CONFIG_SYNC_ABORTED'),
|
||||
toastType: IPicGoCloudConfigSyncToastType.WARNING
|
||||
}
|
||||
}
|
||||
|
||||
if (raw === 'Maximum retry attempts exceeded') {
|
||||
return {
|
||||
message: T('PICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY'),
|
||||
toastType: IPicGoCloudConfigSyncToastType.ERROR
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: T('PICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON', { reason: raw }),
|
||||
toastType: IPicGoCloudConfigSyncToastType.ERROR
|
||||
}
|
||||
}
|
||||
|
||||
const getEncryptionMethodLabel = (method: EncryptionMethod): string => {
|
||||
if (method === EncryptionMethod.E2EE) return T('PICGO_CLOUD_ENCRYPTION_MODE_E2E')
|
||||
return T('PICGO_CLOUD_ENCRYPTION_MODE_SERVER')
|
||||
}
|
||||
|
||||
const getConfigSyncManager = (): ConfigSyncManager => {
|
||||
if (configSyncManager) return configSyncManager
|
||||
|
||||
const guiApi = GuiApi.getInstance()
|
||||
configSyncManager = new ConfigSyncManager(picgo, {
|
||||
onAskPin: async (reason: E2EAskPinReason, retryCount: number) => {
|
||||
const inputOptions: IShowInputBoxOption = {
|
||||
title: (() => {
|
||||
if (reason === E2EAskPinReason.SETUP) return T('PICGO_CLOUD_E2E_PIN_SETUP_TITLE')
|
||||
if (reason === E2EAskPinReason.DECRYPT) return T('PICGO_CLOUD_E2E_PIN_DECRYPT_TITLE')
|
||||
return T('PICGO_CLOUD_E2E_PIN_RETRY_TITLE', { retryCount })
|
||||
})(),
|
||||
placeholder: T('PICGO_CLOUD_E2E_PIN_PLACEHOLDER'),
|
||||
inputType: 'password',
|
||||
width: 520,
|
||||
confirm: reason === E2EAskPinReason.SETUP
|
||||
? { placeholder: T('PICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER') }
|
||||
: undefined
|
||||
}
|
||||
|
||||
const value = await guiApi.showInputBox(inputOptions)
|
||||
if (!value) {
|
||||
// Throw a sentinel code so we can treat it as a user-aborted flow in the GUI.
|
||||
throw new Error(USER_ABORTED_CODE)
|
||||
}
|
||||
return value
|
||||
},
|
||||
onAskEncryptionSwitch: async ({ from, to }: { from: EncryptionMethod, to: EncryptionMethod }): Promise<boolean> => {
|
||||
const title = T('PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE')
|
||||
const message = T('PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY', {
|
||||
from: getEncryptionMethodLabel(from),
|
||||
to: getEncryptionMethodLabel(to)
|
||||
})
|
||||
const res = await guiApi.showMessageBox({
|
||||
title,
|
||||
message,
|
||||
type: 'warning',
|
||||
buttons: [
|
||||
T('PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM'),
|
||||
T('PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL')
|
||||
]
|
||||
})
|
||||
return res.result === 0
|
||||
}
|
||||
})
|
||||
|
||||
return configSyncManager
|
||||
}
|
||||
|
||||
const buildResolvedConfig = async (resolution: IPicGoCloudConfigSyncResolution): Promise<IConfig> => {
|
||||
const base = await readLocalConfigWithComments()
|
||||
|
||||
for (const item of configSyncConflictItems) {
|
||||
const choice = resolution[item.path]
|
||||
if (choice === IPicGoCloudConfigSyncConflictChoice.CLOUD) {
|
||||
if (item.remoteValue === undefined) {
|
||||
unset(base, item.path)
|
||||
} else {
|
||||
set(base, item.path, cloneDeep(item.remoteValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
const getUserInfo = async (): Promise<IPicGoCloudUserInfo | null> => {
|
||||
return await picgo.cloud.getUserInfo()
|
||||
}
|
||||
|
||||
const loginWithTimeout = async (): Promise<void> => {
|
||||
const loginPromise = picgo.cloud.login()
|
||||
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
picgo.cloud.disposeLoginFlow()
|
||||
reject(new Error(T('PICGO_CLOUD_LOGIN_TIMEOUT')))
|
||||
}, LOGIN_TIMEOUT_MS)
|
||||
})
|
||||
|
||||
try {
|
||||
await Promise.race([loginPromise, timeoutPromise])
|
||||
} finally {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
// Avoid unhandled rejection when timeout disposes the core login flow.
|
||||
loginPromise.catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
cloudRouter
|
||||
.add(IRPCActionType.PICGO_CLOUD_GET_USER_INFO, async () => {
|
||||
try {
|
||||
const userInfo = await getUserInfo()
|
||||
return ok(userInfo)
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
.add(IRPCActionType.PICGO_CLOUD_LOGIN, async () => {
|
||||
try {
|
||||
await loginWithTimeout()
|
||||
const userInfo = await getUserInfo()
|
||||
if (!userInfo) {
|
||||
return fail(T('PICGO_CLOUD_LOGIN_FAILED'))
|
||||
}
|
||||
return ok(userInfo)
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
.add(IRPCActionType.PICGO_CLOUD_LOGOUT, async () => {
|
||||
try {
|
||||
picgo.cloud.logout()
|
||||
clearConfigSyncSession()
|
||||
return ok(true)
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
.add(IRPCActionType.PICGO_CLOUD_DISPOSE_LOGIN_FLOW, async () => {
|
||||
try {
|
||||
picgo.cloud.disposeLoginFlow()
|
||||
return ok(true)
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
.add(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_GET_STATE, async () => {
|
||||
try {
|
||||
return ok(await buildConfigSyncState())
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
.add(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_SET_E2E_PREFERENCE, async (args) => {
|
||||
try {
|
||||
const [mode] = args as [IPicGoCloudEncryptionMethod]
|
||||
if (mode === IPicGoCloudEncryptionMethod.AUTO) {
|
||||
picgo.saveConfig({
|
||||
'settings.picgoCloud.encryptionMethod': IPicGoCloudEncryptionMethod.AUTO
|
||||
})
|
||||
} else if (mode === IPicGoCloudEncryptionMethod.SSE) {
|
||||
picgo.saveConfig({
|
||||
'settings.picgoCloud.encryptionMethod': IPicGoCloudEncryptionMethod.SSE
|
||||
})
|
||||
} else {
|
||||
picgo.saveConfig({
|
||||
'settings.picgoCloud.encryptionMethod': IPicGoCloudEncryptionMethod.E2EE
|
||||
})
|
||||
}
|
||||
return ok(getLocalEncryptionMethod())
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
.add(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_ABORT, async () => {
|
||||
try {
|
||||
clearConfigSyncSession()
|
||||
return ok(await buildConfigSyncState())
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
.add(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_START, async () => {
|
||||
const fallbackState = await buildConfigSyncState()
|
||||
|
||||
if (configSyncSessionStatus === IPicGoCloudConfigSyncSessionStatus.SYNCING) {
|
||||
logger.info('[PicGo Cloud][config-sync][sync] already in progress')
|
||||
const runRes: IPicGoCloudConfigSyncRunResult = {
|
||||
status: IPicGoCloudConfigSyncRunStatus.FAILED,
|
||||
message: T('PICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS'),
|
||||
toastType: IPicGoCloudConfigSyncToastType.INFO,
|
||||
state: fallbackState
|
||||
}
|
||||
return ok(runRes)
|
||||
}
|
||||
|
||||
if (configSyncSessionStatus === IPicGoCloudConfigSyncSessionStatus.CONFLICT) {
|
||||
logger.info('[PicGo Cloud][config-sync][sync] pending conflict session')
|
||||
const runRes: IPicGoCloudConfigSyncRunResult = {
|
||||
status: IPicGoCloudConfigSyncRunStatus.CONFLICT,
|
||||
message: T('PICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING'),
|
||||
toastType: IPicGoCloudConfigSyncToastType.INFO,
|
||||
state: fallbackState
|
||||
}
|
||||
return ok(runRes)
|
||||
}
|
||||
|
||||
configSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.SYNCING
|
||||
|
||||
try {
|
||||
const userInfo = await getUserInfo()
|
||||
if (!userInfo) {
|
||||
logger.warn('[PicGo Cloud][config-sync][sync] login expired')
|
||||
clearConfigSyncSession()
|
||||
const runRes: IPicGoCloudConfigSyncRunResult = {
|
||||
status: IPicGoCloudConfigSyncRunStatus.FAILED,
|
||||
message: T('PICGO_CLOUD_LOGIN_EXPIRED'),
|
||||
toastType: IPicGoCloudConfigSyncToastType.WARNING,
|
||||
authInvalidated: true,
|
||||
state: await buildConfigSyncState()
|
||||
}
|
||||
return ok(runRes)
|
||||
}
|
||||
|
||||
const manager = getConfigSyncManager()
|
||||
const encryptionMethod = toSyncEncryptionMethod(getLocalEncryptionMethod())
|
||||
const res = encryptionMethod
|
||||
? await manager.sync({ encryptionMethod })
|
||||
: await manager.sync()
|
||||
|
||||
if (res.status === SyncStatus.CONFLICT && res.diffTree) {
|
||||
configSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.CONFLICT
|
||||
configSyncConflictDiffTree = res.diffTree
|
||||
configSyncConflictItems = extractConflictItems(res.diffTree)
|
||||
logConfigSyncOutcome('sync', res.status, res.message, { conflictCount: configSyncConflictItems.length })
|
||||
} else {
|
||||
clearConfigSyncSession()
|
||||
logConfigSyncOutcome('sync', res.status, res.message)
|
||||
}
|
||||
|
||||
const localized = localizeConfigSyncResult(res.status, res.message)
|
||||
const runStatus = res.status === SyncStatus.SUCCESS
|
||||
? IPicGoCloudConfigSyncRunStatus.SUCCESS
|
||||
: res.status === SyncStatus.CONFLICT
|
||||
? IPicGoCloudConfigSyncRunStatus.CONFLICT
|
||||
: IPicGoCloudConfigSyncRunStatus.FAILED
|
||||
|
||||
const runRes: IPicGoCloudConfigSyncRunResult = {
|
||||
status: runStatus,
|
||||
message: localized.message,
|
||||
toastType: localized.toastType,
|
||||
shouldShowRestartPrompt: res.status === SyncStatus.SUCCESS,
|
||||
state: await buildConfigSyncState()
|
||||
}
|
||||
return ok(runRes)
|
||||
} catch (e) {
|
||||
logger.error('[PicGo Cloud][config-sync][sync] error', e)
|
||||
clearConfigSyncSession()
|
||||
const localized = localizeConfigSyncResult(SyncStatus.FAILED, e instanceof Error ? e.message : String(e))
|
||||
const runRes: IPicGoCloudConfigSyncRunResult = {
|
||||
status: IPicGoCloudConfigSyncRunStatus.FAILED,
|
||||
message: localized.message,
|
||||
toastType: localized.toastType,
|
||||
state: await buildConfigSyncState()
|
||||
}
|
||||
return ok(runRes)
|
||||
}
|
||||
})
|
||||
.add(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_APPLY_RESOLUTION, async (args) => {
|
||||
try {
|
||||
const [resolution] = args as [IPicGoCloudConfigSyncResolution]
|
||||
|
||||
if (configSyncSessionStatus !== IPicGoCloudConfigSyncSessionStatus.CONFLICT || !configSyncConflictDiffTree) {
|
||||
logger.warn('[PicGo Cloud][config-sync][applyResolvedConfig] no conflict session')
|
||||
const runRes: IPicGoCloudConfigSyncRunResult = {
|
||||
status: IPicGoCloudConfigSyncRunStatus.FAILED,
|
||||
message: T('PICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION'),
|
||||
toastType: IPicGoCloudConfigSyncToastType.ERROR,
|
||||
state: await buildConfigSyncState()
|
||||
}
|
||||
return ok(runRes)
|
||||
}
|
||||
|
||||
const expectedPaths = new Set(configSyncConflictItems.map(item => item.path))
|
||||
const providedPaths = new Set(Object.keys(resolution))
|
||||
for (const path of expectedPaths) {
|
||||
if (!providedPaths.has(path)) {
|
||||
logger.warn('[PicGo Cloud][config-sync][applyResolvedConfig] resolution incomplete')
|
||||
const runRes: IPicGoCloudConfigSyncRunResult = {
|
||||
status: IPicGoCloudConfigSyncRunStatus.FAILED,
|
||||
message: T('PICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE'),
|
||||
toastType: IPicGoCloudConfigSyncToastType.ERROR,
|
||||
state: await buildConfigSyncState()
|
||||
}
|
||||
return ok(runRes)
|
||||
}
|
||||
}
|
||||
|
||||
const userInfo = await getUserInfo()
|
||||
if (!userInfo) {
|
||||
logger.warn('[PicGo Cloud][config-sync][applyResolvedConfig] login expired')
|
||||
clearConfigSyncSession()
|
||||
const runRes: IPicGoCloudConfigSyncRunResult = {
|
||||
status: IPicGoCloudConfigSyncRunStatus.FAILED,
|
||||
message: T('PICGO_CLOUD_LOGIN_EXPIRED'),
|
||||
toastType: IPicGoCloudConfigSyncToastType.WARNING,
|
||||
authInvalidated: true,
|
||||
state: await buildConfigSyncState()
|
||||
}
|
||||
return ok(runRes)
|
||||
}
|
||||
|
||||
configSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.SYNCING
|
||||
|
||||
const resolvedConfig = await buildResolvedConfig(resolution)
|
||||
|
||||
const encryptionMethod = getLocalEncryptionMethod()
|
||||
const manager = getConfigSyncManager()
|
||||
const applyRes = await manager.applyResolvedConfig(
|
||||
resolvedConfig,
|
||||
encryptionMethod === IPicGoCloudEncryptionMethod.E2EE
|
||||
? { useE2E: true }
|
||||
: encryptionMethod === IPicGoCloudEncryptionMethod.SSE
|
||||
? { useE2E: false }
|
||||
: {}
|
||||
)
|
||||
|
||||
if (applyRes.status === SyncStatus.SUCCESS) {
|
||||
clearConfigSyncSession()
|
||||
} else {
|
||||
// Keep conflict session so the user can retry.
|
||||
configSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.CONFLICT
|
||||
}
|
||||
|
||||
logConfigSyncOutcome('applyResolvedConfig', applyRes.status, applyRes.message, { conflictCount: configSyncConflictItems.length })
|
||||
|
||||
const localized = localizeConfigSyncResult(applyRes.status, applyRes.message)
|
||||
const runStatus = applyRes.status === SyncStatus.SUCCESS
|
||||
? IPicGoCloudConfigSyncRunStatus.SUCCESS
|
||||
: applyRes.status === SyncStatus.CONFLICT
|
||||
? IPicGoCloudConfigSyncRunStatus.CONFLICT
|
||||
: IPicGoCloudConfigSyncRunStatus.FAILED
|
||||
|
||||
const runRes: IPicGoCloudConfigSyncRunResult = {
|
||||
status: runStatus,
|
||||
message: localized.message,
|
||||
toastType: localized.toastType,
|
||||
shouldShowRestartPrompt: applyRes.status === SyncStatus.SUCCESS,
|
||||
state: await buildConfigSyncState()
|
||||
}
|
||||
return ok(runRes)
|
||||
} catch (e) {
|
||||
logger.error('[PicGo Cloud][config-sync][applyResolvedConfig] error', e)
|
||||
// Keep the conflict session so user can retry from UI.
|
||||
configSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.CONFLICT
|
||||
const localized = localizeConfigSyncResult(SyncStatus.FAILED, e instanceof Error ? e.message : String(e))
|
||||
const runRes: IPicGoCloudConfigSyncRunResult = {
|
||||
status: IPicGoCloudConfigSyncRunStatus.FAILED,
|
||||
message: localized.message,
|
||||
toastType: localized.toastType,
|
||||
state: await buildConfigSyncState()
|
||||
}
|
||||
return ok(runRes)
|
||||
}
|
||||
})
|
||||
|
||||
export {
|
||||
cloudRouter
|
||||
}
|
||||
@@ -1,34 +1,97 @@
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
import { RPCRouter } from '../router'
|
||||
import { copyUploaderConfig, deleteUploaderConfig, getUploaderConfigList, selectUploaderConfig, updateUploaderConfig } from '~/main/utils/handleUploaderConfig'
|
||||
import picgo from '@core/picgo'
|
||||
import { T } from '~/main/i18n'
|
||||
import { fail, ok } from '../utils'
|
||||
import { notifyAppConfigUpdated } from '~/main/utils/appConfigNotifier'
|
||||
|
||||
const configRouter = new RPCRouter()
|
||||
|
||||
configRouter
|
||||
.add(IRPCActionType.GET_PICBED_CONFIG_LIST, async (args) => {
|
||||
const [type] = args as IGetUploaderConfigListArgs
|
||||
const config = getUploaderConfigList(type)
|
||||
return config
|
||||
try {
|
||||
const [type] = args as IGetUploaderConfigListArgs
|
||||
const configList = picgo.uploaderConfig.getConfigList(type)
|
||||
const activeConfig = picgo.uploaderConfig.getActiveConfig(type)
|
||||
return ok({
|
||||
configList,
|
||||
defaultId: activeConfig?._id ?? ''
|
||||
})
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
.add(IRPCActionType.DELETE_PICBED_CONFIG, async (args) => {
|
||||
const [type, id] = args as IDeleteUploaderConfigArgs
|
||||
const config = deleteUploaderConfig(type, id)
|
||||
return config
|
||||
try {
|
||||
const [type, configName] = args as IDeleteUploaderConfigArgs
|
||||
const existing = picgo.uploaderConfig.getConfigList(type)
|
||||
if (existing.length <= 1) {
|
||||
throw new Error(T('TIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST'))
|
||||
}
|
||||
picgo.uploaderConfig.remove(type, configName)
|
||||
const configList = picgo.uploaderConfig.getConfigList(type)
|
||||
const activeConfig = picgo.uploaderConfig.getActiveConfig(type)
|
||||
notifyAppConfigUpdated()
|
||||
return ok({
|
||||
configList,
|
||||
defaultId: activeConfig?._id ?? ''
|
||||
})
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
.add(IRPCActionType.COPY_UPLOADER_CONFIG, async (args) => {
|
||||
const [type, id] = args as ICopyUploaderConfigArgs
|
||||
const config = copyUploaderConfig(type, id)
|
||||
return config
|
||||
try {
|
||||
const [type, configName, newConfigName] = args as ICopyUploaderConfigArgs
|
||||
picgo.uploaderConfig.copy(type, configName, newConfigName)
|
||||
const configList = picgo.uploaderConfig.getConfigList(type)
|
||||
const activeConfig = picgo.uploaderConfig.getActiveConfig(type)
|
||||
notifyAppConfigUpdated()
|
||||
return ok({
|
||||
configList,
|
||||
defaultId: activeConfig?._id ?? ''
|
||||
})
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
.add(IRPCActionType.SELECT_UPLOADER, async (args) => {
|
||||
const [type, id] = args as ISelectUploaderConfigArgs
|
||||
selectUploaderConfig(type, id)
|
||||
return true
|
||||
try {
|
||||
const [type, configName] = args as ISelectUploaderConfigArgs
|
||||
const activeConfig = picgo.uploaderConfig.use(type, configName)
|
||||
notifyAppConfigUpdated()
|
||||
return ok(activeConfig._id)
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
.add(IRPCActionType.UPDATE_UPLOADER_CONFIG, async (args) => {
|
||||
const [type, id, config] = args as IUpdateUploaderConfigArgs
|
||||
updateUploaderConfig(type, id, config)
|
||||
return true
|
||||
try {
|
||||
const [type, configId, config] = args as IUpdateUploaderConfigArgs
|
||||
const configName = typeof config._configName === 'string' ? config._configName : ''
|
||||
if (configId && !configName) {
|
||||
throw new Error(T('TIPS_UPLOADER_CONFIG_NAME_EMPTY'))
|
||||
}
|
||||
|
||||
let oldConfigName = ''
|
||||
if (configId) {
|
||||
const configList = picgo.uploaderConfig.getConfigList(type)
|
||||
const existConfig = configList.find(item => item._id === configId)
|
||||
if (!existConfig) {
|
||||
throw new Error(T('TIPS_UPLOADER_CONFIG_NOT_FOUND'))
|
||||
}
|
||||
oldConfigName = existConfig._configName
|
||||
}
|
||||
|
||||
if (oldConfigName && oldConfigName !== configName) {
|
||||
picgo.uploaderConfig.rename(type, oldConfigName, configName)
|
||||
}
|
||||
picgo.uploaderConfig.createOrUpdate(type, configName, config)
|
||||
notifyAppConfigUpdated()
|
||||
return ok(true)
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
|
||||
export {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import fs from 'fs-extra'
|
||||
import { IpcMainEvent } from 'electron'
|
||||
import { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'
|
||||
import { sendToolboxResWithType } from './utils'
|
||||
import { dbPathChecker, getGalleryDBPath } from '~/main/apis/core/datastore/dbChecker'
|
||||
@@ -10,7 +9,7 @@ import { T } from '~/main/i18n'
|
||||
export const checkFileMap: IToolboxCheckerMap<
|
||||
IToolboxItemType.IS_CONFIG_FILE_BROKEN | IToolboxItemType.IS_GALLERY_FILE_BROKEN
|
||||
> = {
|
||||
[IToolboxItemType.IS_CONFIG_FILE_BROKEN]: async (event: IpcMainEvent) => {
|
||||
[IToolboxItemType.IS_CONFIG_FILE_BROKEN]: async (event) => {
|
||||
const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.IS_CONFIG_FILE_BROKEN)
|
||||
sendToolboxRes(event, {
|
||||
status: IToolboxItemCheckStatus.LOADING
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IpcMainEvent } from 'electron'
|
||||
import type { IpcMainEvent, IpcMainInvokeEvent } from 'electron'
|
||||
import { IRPCActionType, IToolboxItemType } from '~/universal/types/enum'
|
||||
|
||||
export const sendToolboxResWithType = (type: IToolboxItemType) => (event: IpcMainEvent, res?: Omit<IToolboxCheckRes, 'type'>) => {
|
||||
export const sendToolboxResWithType = (type: IToolboxItemType) => (event: IpcMainEvent | IpcMainInvokeEvent, res?: Omit<IToolboxCheckRes, 'type'>) => {
|
||||
return event.sender.send(IRPCActionType.TOOLBOX_CHECK_RES, {
|
||||
...res,
|
||||
type
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const errorToMessage = (e: unknown): string => {
|
||||
if (e instanceof Error) return e.message
|
||||
return String(e)
|
||||
}
|
||||
|
||||
export const ok = <T>(data: T): IRPCResult<T> => ({
|
||||
success: true,
|
||||
data
|
||||
})
|
||||
|
||||
export const fail = <T>(e: unknown): IRPCResult<T> => ({
|
||||
success: false,
|
||||
error: errorToMessage(e)
|
||||
})
|
||||
|
||||
export const isIRPCResult = (value: unknown): value is IRPCResult<any> => {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
const maybe = value as { success?: unknown, data?: unknown, error?: unknown }
|
||||
if (typeof maybe.success !== 'boolean') return false
|
||||
|
||||
// success=true MUST have data; success=false MUST have error.
|
||||
if (maybe.success) return 'data' in maybe
|
||||
return typeof maybe.error === 'string'
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ class I18nManager {
|
||||
private builtinI18nFolder = getStaticPath('i18n')
|
||||
private outerI18nFolder = ''
|
||||
private localesMap: Map<string, ILocales> = new Map()
|
||||
private currentLanguage: string = 'zh-CN'
|
||||
readonly defaultLanguage: string = 'zh-CN'
|
||||
private currentLanguage: string = 'en'
|
||||
readonly defaultLanguage: string = 'en'
|
||||
private i18nFileList: II18nItem[] = builtinI18nList
|
||||
|
||||
setOuterI18nFolder (folder: string) {
|
||||
|
||||
@@ -11,7 +11,7 @@ const updateShortKeyFromVersion212 = (picgo: PicGoCore) => {
|
||||
if (shortKeyConfig === undefined) {
|
||||
const defaultShortKeyConfig = {
|
||||
enable: true,
|
||||
key: 'CommandOrControl+Shift+P',
|
||||
key: 'CommandOrControl+Shift+U',
|
||||
name: 'upload',
|
||||
label: T('QUICK_UPLOAD')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import logger from '@core/picgo/logger'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
import { uploadClipboardFiles, uploadSelectedFiles } from 'apis/app/uploader/apis'
|
||||
import path from 'path'
|
||||
import { dbPathDir, getFormImageFolderPath } from 'apis/core/datastore/dbChecker'
|
||||
import fs from 'fs-extra'
|
||||
import { cleanupFormUploaderFiles } from '~/main/utils/cleanupFormUploaderFiles'
|
||||
import {
|
||||
buildError,
|
||||
buildSuccess,
|
||||
getFormDataFileName,
|
||||
isFileLike,
|
||||
isRecord,
|
||||
type HonoContextLike
|
||||
} from './utils'
|
||||
|
||||
const STORE_PATH = dbPathDir()
|
||||
const LOG_PATH = path.join(STORE_PATH, 'picgo.log')
|
||||
|
||||
const errorMessage = `upload error. see ${LOG_PATH} for more detail.`
|
||||
|
||||
const handleClipboardUpload = async (c: HonoContextLike): Promise<Response> => {
|
||||
try {
|
||||
logger.info('[PicGo Server] upload clipboard file')
|
||||
const res = await uploadClipboardFiles()
|
||||
if (res) {
|
||||
return c.json(buildSuccess([res]))
|
||||
}
|
||||
return c.json(buildError(errorMessage), 500)
|
||||
} catch (e: unknown) {
|
||||
logger.error('[PicGo Server] upload clipboard error', e)
|
||||
return c.json(buildError(errorMessage), 500)
|
||||
}
|
||||
}
|
||||
|
||||
const handleListUpload = async (c: HonoContextLike, list: string[]): Promise<Response> => {
|
||||
try {
|
||||
logger.info('[PicGo Server] upload files in list')
|
||||
const win = windowManager.getAvailableWindow()
|
||||
if (!win) {
|
||||
return c.json(buildError(errorMessage), 500)
|
||||
}
|
||||
const pathList = list.map(item => ({ path: item }))
|
||||
const res = await uploadSelectedFiles(win.webContents, pathList)
|
||||
if (res.length) {
|
||||
return c.json(buildSuccess(res))
|
||||
}
|
||||
return c.json(buildError(errorMessage), 500)
|
||||
} catch (e: unknown) {
|
||||
logger.error('[PicGo Server] upload list error', e)
|
||||
return c.json(buildError(errorMessage), 500)
|
||||
}
|
||||
}
|
||||
|
||||
export const uploadHandler = async (c: HonoContextLike): Promise<Response> => {
|
||||
const contentType = c.req.raw.headers.get('content-type') || ''
|
||||
|
||||
if (contentType.includes('multipart/form-data')) {
|
||||
const tempFiles: string[] = []
|
||||
try {
|
||||
const formData = await c.req.formData()
|
||||
const files = formData.getAll('files') as unknown[]
|
||||
if (files.length === 0) {
|
||||
return c.json(buildError('No files found in form-data: files'), 400)
|
||||
}
|
||||
|
||||
// Pre-validate to avoid leaking already-written temp files.
|
||||
const fileLikes: Array<Parameters<typeof getFormDataFileName>[0]> = []
|
||||
for (const file of files) {
|
||||
if (!isFileLike(file)) {
|
||||
return c.json(buildError('Invalid form-data: files must be file(s)'), 400)
|
||||
}
|
||||
fileLikes.push(file)
|
||||
}
|
||||
|
||||
const formImagesPath = getFormImageFolderPath()
|
||||
await fs.ensureDir(formImagesPath)
|
||||
|
||||
for (const file of fileLikes) {
|
||||
const fileName = getFormDataFileName(file)
|
||||
const safeName = path.basename(fileName)
|
||||
const filePath = path.join(formImagesPath, safeName)
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
await fs.writeFile(filePath, buffer)
|
||||
tempFiles.push(filePath)
|
||||
}
|
||||
|
||||
return await handleListUpload(c, tempFiles)
|
||||
} catch (e: unknown) {
|
||||
logger.error('[PicGo Server] process form upload error', e)
|
||||
return c.json(buildError(errorMessage), 500)
|
||||
} finally {
|
||||
cleanupFormUploaderFiles(tempFiles)
|
||||
}
|
||||
}
|
||||
|
||||
const bodyText = await c.req.raw.text().catch(() => '')
|
||||
|
||||
// No request body -> upload from clipboard.
|
||||
if (bodyText.trim() === '') {
|
||||
return await handleClipboardUpload(c)
|
||||
}
|
||||
|
||||
let body: unknown
|
||||
try {
|
||||
body = JSON.parse(bodyText)
|
||||
} catch {
|
||||
return c.json(buildError('Invalid JSON body'), 400)
|
||||
}
|
||||
|
||||
// GUI compatibility: JSON without list (or empty list) -> upload from clipboard.
|
||||
if (!isRecord(body) || !('list' in body)) {
|
||||
return await handleClipboardUpload(c)
|
||||
}
|
||||
|
||||
const listValue = body.list
|
||||
if (listValue === undefined) {
|
||||
return await handleClipboardUpload(c)
|
||||
}
|
||||
|
||||
if (!Array.isArray(listValue)) {
|
||||
return c.json(buildError('Invalid request body: { list: string[] } required'), 400)
|
||||
}
|
||||
|
||||
if (listValue.length === 0) {
|
||||
return await handleClipboardUpload(c)
|
||||
}
|
||||
|
||||
if (!listValue.every((item) => typeof item === 'string')) {
|
||||
return c.json(buildError('Invalid request body: { list: string[] } required'), 400)
|
||||
}
|
||||
|
||||
return await handleListUpload(c, listValue)
|
||||
}
|
||||
+28
-102
@@ -1,34 +1,12 @@
|
||||
import http from 'http'
|
||||
import routers from './routerManager'
|
||||
import {
|
||||
handleResponse,
|
||||
ensureHTTPLink
|
||||
} from './utils'
|
||||
import picgo from '@core/picgo'
|
||||
import logger from '@core/picgo/logger'
|
||||
import axios from 'axios'
|
||||
import formUploader from './middlewares/formUploader'
|
||||
import { uploadHandler } from './handler'
|
||||
|
||||
class Server {
|
||||
private httpServer: http.Server
|
||||
private config: IServerConfig
|
||||
private hasRegisteredUploadOverride = false
|
||||
constructor () {
|
||||
let config = picgo.getConfig<IServerConfig>('settings.server')
|
||||
const result = this.checkIfConfigIsValid(config)
|
||||
if (result) {
|
||||
this.config = config
|
||||
} else {
|
||||
config = {
|
||||
port: 36677,
|
||||
host: '127.0.0.1',
|
||||
enable: true
|
||||
}
|
||||
this.config = config
|
||||
picgo.saveConfig({
|
||||
'settings.server': config
|
||||
})
|
||||
}
|
||||
this.httpServer = http.createServer(this.handleRequest)
|
||||
this.config = this.ensureConfig()
|
||||
}
|
||||
|
||||
private checkIfConfigIsValid (config: IObj | undefined) {
|
||||
@@ -39,98 +17,46 @@ class Server {
|
||||
}
|
||||
}
|
||||
|
||||
private handleRequest = (request: http.IncomingMessage, response: http.ServerResponse) => {
|
||||
if (request.method === 'OPTIONS') {
|
||||
handleResponse({
|
||||
response
|
||||
})
|
||||
return
|
||||
private ensureConfig (): IServerConfig {
|
||||
let config = picgo.getConfig<IServerConfig>('settings.server')
|
||||
if (this.checkIfConfigIsValid(config)) {
|
||||
return config
|
||||
}
|
||||
|
||||
if (request.method === 'POST') {
|
||||
if (!routers.getHandler(request.url!)) {
|
||||
logger.warn(`[PicGo Server] don't support [${request.url}] url`)
|
||||
handleResponse({
|
||||
response,
|
||||
statusCode: 404,
|
||||
body: {
|
||||
success: false
|
||||
}
|
||||
})
|
||||
} else if (formUploader.isFileUpload(request)) {
|
||||
formUploader.handleFileUpload(request, response)
|
||||
} else {
|
||||
let body: string = ''
|
||||
let postObj: IObj
|
||||
request.on('data', chunk => {
|
||||
body += chunk
|
||||
})
|
||||
request.on('end', () => {
|
||||
try {
|
||||
postObj = (body === '') ? {} : JSON.parse(body)
|
||||
} catch (err: any) {
|
||||
logger.error('[PicGo Server]', err)
|
||||
return handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Not sending data in JSON format'
|
||||
}
|
||||
})
|
||||
}
|
||||
logger.info('[PicGo Server] get the request', body)
|
||||
const handler = routers.getHandler(request.url!)
|
||||
handler!({
|
||||
...postObj,
|
||||
response
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logger.warn(`[PicGo Server] don't support [${request.method}] method`)
|
||||
response.statusCode = 404
|
||||
response.end()
|
||||
config = {
|
||||
port: 36677,
|
||||
host: '127.0.0.1',
|
||||
enable: true
|
||||
}
|
||||
picgo.saveConfig({
|
||||
'settings.server': config
|
||||
})
|
||||
return config
|
||||
}
|
||||
|
||||
// port as string is a bug
|
||||
private listen = (port: number | string) => {
|
||||
logger.info(`[PicGo Server] is listening at ${port}`)
|
||||
if (typeof port === 'string') {
|
||||
port = parseInt(port, 10)
|
||||
}
|
||||
this.httpServer.listen(port, this.config.host).on('error', async (err: ErrnoException) => {
|
||||
if (err.errno === 'EADDRINUSE') {
|
||||
try {
|
||||
// make sure the system has a PicGo Server instance
|
||||
await axios.post(ensureHTTPLink(`${this.config.host}:${port}/heartbeat`))
|
||||
this.shutdown(true)
|
||||
} catch (e) {
|
||||
logger.warn(`[PicGo Server] ${port} is busy, trying with port ${(port as number) + 1}`)
|
||||
// fix a bug: not write an increase number to config file
|
||||
// to solve the auto number problem
|
||||
this.listen((port as number) + 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
private ensureUploadOverrideRegistered () {
|
||||
if (this.hasRegisteredUploadOverride) return
|
||||
// @ts-expect-error override internal handler
|
||||
picgo.server.registerPost('/upload', uploadHandler, true)
|
||||
this.hasRegisteredUploadOverride = true
|
||||
}
|
||||
|
||||
startup () {
|
||||
console.log('startup', this.config.enable)
|
||||
this.config = this.ensureConfig()
|
||||
if (this.config.enable) {
|
||||
this.listen(this.config.port)
|
||||
this.ensureUploadOverrideRegistered()
|
||||
// let core resolve config defaults when possible, but preserve GUI config semantics.
|
||||
const port = typeof this.config.port === 'string' ? parseInt(this.config.port, 10) : this.config.port
|
||||
picgo.server.listen(port, this.config.host)
|
||||
}
|
||||
}
|
||||
|
||||
shutdown (hasStarted?: boolean) {
|
||||
this.httpServer.close()
|
||||
if (!hasStarted) {
|
||||
logger.info('[PicGo Server] shutdown')
|
||||
}
|
||||
shutdown () {
|
||||
picgo.server.shutdown()
|
||||
logger.info('[PicGo Server] shutdown')
|
||||
}
|
||||
|
||||
restart () {
|
||||
this.config = picgo.getConfig('settings.server')
|
||||
this.shutdown()
|
||||
this.startup()
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import http from 'http'
|
||||
import multer from 'multer'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { dbPathDir } from 'apis/core/datastore/dbChecker'
|
||||
import logger from '@core/picgo/logger'
|
||||
import { handleResponse } from '../utils'
|
||||
import routers from '../routerManager'
|
||||
import { FORM_IMAGE_FOLDER } from '~/universal/utils/static'
|
||||
|
||||
// Multer 错误类型定义
|
||||
export interface MulterError extends Error {
|
||||
code: string
|
||||
field?: string
|
||||
storageErrors?: any[]
|
||||
}
|
||||
|
||||
// Multer 中间件适配器类型
|
||||
export type MulterMiddleware = (
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
callback: (error?: MulterError) => void
|
||||
) => void
|
||||
|
||||
// 扩展 multer 函数的返回类型
|
||||
declare module 'multer' {
|
||||
interface Multer {
|
||||
array(fieldname: string, maxCount?: number): MulterMiddleware;
|
||||
single(fieldname: string): MulterMiddleware;
|
||||
fields(fields: { name: string; maxCount?: number }[]): MulterMiddleware;
|
||||
none(): MulterMiddleware;
|
||||
}
|
||||
}
|
||||
|
||||
class FormUploader {
|
||||
private upload!: multer.Multer
|
||||
private formImagesPath!: string
|
||||
|
||||
constructor () {
|
||||
this.initializeStorage()
|
||||
this.setupMulter()
|
||||
}
|
||||
|
||||
private initializeStorage () {
|
||||
const STORE_PATH = dbPathDir()
|
||||
this.formImagesPath = path.join(STORE_PATH, FORM_IMAGE_FOLDER)
|
||||
if (!fs.existsSync(this.formImagesPath)) {
|
||||
fs.mkdirSync(this.formImagesPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
private setupMulter () {
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req: http.IncomingMessage, file: Express.Multer.File, cb: (error: Error | null, destination: string) => void) => {
|
||||
cb(null, this.formImagesPath)
|
||||
},
|
||||
filename: (req: http.IncomingMessage, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => {
|
||||
cb(null, file.originalname || Date.now() + path.extname(file.originalname))
|
||||
}
|
||||
})
|
||||
|
||||
this.upload = multer({
|
||||
storage
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文件上传的中间件
|
||||
*/
|
||||
public handleFileUpload = (request: http.IncomingMessage, response: http.ServerResponse): void => {
|
||||
logger.info('[PicGo Server] handling file upload')
|
||||
|
||||
this.upload.array('files')(request, response, async (err?: MulterError) => {
|
||||
if (err) {
|
||||
logger.error('[PicGo Server] file upload error', err)
|
||||
return handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'File upload failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const files = request.files
|
||||
if (!files || files.length === 0) {
|
||||
return handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'No files were uploaded'
|
||||
}
|
||||
})
|
||||
}
|
||||
const filePaths = files.map((file) => file.path)
|
||||
logger.info('[PicGo Server] files uploaded: ' + filePaths.join(', '))
|
||||
|
||||
const handler = routers.getHandler(request.url!)
|
||||
handler!({
|
||||
list: filePaths,
|
||||
response
|
||||
})
|
||||
} catch (err: any) {
|
||||
logger.error('[PicGo Server] process upload files error', err)
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Failed to process uploaded files'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查请求是否为文件上传
|
||||
*/
|
||||
public isFileUpload (request: http.IncomingMessage): boolean {
|
||||
return !!(request.headers['content-type'] && request.headers['content-type'].includes('multipart/form-data'))
|
||||
}
|
||||
}
|
||||
|
||||
export default new FormUploader()
|
||||
@@ -1,21 +0,0 @@
|
||||
class Router {
|
||||
private router = new Map<string, routeHandler>()
|
||||
|
||||
get (url: string, callback: routeHandler): void {
|
||||
this.router.set(url, callback)
|
||||
}
|
||||
|
||||
post (url: string, callback: routeHandler): void {
|
||||
this.router.set(url, callback)
|
||||
}
|
||||
|
||||
getHandler (url: string) {
|
||||
if (this.router.has(url)) {
|
||||
return this.router.get(url)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Router()
|
||||
@@ -1,100 +0,0 @@
|
||||
import router from './router'
|
||||
import {
|
||||
handleResponse
|
||||
} from './utils'
|
||||
import logger from '@core/picgo/logger'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
import { uploadSelectedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis'
|
||||
import path from 'path'
|
||||
import { dbPathDir } from 'apis/core/datastore/dbChecker'
|
||||
const STORE_PATH = dbPathDir()
|
||||
const LOG_PATH = path.join(STORE_PATH, 'picgo.log')
|
||||
|
||||
const errorMessage = `upload error. see ${LOG_PATH} for more detail.`
|
||||
|
||||
router.post('/upload', async ({
|
||||
response,
|
||||
list = []
|
||||
} : {
|
||||
response: IHttpResponse,
|
||||
list?: string[]
|
||||
}): Promise<void> => {
|
||||
try {
|
||||
if (list.length === 0) {
|
||||
// upload with clipboard
|
||||
logger.info('[PicGo Server] upload clipboard file')
|
||||
const res = await uploadClipboardFiles()
|
||||
logger.info('[PicGo Server] upload result:', res)
|
||||
if (res) {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: true,
|
||||
result: [res]
|
||||
}
|
||||
})
|
||||
} else {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logger.info('[PicGo Server] upload files in list')
|
||||
// upload with files
|
||||
const pathList = list.map(item => {
|
||||
return {
|
||||
path: item
|
||||
}
|
||||
})
|
||||
const win = windowManager.getAvailableWindow()
|
||||
const res = await uploadSelectedFiles(win.webContents, pathList)
|
||||
logger.info('[PicGo Server] upload result', res.join(' ; '))
|
||||
if (res.length) {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: true,
|
||||
result: res
|
||||
}
|
||||
})
|
||||
} else {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/heartbeat', async ({
|
||||
response
|
||||
} : {
|
||||
response: IHttpResponse,
|
||||
}) => {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: true,
|
||||
result: 'alive'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export default router
|
||||
+44
-29
@@ -1,33 +1,48 @@
|
||||
import logger from '@core/picgo/logger'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
export const handleResponse = ({
|
||||
response,
|
||||
statusCode = 200,
|
||||
header = {
|
||||
'Content-Type': 'application/json',
|
||||
'access-control-allow-headers': '*',
|
||||
'access-control-allow-methods': 'POST, GET, OPTIONS',
|
||||
'access-control-allow-origin': '*'
|
||||
},
|
||||
body = {
|
||||
success: false
|
||||
}
|
||||
} : {
|
||||
response: IHttpResponse,
|
||||
statusCode?: number,
|
||||
header?: IObj,
|
||||
body?: any
|
||||
}) => {
|
||||
if (body?.success === false) {
|
||||
logger.warn('[PicGo Server] upload failed, see picgo.log for more detail ↑')
|
||||
}
|
||||
response.writeHead(statusCode, header)
|
||||
response.write(JSON.stringify(body))
|
||||
response.end()
|
||||
export type UploadResponseBody = {
|
||||
success: boolean
|
||||
result: string[]
|
||||
message?: string
|
||||
}
|
||||
|
||||
export const ensureHTTPLink = (url: string): string => {
|
||||
return url.startsWith('http')
|
||||
? url
|
||||
: `http://${url}`
|
||||
export type HonoContextLike = {
|
||||
req: {
|
||||
raw: Request
|
||||
formData: () => Promise<FormData>
|
||||
}
|
||||
json: (data: unknown, status?: number) => Response
|
||||
}
|
||||
|
||||
type FormDataFileLike = {
|
||||
name?: string
|
||||
arrayBuffer: () => Promise<ArrayBuffer>
|
||||
}
|
||||
|
||||
export const isFileLike = (value: unknown): value is FormDataFileLike => {
|
||||
if (typeof value !== 'object' || value === null) return false
|
||||
if (!('arrayBuffer' in value)) return false
|
||||
return typeof (value as { arrayBuffer?: unknown }).arrayBuffer === 'function'
|
||||
}
|
||||
|
||||
export const isRecord = (value: unknown): value is Record<string, unknown> => {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
export const buildSuccess = (result: string[]): UploadResponseBody => ({
|
||||
success: true,
|
||||
result
|
||||
})
|
||||
|
||||
export const buildError = (message: string): UploadResponseBody => ({
|
||||
success: false,
|
||||
result: [],
|
||||
message
|
||||
})
|
||||
|
||||
export const getFormDataFileName = (value: FormDataFileLike): string => {
|
||||
const name = value.name
|
||||
if (typeof name === 'string' && name.trim() !== '') return name
|
||||
return `${randomUUID()}.png`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { APP_CONFIG_UPDATED } from '#/events/constants'
|
||||
import { IWindowList } from '#/types/enum'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
|
||||
const TARGET_WINDOWS: IWindowList[] = [
|
||||
IWindowList.SETTING_WINDOW,
|
||||
IWindowList.TRAY_WINDOW,
|
||||
IWindowList.MINI_WINDOW
|
||||
]
|
||||
|
||||
export const notifyAppConfigUpdated = (): void => {
|
||||
TARGET_WINDOWS.forEach((windowType) => {
|
||||
if (!windowManager.has(windowType)) return
|
||||
windowManager.get(windowType)?.webContents.send(APP_CONFIG_UPDATED)
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import picgo from '@core/picgo'
|
||||
import { i18nManager } from '~/main/i18n'
|
||||
export const initI18n = () => {
|
||||
const currentLanguage = picgo.getConfig<string>('settings.language') || 'zh-CN'
|
||||
const currentLanguage = picgo.getConfig<string>('settings.language') || 'en'
|
||||
i18nManager.setCurrentLanguage(currentLanguage)
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import { T } from '~/main/i18n'
|
||||
|
||||
class PrivacyManager {
|
||||
async check () {
|
||||
if (picgo.getConfig<boolean>('settings.privacyEnsure') !== true) {
|
||||
if (picgo.getConfig<string | boolean>('settings.privacyEnsure') !== '20260127') {
|
||||
const res = await this.show(true)
|
||||
// cancel
|
||||
if (res.result === 1) {
|
||||
return false
|
||||
} else {
|
||||
picgo.saveConfig({ 'settings.privacyEnsure': true })
|
||||
picgo.saveConfig({ 'settings.privacyEnsure': '20260127' })
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,16 @@ class PrivacyManager {
|
||||
}
|
||||
|
||||
async show (showCancel = true) {
|
||||
const privacyUrl = 'https://picgo.app/privacy/'
|
||||
const termsUrl = 'https://picgo.app/terms/'
|
||||
const res = await showMessageBox({
|
||||
type: 'info',
|
||||
buttons: showCancel ? ['Yes', 'No'] : ['Yes'],
|
||||
title: T('PRIVACY_AGREEMENT'),
|
||||
message: T('PRIVACY')
|
||||
title: T('PRIVACY_TERMS_AGREEMENT'),
|
||||
message: T('PRIVACY', {
|
||||
privacyUrl,
|
||||
termsUrl
|
||||
})
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
+12
-7
@@ -7,22 +7,27 @@
|
||||
<script lang="ts" setup>
|
||||
import { useStore } from '@/hooks/useStore'
|
||||
import { onBeforeMount, onMounted, onUnmounted } from 'vue'
|
||||
import { getConfig } from './utils/dataSender'
|
||||
import type { IConfig } from 'picgo'
|
||||
import bus from './utils/bus'
|
||||
import { FORCE_UPDATE } from '~/universal/events/constants'
|
||||
import { APP_CONFIG_UPDATED, FORCE_UPDATE } from '~/universal/events/constants'
|
||||
import { useATagClick } from './hooks/useATagClick'
|
||||
import { useIPCOn } from './hooks/useIPC'
|
||||
|
||||
useATagClick()
|
||||
|
||||
const store = useStore()
|
||||
const handleAppConfigUpdated = () => {
|
||||
store?.refreshAppConfig()
|
||||
store?.refreshPicBeds()
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const config = await getConfig<IConfig>()
|
||||
if (config) {
|
||||
store?.setDefaultPicBed(config?.picBed?.uploader || config?.picBed?.current || 'smms')
|
||||
}
|
||||
if (!store) return
|
||||
await store.refreshAppConfig()
|
||||
await store.refreshPicBeds()
|
||||
})
|
||||
|
||||
useIPCOn(APP_CONFIG_UPDATED, handleAppConfigUpdated)
|
||||
|
||||
onMounted(() => {
|
||||
bus.on(FORCE_UPDATE, () => {
|
||||
store?.updateForceUpdateTime()
|
||||
|
||||
@@ -32,6 +32,7 @@ import { getConfig } from '@/utils/dataSender'
|
||||
import { useRoute } from 'vue-router'
|
||||
import BaseConfigForm from './form/BaseConfigForm.vue'
|
||||
import { useConfigForm } from '@/hooks/useConfigForm'
|
||||
import { useStore } from '@/hooks/useStore'
|
||||
|
||||
interface IProps {
|
||||
config: any[]
|
||||
@@ -46,6 +47,7 @@ const $form = ref<IFormInstance>()
|
||||
const configList = ref<IPicGoPluginConfig[]>([])
|
||||
const formModel = reactive<IStringKeyMap>({})
|
||||
const isUploader = props.type === 'uploader'
|
||||
const store = useStore()
|
||||
|
||||
async function validate (): Promise<IStringKeyMap | false> {
|
||||
const res = await $form.value?.validate() || false
|
||||
@@ -81,7 +83,10 @@ async function getCurConfigFormData () {
|
||||
const configId = $route.params.configId
|
||||
const configType = getConfigType()
|
||||
if (isUploader) {
|
||||
const curTypeConfigList = await getConfig<IStringKeyMap[]>(`uploader.${props.id}.configList`) || []
|
||||
const cachedList = store?.state.appConfig?.uploader?.[props.id]?.configList
|
||||
const curTypeConfigList = Array.isArray(cachedList)
|
||||
? cachedList
|
||||
: await getConfig<IStringKeyMap[]>(`uploader.${props.id}.configList`) || []
|
||||
return curTypeConfigList.find(i => i._id === configId) || {}
|
||||
} else {
|
||||
const config = await getConfig<IStringKeyMap>(configType)
|
||||
|
||||
@@ -9,9 +9,24 @@
|
||||
v-model="inputBoxValue"
|
||||
:placeholder="inputBoxOptions.placeholder"
|
||||
:type="inputBoxOptions.inputType || 'text'"
|
||||
:show-password="inputBoxOptions.inputType === 'password'"
|
||||
:rows="inputBoxOptions.inputType === 'textarea' ? 6 : undefined"
|
||||
:class="{ 'input-box__textarea': inputBoxOptions.inputType === 'textarea' }"
|
||||
/>
|
||||
<el-input
|
||||
v-if="inputBoxOptions.hasConfirm"
|
||||
v-model="inputBoxConfirmValue"
|
||||
class="mt-[12px]"
|
||||
:placeholder="inputBoxOptions.confirmPlaceholder"
|
||||
:type="inputBoxOptions.inputType || 'text'"
|
||||
:show-password="inputBoxOptions.inputType === 'password'"
|
||||
/>
|
||||
<div
|
||||
v-if="confirmError"
|
||||
class="mt-[8px] text-[12px] text-[#f56c6c] leading-[18px]"
|
||||
>
|
||||
{{ confirmError }}
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button
|
||||
round
|
||||
@@ -30,7 +45,7 @@
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onBeforeUnmount, onBeforeMount } from 'vue'
|
||||
import { ref, reactive, onBeforeUnmount, onBeforeMount, watch } from 'vue'
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron'
|
||||
import {
|
||||
SHOW_INPUT_BOX,
|
||||
@@ -38,13 +53,18 @@ import {
|
||||
} from '~/universal/events/constants'
|
||||
import $bus from '@/utils/bus'
|
||||
import { sendToMain } from '@/utils/dataSender'
|
||||
import { T as $T } from '@/i18n/index'
|
||||
const inputBoxValue = ref('')
|
||||
const inputBoxConfirmValue = ref('')
|
||||
const confirmError = ref('')
|
||||
const showInputBoxVisible = ref(false)
|
||||
const inputBoxOptions = reactive({
|
||||
title: '',
|
||||
placeholder: '',
|
||||
inputType: 'text' as 'text' | 'textarea',
|
||||
width: 500
|
||||
inputType: 'text' as 'text' | 'textarea' | 'password',
|
||||
width: 500,
|
||||
hasConfirm: false,
|
||||
confirmPlaceholder: ''
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
@@ -58,10 +78,14 @@ function ipcEventHandler (evt: IpcRendererEvent, options: IShowInputBoxOption) {
|
||||
|
||||
function initInputBoxValue (options: IShowInputBoxOption) {
|
||||
inputBoxValue.value = options.value || ''
|
||||
inputBoxConfirmValue.value = options.confirm?.value || ''
|
||||
inputBoxOptions.title = options.title || ''
|
||||
inputBoxOptions.placeholder = options.placeholder || ''
|
||||
inputBoxOptions.inputType = options.inputType || 'text'
|
||||
inputBoxOptions.width = options.width || 400
|
||||
inputBoxOptions.hasConfirm = !!options.confirm
|
||||
inputBoxOptions.confirmPlaceholder = options.confirm?.placeholder || ''
|
||||
confirmError.value = ''
|
||||
showInputBoxVisible.value = true
|
||||
}
|
||||
|
||||
@@ -73,11 +97,19 @@ function handleInputBoxCancel () {
|
||||
}
|
||||
|
||||
function handleInputBoxConfirm () {
|
||||
if (inputBoxOptions.hasConfirm && inputBoxValue.value !== inputBoxConfirmValue.value) {
|
||||
confirmError.value = $T('INPUT_BOX_CONFIRM_MISMATCH')
|
||||
return
|
||||
}
|
||||
showInputBoxVisible.value = false
|
||||
sendToMain(SHOW_INPUT_BOX, inputBoxValue.value)
|
||||
$bus.emit(SHOW_INPUT_BOX_RESPONSE, inputBoxValue.value)
|
||||
}
|
||||
|
||||
watch([inputBoxValue, inputBoxConfirmValue], () => {
|
||||
confirmError.value = ''
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
ipcRenderer.removeListener(SHOW_INPUT_BOX, ipcEventHandler)
|
||||
$bus.off(SHOW_INPUT_BOX)
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
class="picgo-cloud-config-sync-conflict-dialog"
|
||||
width="80%"
|
||||
top="6vh"
|
||||
append-to-body
|
||||
lock-scroll
|
||||
header-class="!px-[20px] !pt-[18px] !pb-[14px] !m-0 border-b border-slate-100"
|
||||
body-class="!p-0 overflow-hidden"
|
||||
footer-class="!px-[20px] !py-[16px] border-t border-slate-100"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-start justify-between gap-[16px]">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<el-icon>
|
||||
<WarningFilled class="text-[20px] text-orange-500" />
|
||||
</el-icon>
|
||||
<div class="text-[20px] font-semibold text-slate-800 leading-[22px]">
|
||||
{{ $T('PICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE', { count: conflicts.length }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-[10px] flex-shrink-0">
|
||||
<el-button
|
||||
size="small"
|
||||
round
|
||||
@click="handleResetAll"
|
||||
>
|
||||
<el-icon class="mr-1">
|
||||
<RefreshLeft />
|
||||
</el-icon>
|
||||
{{ $T('PICGO_CLOUD_CONFIG_SYNC_RESET_ALL') }}
|
||||
</el-button>
|
||||
<el-button-group>
|
||||
<el-button
|
||||
size="small"
|
||||
round
|
||||
@click="handleChooseAllLocal"
|
||||
>
|
||||
{{ $T('PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
round
|
||||
@click="handleChooseAllCloud"
|
||||
>
|
||||
{{ $T('PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-[4px] text-[12px] text-slate-500 leading-[18px]">
|
||||
{{ $T('PICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Conflict List -->
|
||||
<div>
|
||||
<div class="h-full overflow-y-auto px-[20px] pt-[16px] pb-[24px] picgo-cloud-conflict-list">
|
||||
<div
|
||||
v-for="item in conflicts"
|
||||
:key="item.path"
|
||||
class="mb-[16px] last:mb-0 rounded-[12px] border-2 bg-white shadow-[0_1px_6px_rgba(15,23,42,0.10)] transition-colors duration-200 overflow-hidden"
|
||||
:class="getCardBorderClass(item.path)"
|
||||
>
|
||||
<!-- Card Header -->
|
||||
<div class="bg-slate-100/80 px-[16px] py-[10px] border-b border-slate-200 flex justify-between items-center gap-[12px]">
|
||||
<div class="min-w-0 flex items-center gap-2">
|
||||
<el-icon class="text-slate-500">
|
||||
<Operation />
|
||||
</el-icon>
|
||||
<span class="font-mono font-medium text-[12px] text-slate-700 leading-[16px] break-all">
|
||||
{{ item.path }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="selections[item.path]"
|
||||
class="px-[10px] py-[4px] rounded-full text-[12px] font-medium flex items-center gap-1 shrink-0 border"
|
||||
:class="selections[item.path] === IPicGoCloudConfigSyncConflictChoice.LOCAL
|
||||
? 'bg-blue-100 text-blue-700 border-blue-200'
|
||||
: 'bg-purple-100 text-purple-700 border-purple-200'"
|
||||
>
|
||||
<el-icon class="text-[14px]">
|
||||
<Check />
|
||||
</el-icon>
|
||||
{{ selections[item.path] === IPicGoCloudConfigSyncConflictChoice.LOCAL
|
||||
? $T('PICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION')
|
||||
: $T('PICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Content -->
|
||||
<div>
|
||||
<div class="grid grid-cols-[1fr_auto_1fr] gap-[12px] items-stretch overflow-x-auto p-[16px]">
|
||||
<!-- Local Option -->
|
||||
<div
|
||||
class="relative cursor-pointer rounded-[8px] p-[16px] border-2 transition-all duration-200 group flex flex-col"
|
||||
:class="localBoxClass(item.path)"
|
||||
@click="setChoice(item.path, IPicGoCloudConfigSyncConflictChoice.LOCAL)"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-[8px] mb-[8px] font-semibold text-[14px]"
|
||||
:class="selections[item.path] === IPicGoCloudConfigSyncConflictChoice.LOCAL ? 'text-blue-600' : 'text-gray-600'"
|
||||
>
|
||||
<el-icon class="text-[16px]">
|
||||
<Monitor />
|
||||
</el-icon>
|
||||
<span>{{ $T('PICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION') }}</span>
|
||||
</div>
|
||||
<div class="font-mono text-[13px] text-slate-800 break-words leading-relaxed whitespace-pre-wrap">
|
||||
{{ formatValue(item.localValue) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="selections[item.path] === IPicGoCloudConfigSyncConflictChoice.LOCAL"
|
||||
class="absolute top-[12px] right-[12px] text-blue-500"
|
||||
>
|
||||
<el-icon class="text-[20px]">
|
||||
<Check />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Middle Indicator -->
|
||||
<div class="flex flex-col items-center justify-center w-[40px]">
|
||||
<el-icon
|
||||
v-if="selections[item.path] === IPicGoCloudConfigSyncConflictChoice.LOCAL"
|
||||
class="text-[24px] text-blue-500"
|
||||
>
|
||||
<ArrowLeft />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
v-else-if="selections[item.path] === IPicGoCloudConfigSyncConflictChoice.CLOUD"
|
||||
class="text-[24px] text-purple-500"
|
||||
>
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
<div
|
||||
v-else
|
||||
class="w-[2px] h-full bg-gray-100 rounded-full my-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Cloud Option -->
|
||||
<div
|
||||
class="relative cursor-pointer rounded-[8px] p-[16px] border-2 transition-all duration-200 group flex flex-col"
|
||||
:class="cloudBoxClass(item.path)"
|
||||
@click="setChoice(item.path, IPicGoCloudConfigSyncConflictChoice.CLOUD)"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-[8px] mb-[8px] font-semibold text-[14px]"
|
||||
:class="selections[item.path] === IPicGoCloudConfigSyncConflictChoice.CLOUD ? 'text-purple-600' : 'text-gray-600'"
|
||||
>
|
||||
<el-icon class="text-[16px]">
|
||||
<Cloudy />
|
||||
</el-icon>
|
||||
<span>{{ $T('PICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION') }}</span>
|
||||
</div>
|
||||
<div class="font-mono text-[13px] text-slate-800 break-words leading-relaxed whitespace-pre-wrap">
|
||||
{{ formatValue(item.remoteValue) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="selections[item.path] === IPicGoCloudConfigSyncConflictChoice.CLOUD"
|
||||
class="absolute top-[12px] right-[12px] text-purple-500"
|
||||
>
|
||||
<el-icon class="text-[20px]">
|
||||
<Check />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-[13px] text-gray-500 mr-[20px] text-left">
|
||||
<span
|
||||
v-if="remainingCount > 0"
|
||||
>
|
||||
{{ $T('PICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING') }}
|
||||
<span class="font-bold text-orange-500 ml-1">{{ remainingCount }}</span>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-green-600 flex items-center gap-1"
|
||||
>
|
||||
<el-icon class="text-[14px]">
|
||||
<SuccessFilled />
|
||||
</el-icon>
|
||||
{{ $T('PICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-[12px]">
|
||||
<el-button
|
||||
round
|
||||
plain
|
||||
:disabled="confirmLoading"
|
||||
@click="handleAbort"
|
||||
>
|
||||
{{ $T('PICGO_CLOUD_CONFIG_SYNC_ABORT') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
round
|
||||
:loading="confirmLoading"
|
||||
:disabled="isConfirmDisabled || confirmLoading"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ $T('PICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeUnmount, reactive, watch } from 'vue'
|
||||
import { T as $T } from '@/i18n/index'
|
||||
import {
|
||||
IPicGoCloudConfigSyncConflictChoice,
|
||||
type IPicGoCloudConfigSyncConflictItem,
|
||||
type IPicGoCloudConfigSyncResolution
|
||||
} from '#/types/cloudConfigSync'
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Check,
|
||||
Cloudy,
|
||||
Monitor,
|
||||
Operation,
|
||||
RefreshLeft,
|
||||
SuccessFilled,
|
||||
WarningFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
conflicts: IPicGoCloudConfigSyncConflictItem[]
|
||||
confirmLoading: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'abort'): void
|
||||
(e: 'confirm', resolution: IPicGoCloudConfigSyncResolution): void
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
type IScrollLockRecord = {
|
||||
el: HTMLElement
|
||||
previousOverflow: string
|
||||
}
|
||||
|
||||
const scrollLockRecords: IScrollLockRecord[] = []
|
||||
|
||||
const lockPageScroll = () => {
|
||||
if (typeof document === 'undefined') return
|
||||
if (scrollLockRecords.length > 0) return
|
||||
|
||||
const targets: Array<HTMLElement | null> = [
|
||||
document.documentElement,
|
||||
document.body,
|
||||
document.getElementById('main-page'),
|
||||
document.querySelector<HTMLElement>('.main-wrapper')
|
||||
]
|
||||
|
||||
targets.forEach((el) => {
|
||||
if (!el) return
|
||||
scrollLockRecords.push({
|
||||
el,
|
||||
previousOverflow: el.style.overflow
|
||||
})
|
||||
el.style.overflow = 'hidden'
|
||||
})
|
||||
}
|
||||
|
||||
const unlockPageScroll = () => {
|
||||
if (scrollLockRecords.length === 0) return
|
||||
scrollLockRecords.forEach(({ el, previousOverflow }) => {
|
||||
el.style.overflow = previousOverflow
|
||||
})
|
||||
scrollLockRecords.length = 0
|
||||
}
|
||||
|
||||
watch(visible, (nextVisible) => {
|
||||
if (nextVisible) {
|
||||
lockPageScroll()
|
||||
return
|
||||
}
|
||||
unlockPageScroll()
|
||||
}, { immediate: true })
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unlockPageScroll()
|
||||
})
|
||||
|
||||
const selections = reactive<Record<string, IPicGoCloudConfigSyncConflictChoice | undefined>>({})
|
||||
|
||||
watch(() => props.conflicts, (next) => {
|
||||
// New conflict set: reset selections.
|
||||
Object.keys(selections).forEach((key) => {
|
||||
delete selections[key]
|
||||
})
|
||||
next.forEach((item) => {
|
||||
selections[item.path] = undefined
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
const isConfirmDisabled = computed(() => {
|
||||
if (!props.conflicts.length) return true
|
||||
return props.conflicts.some(item => !selections[item.path])
|
||||
})
|
||||
|
||||
const remainingCount = computed(() => {
|
||||
return props.conflicts.filter(item => !selections[item.path]).length
|
||||
})
|
||||
|
||||
const setChoice = (path: string, choice: IPicGoCloudConfigSyncConflictChoice) => {
|
||||
selections[path] = choice
|
||||
}
|
||||
|
||||
const getCardBorderClass = (path: string) => {
|
||||
const choice = selections[path]
|
||||
if (!choice) return 'border-slate-300'
|
||||
return choice === IPicGoCloudConfigSyncConflictChoice.LOCAL ? 'border-blue-300' : 'border-purple-300'
|
||||
}
|
||||
|
||||
const localBoxClass = (path: string) => {
|
||||
const selected = selections[path] === IPicGoCloudConfigSyncConflictChoice.LOCAL
|
||||
return selected
|
||||
? 'bg-blue-50 border-blue-500 ring-1 ring-blue-500'
|
||||
: 'bg-white border-gray-200 hover:border-blue-300 hover:bg-gray-100'
|
||||
}
|
||||
|
||||
const cloudBoxClass = (path: string) => {
|
||||
const selected = selections[path] === IPicGoCloudConfigSyncConflictChoice.CLOUD
|
||||
return selected
|
||||
? 'bg-purple-50 border-purple-500 ring-1 ring-purple-500'
|
||||
: 'bg-white border-gray-200 hover:border-purple-300 hover:bg-gray-100'
|
||||
}
|
||||
|
||||
const handleChooseAllLocal = () => {
|
||||
props.conflicts.forEach(item => {
|
||||
selections[item.path] = IPicGoCloudConfigSyncConflictChoice.LOCAL
|
||||
})
|
||||
}
|
||||
|
||||
const handleChooseAllCloud = () => {
|
||||
props.conflicts.forEach(item => {
|
||||
selections[item.path] = IPicGoCloudConfigSyncConflictChoice.CLOUD
|
||||
})
|
||||
}
|
||||
|
||||
const handleResetAll = () => {
|
||||
props.conflicts.forEach(item => {
|
||||
selections[item.path] = undefined
|
||||
})
|
||||
}
|
||||
|
||||
const handleAbort = () => {
|
||||
emit('abort')
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
const resolution: IPicGoCloudConfigSyncResolution = {}
|
||||
props.conflicts.forEach(item => {
|
||||
const choice = selections[item.path]
|
||||
if (choice) {
|
||||
resolution[item.path] = choice
|
||||
}
|
||||
})
|
||||
emit('confirm', resolution)
|
||||
}
|
||||
|
||||
const formatValue = (value: unknown): string => {
|
||||
if (value === undefined) return $T('PICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED')
|
||||
try {
|
||||
return JSON.stringify(value, null, 2) ?? String(value)
|
||||
} catch (e) {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.picgo-cloud-conflict-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.picgo-cloud-conflict-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.picgo-cloud-conflict-list::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.picgo-cloud-conflict-list::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,6 @@
|
||||
<el-tooltip
|
||||
class="item"
|
||||
effect="dark"
|
||||
:open="true"
|
||||
placement="right"
|
||||
>
|
||||
<template #content>
|
||||
|
||||
@@ -77,6 +77,12 @@
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
<el-menu-item :index="routerConfig.PICGO_CLOUD_PAGE">
|
||||
<el-icon>
|
||||
<Cloudy />
|
||||
</el-icon>
|
||||
<span>PicGo Cloud</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="routerConfig.SETTING_PAGE">
|
||||
<el-icon>
|
||||
<Setting />
|
||||
@@ -205,6 +211,7 @@ import {
|
||||
UploadFilled,
|
||||
PictureFilled,
|
||||
Menu,
|
||||
Cloudy,
|
||||
Share,
|
||||
InfoFilled,
|
||||
Minus,
|
||||
@@ -213,7 +220,7 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import { ElMessage as $message } from 'element-plus'
|
||||
import { T as $T } from '@/i18n/index'
|
||||
import { ref, onBeforeUnmount, Ref, onBeforeMount, watch, nextTick, reactive } from 'vue'
|
||||
import { ref, onBeforeUnmount, Ref, onBeforeMount, watch, nextTick, reactive, computed } from 'vue'
|
||||
import { onBeforeRouteUpdate, useRouter } from 'vue-router'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import pick from 'lodash/pick'
|
||||
@@ -221,7 +228,6 @@ import pkg from 'root/package.json'
|
||||
import * as config from '@/router/config'
|
||||
import {
|
||||
ipcRenderer,
|
||||
IpcRendererEvent,
|
||||
clipboard
|
||||
} from 'electron'
|
||||
import InputBoxDialog from '@/components/dialog/InputBoxDialog.vue'
|
||||
@@ -230,18 +236,19 @@ import {
|
||||
CLOSE_WINDOW,
|
||||
SHOW_MAIN_PAGE_MENU,
|
||||
SHOW_MAIN_PAGE_QRCODE,
|
||||
SHOW_MAIN_PAGE_DONATION,
|
||||
GET_PICBEDS
|
||||
SHOW_MAIN_PAGE_DONATION
|
||||
} from '~/universal/events/constants'
|
||||
import { getConfig, sendToMain } from '@/utils/dataSender'
|
||||
import { useOS } from '@/hooks/useOS'
|
||||
import { useStore } from '@/hooks/useStore'
|
||||
const version = ref(process.env.NODE_ENV === 'production' ? pkg.version : 'Dev')
|
||||
const routerConfig = reactive(config)
|
||||
const defaultActive = ref(routerConfig.UPLOAD_PAGE)
|
||||
const visible = ref(false)
|
||||
const os = useOS()
|
||||
const $router = useRouter()
|
||||
const picBed: Ref<IPicBedType[]> = ref([])
|
||||
const store = useStore()
|
||||
const picBed = computed(() => store?.state.picBeds ?? [])
|
||||
const qrcodeVisible = ref(false)
|
||||
const picBedConfigString = ref('')
|
||||
const choosedPicBedForQRCode: Ref<string[]> = ref([])
|
||||
@@ -249,8 +256,7 @@ const choosedPicBedForQRCode: Ref<string[]> = ref([])
|
||||
const keepAlivePages = $router.getRoutes().filter(item => item.meta.keepAlive).map(item => item.name as string)
|
||||
|
||||
onBeforeMount(() => {
|
||||
sendToMain(GET_PICBEDS)
|
||||
ipcRenderer.on(GET_PICBEDS, getPicBeds)
|
||||
store?.refreshPicBeds()
|
||||
handleGetPicPeds()
|
||||
ipcRenderer.on(SHOW_MAIN_PAGE_QRCODE, () => {
|
||||
qrcodeVisible.value = true
|
||||
@@ -263,7 +269,7 @@ onBeforeMount(() => {
|
||||
watch(() => choosedPicBedForQRCode, (val) => {
|
||||
if (val.value.length > 0) {
|
||||
nextTick(async () => {
|
||||
const picBedConfig = await getConfig('picBed')
|
||||
const picBedConfig = store?.state.appConfig?.picBed ?? await getConfig('picBed')
|
||||
const config = pick(picBedConfig, ...choosedPicBedForQRCode.value)
|
||||
picBedConfigString.value = JSON.stringify(config)
|
||||
})
|
||||
@@ -271,7 +277,7 @@ watch(() => choosedPicBedForQRCode, (val) => {
|
||||
}, { deep: true })
|
||||
|
||||
const handleGetPicPeds = () => {
|
||||
sendToMain(GET_PICBEDS)
|
||||
store?.refreshPicBeds()
|
||||
}
|
||||
|
||||
const handleSelect = (index: string) => {
|
||||
@@ -325,10 +331,6 @@ function handleCopyPicBedConfig () {
|
||||
$message.success($T('COPY_PICBED_CONFIG_SUCCEED'))
|
||||
}
|
||||
|
||||
function getPicBeds (event: IpcRendererEvent, picBeds: IPicBedType[]) {
|
||||
picBed.value = picBeds
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate(async (to) => {
|
||||
if (to.params.type) {
|
||||
defaultActive.value = `${routerConfig.UPLOADER_CONFIG_PAGE}-${to.params.type}`
|
||||
@@ -338,7 +340,8 @@ onBeforeRouteUpdate(async (to) => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
ipcRenderer.removeListener(GET_PICBEDS, getPicBeds)
|
||||
ipcRenderer.removeAllListeners(SHOW_MAIN_PAGE_QRCODE)
|
||||
ipcRenderer.removeAllListeners(SHOW_MAIN_PAGE_DONATION)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { dragMixin } from '@/utils/mixin'
|
||||
import { initTalkingData } from './utils/analytics'
|
||||
import db from './utils/db'
|
||||
import { i18nManager, T } from './i18n/index'
|
||||
import { getConfig, saveConfig, sendToMain, triggerRPC } from '@/utils/dataSender'
|
||||
import { getConfig, saveConfig, sendToMain } from '@/utils/dataSender'
|
||||
import { store } from '@/store'
|
||||
import vue3PhotoPreview from 'vue3-photo-preview'
|
||||
import 'vue3-photo-preview/dist/index.css'
|
||||
@@ -37,7 +37,6 @@ app.config.globalProperties.$http = axios
|
||||
app.config.globalProperties.$T = T
|
||||
app.config.globalProperties.$i18n = i18nManager
|
||||
app.config.globalProperties.getConfig = getConfig
|
||||
app.config.globalProperties.triggerRPC = triggerRPC
|
||||
app.config.globalProperties.saveConfig = saveConfig
|
||||
app.config.globalProperties.sendToMain = sendToMain
|
||||
|
||||
|
||||
@@ -187,16 +187,15 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { IResult } from '@picgo/store/dist/types'
|
||||
import { PASTE_TEXT, GET_PICBEDS } from '#/events/constants'
|
||||
import { PASTE_TEXT } from '#/events/constants'
|
||||
import { CheckboxValueType, ElMessageBox } from 'element-plus'
|
||||
import { Close, CaretBottom, Document, Edit, Delete, CaretTop } from '@element-plus/icons-vue'
|
||||
import {
|
||||
ipcRenderer,
|
||||
clipboard,
|
||||
IpcRendererEvent
|
||||
clipboard
|
||||
} from 'electron'
|
||||
import { computed, nextTick, onActivated, onBeforeUnmount, onBeforeMount, reactive, ref, watch } from 'vue'
|
||||
import { getConfig, saveConfig, sendRPC, sendToMain } from '@/utils/dataSender'
|
||||
import { saveConfig, sendRPC, sendToMain } from '@/utils/dataSender'
|
||||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
import { T as $T } from '@/i18n/index'
|
||||
import $$db from '@/utils/db'
|
||||
@@ -204,6 +203,7 @@ import GalleryToolbar from './components/gallery/GalleryToolbar.vue'
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
import { getRawData } from '@/utils/common'
|
||||
import { showNotification } from '@/utils/notification'
|
||||
import { useStore } from '@/hooks/useStore'
|
||||
const images = ref<ImgInfo[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const imgInfo = reactive({
|
||||
@@ -229,7 +229,8 @@ const pasteStyleMap = {
|
||||
UBB: 'UBB',
|
||||
Custom: 'Custom'
|
||||
}
|
||||
const picBed = ref<IPicBedType[]>([])
|
||||
const store = useStore()
|
||||
const picBed = computed(() => store?.state.picBeds ?? [])
|
||||
const visiblePicBedList = computed(() => picBed.value.filter(item => item.visible))
|
||||
onBeforeRouteUpdate((to, from) => {
|
||||
if (from.name === 'gallery') {
|
||||
@@ -246,8 +247,8 @@ onBeforeMount(async () => {
|
||||
updateGallery()
|
||||
})
|
||||
})
|
||||
sendToMain(GET_PICBEDS)
|
||||
ipcRenderer.on(GET_PICBEDS, getPicBeds)
|
||||
store?.refreshPicBeds()
|
||||
store?.refreshAppConfig()
|
||||
updateGallery()
|
||||
|
||||
document.addEventListener('keydown', handleDetectShiftKey)
|
||||
@@ -279,13 +280,6 @@ const isAllSelected = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
function getPicBeds (event: IpcRendererEvent, picBeds: IPicBedType[]) {
|
||||
picBed.value = picBeds
|
||||
if (selectedPicBed.value.length === 0) return
|
||||
const visibleTypes = new Set(picBeds.filter(item => item.visible).map(item => item.type))
|
||||
selectedPicBed.value = selectedPicBed.value.filter(type => visibleTypes.has(type))
|
||||
}
|
||||
|
||||
function getGallery (): IGalleryItem[] {
|
||||
if (searchText.value || selectedPicBed.value.length > 0) {
|
||||
return images.value
|
||||
@@ -327,6 +321,12 @@ watch(() => filterList, () => {
|
||||
clearSelectedList()
|
||||
})
|
||||
|
||||
watch(picBed, (list) => {
|
||||
if (selectedPicBed.value.length === 0) return
|
||||
const visibleTypes = new Set(list.filter(item => item.visible).map(item => item.type))
|
||||
selectedPicBed.value = selectedPicBed.value.filter(type => visibleTypes.has(type))
|
||||
})
|
||||
|
||||
function handleChooseImage (val: CheckboxValueType, index: number) {
|
||||
if (val === true) {
|
||||
handleBarActive.value = true
|
||||
@@ -527,17 +527,25 @@ function toggleHandleBar () {
|
||||
}
|
||||
|
||||
async function handlePasteStyleChange (val: string) {
|
||||
saveConfig('settings.pasteStyle', val)
|
||||
await saveConfig('settings.pasteStyle', val)
|
||||
pasteStyle.value = val
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
ipcRenderer.removeAllListeners('updateGallery')
|
||||
ipcRenderer.removeListener(GET_PICBEDS, getPicBeds)
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
pasteStyle.value = (await getConfig('settings.pasteStyle')) || 'markdown'
|
||||
const applyAppConfig = () => {
|
||||
const settings = store?.state.appConfig?.settings ?? {}
|
||||
pasteStyle.value = settings.pasteStyle || 'markdown'
|
||||
}
|
||||
|
||||
watch(() => store?.state.appConfig, () => {
|
||||
applyAppConfig()
|
||||
}, { immediate: true })
|
||||
|
||||
onActivated(() => {
|
||||
applyAppConfig()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,583 @@
|
||||
<template>
|
||||
<div id="picgo-cloud-page">
|
||||
<div class="view-title">
|
||||
{{ $T('PICGO_CLOUD_TITLE') }}
|
||||
</div>
|
||||
<el-row>
|
||||
<el-col
|
||||
:span="20"
|
||||
:offset="2"
|
||||
>
|
||||
<div
|
||||
v-loading="isUserInfoLoading"
|
||||
element-loading-background="rgba(0, 0, 0, 0.6)"
|
||||
class="mt-[16px] rounded-[8px] border border-[rgba(255,255,255,0.06)] bg-[rgba(130,130,130,0.12)] p-[16px]"
|
||||
>
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
class="!mb-[12px]"
|
||||
type="error"
|
||||
show-icon
|
||||
:title="$T('PICGO_CLOUD_ERROR_TITLE')"
|
||||
:description="errorMessage"
|
||||
/>
|
||||
|
||||
<template v-if="isLoginInProgress">
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px] mb-[12px]">
|
||||
{{ $T('PICGO_CLOUD_LOGIN_IN_PROGRESS') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="userInfo">
|
||||
<div class="flex items-center gap-[12px] mb-[12px]">
|
||||
<div class="text-[16px] font-medium text-[#ddd] leading-[22px]">
|
||||
{{ $T('PICGO_CLOUD_LOGGED_IN_AS', { user: userInfo.user }) }}
|
||||
</div>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="handleOpenCloud"
|
||||
>
|
||||
{{ $T('PICGO_CLOUD_OPEN') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleLogout"
|
||||
>
|
||||
{{ $T('PICGO_CLOUD_LOGOUT') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-[12px] flex-wrap">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="isConfigSyncRunning"
|
||||
:disabled="isConfigSyncBusy"
|
||||
@click="handleConfigSyncStart"
|
||||
>
|
||||
{{ $T('PICGO_CLOUD_CONFIG_SYNC') }}
|
||||
</el-button>
|
||||
|
||||
<div class="flex items-center gap-[8px] text-[#bbb]">
|
||||
<span class="text-[12px] text-[#bbb] leading-[18px] shrink-0 whitespace-nowrap">
|
||||
{{ $T('PICGO_CLOUD_ENCRYPTION_MODE_LABEL') }}
|
||||
</span>
|
||||
<el-select
|
||||
v-model="encryptionMethodValue"
|
||||
size="small"
|
||||
class="w-[180px] shrink-0"
|
||||
:disabled="isEncryptionModeDisabled"
|
||||
>
|
||||
<el-option
|
||||
:label="$T('PICGO_CLOUD_ENCRYPTION_MODE_AUTO')"
|
||||
:value="IPicGoCloudEncryptionMethod.AUTO"
|
||||
/>
|
||||
<el-option
|
||||
:label="$T('PICGO_CLOUD_ENCRYPTION_MODE_SERVER')"
|
||||
:value="IPicGoCloudEncryptionMethod.SSE"
|
||||
/>
|
||||
<el-option
|
||||
:label="$T('PICGO_CLOUD_ENCRYPTION_MODE_E2E')"
|
||||
:value="IPicGoCloudEncryptionMethod.E2EE"
|
||||
/>
|
||||
</el-select>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
placement="top"
|
||||
:enterable="true"
|
||||
>
|
||||
<template #content>
|
||||
<div class="text-[12px] leading-[18px] max-w-[320px]">
|
||||
<div>{{ $T('PICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO') }}</div>
|
||||
<div>{{ $T('PICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER') }}</div>
|
||||
<div>{{ $T('PICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E') }}</div>
|
||||
<div class="mt-[6px]">
|
||||
<el-link
|
||||
type="primary"
|
||||
:underline="false"
|
||||
@click.stop.prevent="handleOpenDocs"
|
||||
>
|
||||
{{ $T('PICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC') }}
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="cursor-pointer text-[#999] hover:text-blue">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-[8px] text-[12px] text-[#999] leading-[18px]">
|
||||
{{ $T('PICGO_CLOUD_LAST_SYNC_TIME', { time: lastSyncedAtText || $T('PICGO_CLOUD_LAST_SYNC_TIME_NONE') }) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px] mb-[12px]">
|
||||
{{ $T('PICGO_CLOUD_NOT_LOGGED_IN') }}
|
||||
</div>
|
||||
<div class="flex gap-[8px]">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="isLoginInProgress"
|
||||
:disabled="isLoginInProgress || !hasAgreedToTermsAndPrivacy"
|
||||
@click="handleLogin"
|
||||
>
|
||||
{{ $T('PICGO_CLOUD_LOGIN') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isLoginInProgress"
|
||||
@click="handleDisposeLoginFlow"
|
||||
>
|
||||
{{ $T('PICGO_CLOUD_CANCEL_LOGIN') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-checkbox
|
||||
v-model="hasAgreedToTermsAndPrivacy"
|
||||
:disabled="isLoginInProgress"
|
||||
class="mt-[8px]"
|
||||
>
|
||||
<span class="text-[12px] text-[#bbb] leading-[18px]">
|
||||
{{ $T('PICGO_CLOUD_AGREE_PREFIX') }}
|
||||
<el-link
|
||||
type="primary"
|
||||
:underline="false"
|
||||
@click.stop.prevent="handleOpenTerms"
|
||||
>
|
||||
{{ $T('PICGO_CLOUD_TERMS_OF_SERVICE') }}
|
||||
</el-link>
|
||||
{{ $T('PICGO_CLOUD_AGREE_AND') }}
|
||||
<el-link
|
||||
type="primary"
|
||||
:underline="false"
|
||||
@click.stop.prevent="handleOpenPrivacy"
|
||||
>
|
||||
{{ $T('PICGO_CLOUD_PRIVACY_POLICY') }}
|
||||
</el-link>
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</template>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<ConfigSyncConflictDialog
|
||||
v-model="isConflictDialogVisible"
|
||||
:conflicts="configSyncConflicts"
|
||||
:confirm-loading="isApplyResolutionLoading"
|
||||
@abort="handleAbortConfigSync"
|
||||
@confirm="handleConfirmConfigSyncResolution"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { T as $T } from '@/i18n/index'
|
||||
import { useStore } from '@/hooks/useStore'
|
||||
import type { IPicGoCloudUserInfo } from '#/types/cloud'
|
||||
import { IPicGoCloudLoginStatus, IPicGoCloudRequestStatus } from '@/store'
|
||||
import { invokeRPC } from '@/utils/dataSender'
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
import { openURL } from '@/utils/common'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import ConfigSyncConflictDialog from '@/components/picgoCloud/ConfigSyncConflictDialog.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||
import {
|
||||
IPicGoCloudConfigSyncSessionStatus,
|
||||
IPicGoCloudConfigSyncToastType,
|
||||
IPicGoCloudEncryptionMethod,
|
||||
type IPicGoCloudConfigSyncRunResult,
|
||||
type IPicGoCloudConfigSyncState,
|
||||
type IPicGoCloudConfigSyncResolution
|
||||
} from '#/types/cloudConfigSync'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const userInfo = computed(() => store?.state.picgoCloud.userInfo)
|
||||
const userInfoStatus = computed(() => store?.state.picgoCloud.userInfoStatus ?? IPicGoCloudRequestStatus.IDLE)
|
||||
const userInfoError = computed(() => store?.state.picgoCloud.userInfoError)
|
||||
const loginStatus = computed(() => store?.state.picgoCloud.loginStatus ?? IPicGoCloudLoginStatus.IDLE)
|
||||
const loginError = computed(() => store?.state.picgoCloud.loginError)
|
||||
const hasAgreedToTermsAndPrivacy = computed({
|
||||
get: () => store?.state.picgoCloud.hasAgreedToTermsAndPrivacy ?? false,
|
||||
set: (value: boolean) => {
|
||||
store?.setPicGoCloudHasAgreedToTermsAndPrivacy(value)
|
||||
}
|
||||
})
|
||||
|
||||
const TERMS_URL = 'https://picgo.app/terms/'
|
||||
const PRIVACY_URL = 'https://picgo.app/privacy/'
|
||||
const DOC_URL = 'https://picgo.app/blog/2026/picgo-configuration-sync-release/'
|
||||
const CLOUD_URL = 'https://cloud.picgo.app'
|
||||
|
||||
const isUserInfoLoading = computed(() => userInfoStatus.value === IPicGoCloudRequestStatus.LOADING)
|
||||
const isLoginInProgress = computed(() => loginStatus.value === IPicGoCloudLoginStatus.IN_PROGRESS)
|
||||
|
||||
const errorMessage = computed(() => loginError.value || userInfoError.value)
|
||||
|
||||
const configSyncState = ref<IPicGoCloudConfigSyncState>({
|
||||
sessionStatus: IPicGoCloudConfigSyncSessionStatus.IDLE,
|
||||
encryptionMethod: undefined
|
||||
})
|
||||
const isConfigSyncStateLoading = ref(false)
|
||||
const isE2EPreferenceUpdating = ref(false)
|
||||
const isApplyResolutionLoading = ref(false)
|
||||
const isConflictDialogVisible = ref(false)
|
||||
|
||||
const configSyncSessionStatus = computed(() => configSyncState.value.sessionStatus)
|
||||
const isConfigSyncRunning = computed(() => configSyncSessionStatus.value === IPicGoCloudConfigSyncSessionStatus.SYNCING)
|
||||
const isConfigSyncBusy = computed(() => configSyncSessionStatus.value !== IPicGoCloudConfigSyncSessionStatus.IDLE)
|
||||
const configSyncConflicts = computed(() => configSyncState.value.conflicts ?? [])
|
||||
const lastSyncedAtText = computed<string | undefined>(() => {
|
||||
const raw = configSyncState.value.lastSyncedAt
|
||||
if (!raw) return undefined
|
||||
const date = dayjs(raw)
|
||||
if (!date.isValid()) return undefined
|
||||
return date.format('YYYY-MM-DD HH:mm:ss')
|
||||
})
|
||||
|
||||
const isEncryptionModeDisabled = computed(() => {
|
||||
if (isConfigSyncBusy.value) return true
|
||||
return isConfigSyncStateLoading.value || isE2EPreferenceUpdating.value
|
||||
})
|
||||
|
||||
const encryptionMethodValue = computed<IPicGoCloudEncryptionMethod>({
|
||||
get: () => configSyncState.value.encryptionMethod ?? IPicGoCloudEncryptionMethod.AUTO,
|
||||
set: (value: IPicGoCloudEncryptionMethod) => {
|
||||
handleSetEncryptionMethod(value)
|
||||
}
|
||||
})
|
||||
|
||||
const handleOpenTerms = () => {
|
||||
openURL(TERMS_URL)
|
||||
}
|
||||
|
||||
const handleOpenPrivacy = () => {
|
||||
openURL(PRIVACY_URL)
|
||||
}
|
||||
|
||||
const handleOpenDocs = () => {
|
||||
openURL(DOC_URL)
|
||||
}
|
||||
|
||||
const handleOpenCloud = () => {
|
||||
openURL(CLOUD_URL)
|
||||
}
|
||||
|
||||
const refreshAppStateAfterSync = async () => {
|
||||
if (!store) return
|
||||
await store.refreshAppConfig()
|
||||
await store.refreshPicBeds()
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
// First entry: only fetch when store is empty (undefined). Subsequent page entries read store.
|
||||
if (!store) return
|
||||
if (store.state.picgoCloud.userInfo !== undefined) {
|
||||
if (store.state.picgoCloud.userInfo) {
|
||||
loadConfigSyncState()
|
||||
}
|
||||
return
|
||||
}
|
||||
loadUserInfoAndMaybeHydrateCloudState()
|
||||
})
|
||||
|
||||
const loadUserInfoAndMaybeHydrateCloudState = async () => {
|
||||
await loadUserInfo()
|
||||
if (store?.state.picgoCloud.userInfo) {
|
||||
await loadConfigSyncState()
|
||||
}
|
||||
}
|
||||
|
||||
const loadUserInfo = async () => {
|
||||
if (!store) return
|
||||
|
||||
store.setPicGoCloudUserInfoStatus(IPicGoCloudRequestStatus.LOADING)
|
||||
store.setPicGoCloudUserInfoError(null)
|
||||
|
||||
const res = await invokeRPC<IPicGoCloudUserInfo | null>(IRPCActionType.PICGO_CLOUD_GET_USER_INFO)
|
||||
if (!res.success) {
|
||||
store.setPicGoCloudUserInfoStatus(IPicGoCloudRequestStatus.ERROR)
|
||||
store.setPicGoCloudUserInfoError(res.error)
|
||||
return
|
||||
}
|
||||
store.setPicGoCloudUserInfo(res.data)
|
||||
store.setPicGoCloudUserInfoStatus(IPicGoCloudRequestStatus.IDLE)
|
||||
}
|
||||
|
||||
const applyConfigSyncState = (state: IPicGoCloudConfigSyncState) => {
|
||||
configSyncState.value = state
|
||||
|
||||
if (state.sessionStatus === IPicGoCloudConfigSyncSessionStatus.CONFLICT) {
|
||||
isConflictDialogVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const loadConfigSyncState = async () => {
|
||||
isConfigSyncStateLoading.value = true
|
||||
const res = await invokeRPC<IPicGoCloudConfigSyncState>(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_GET_STATE)
|
||||
isConfigSyncStateLoading.value = false
|
||||
|
||||
if (!res.success) {
|
||||
ElMessage.error(res.error)
|
||||
return
|
||||
}
|
||||
|
||||
applyConfigSyncState(res.data)
|
||||
}
|
||||
|
||||
const showConfigSyncToast = (toastType: IPicGoCloudConfigSyncToastType, message: string) => {
|
||||
if (toastType === IPicGoCloudConfigSyncToastType.SUCCESS) {
|
||||
ElMessage.success(message)
|
||||
return
|
||||
}
|
||||
if (toastType === IPicGoCloudConfigSyncToastType.WARNING) {
|
||||
ElMessage.warning(message)
|
||||
return
|
||||
}
|
||||
if (toastType === IPicGoCloudConfigSyncToastType.ERROR) {
|
||||
ElMessage.error(message)
|
||||
return
|
||||
}
|
||||
ElMessage.info(message)
|
||||
}
|
||||
|
||||
const promptRestartIfNeeded = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$T('PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE'),
|
||||
$T('PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: $T('PICGO_CLOUD_CONFIG_SYNC_RESTART_NOW'),
|
||||
cancelButtonText: $T('PICGO_CLOUD_CONFIG_SYNC_RESTART_LATER'),
|
||||
closeOnClickModal: false,
|
||||
closeOnPressEscape: false
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const res = await invokeRPC<void>(IRPCActionType.RELOAD_APP)
|
||||
if (!res.success) {
|
||||
ElMessage.error(res.error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfigSyncStart = async () => {
|
||||
if (isConfigSyncBusy.value) return
|
||||
|
||||
// Optimistically switch to SYNCING for immediate loading feedback.
|
||||
// The main process still owns the source-of-truth session state.
|
||||
configSyncState.value = {
|
||||
...configSyncState.value,
|
||||
sessionStatus: IPicGoCloudConfigSyncSessionStatus.SYNCING,
|
||||
conflicts: undefined
|
||||
}
|
||||
ElMessage.info($T('PICGO_CLOUD_CONFIG_SYNC_STARTING'))
|
||||
|
||||
const res = await invokeRPC<IPicGoCloudConfigSyncRunResult>(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_START)
|
||||
if (!res.success) {
|
||||
ElMessage.error(res.error)
|
||||
await loadConfigSyncState()
|
||||
return
|
||||
}
|
||||
|
||||
const runRes = res.data
|
||||
applyConfigSyncState(runRes.state)
|
||||
showConfigSyncToast(runRes.toastType, runRes.message)
|
||||
|
||||
if (runRes.authInvalidated && store) {
|
||||
store.setPicGoCloudUserInfo(null)
|
||||
store.setPicGoCloudUserInfoError(null)
|
||||
store.setPicGoCloudUserInfoStatus(IPicGoCloudRequestStatus.IDLE)
|
||||
}
|
||||
|
||||
if (runRes.shouldShowRestartPrompt) {
|
||||
await refreshAppStateAfterSync()
|
||||
await promptRestartIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
const handleAbortConfigSync = async () => {
|
||||
const res = await invokeRPC<IPicGoCloudConfigSyncState>(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_ABORT)
|
||||
if (!res.success) {
|
||||
ElMessage.error(res.error)
|
||||
return
|
||||
}
|
||||
applyConfigSyncState(res.data)
|
||||
isConflictDialogVisible.value = false
|
||||
ElMessage.warning($T('PICGO_CLOUD_CONFIG_SYNC_ABORTED'))
|
||||
}
|
||||
|
||||
const handleConfirmConfigSyncResolution = async (resolution: IPicGoCloudConfigSyncResolution) => {
|
||||
isApplyResolutionLoading.value = true
|
||||
const res = await invokeRPC<IPicGoCloudConfigSyncRunResult>(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_APPLY_RESOLUTION, resolution)
|
||||
isApplyResolutionLoading.value = false
|
||||
|
||||
if (!res.success) {
|
||||
ElMessage.error(res.error)
|
||||
return
|
||||
}
|
||||
|
||||
const runRes = res.data
|
||||
applyConfigSyncState(runRes.state)
|
||||
showConfigSyncToast(runRes.toastType, runRes.message)
|
||||
|
||||
if (runRes.authInvalidated && store) {
|
||||
isConflictDialogVisible.value = false
|
||||
store.setPicGoCloudUserInfo(null)
|
||||
store.setPicGoCloudUserInfoError(null)
|
||||
store.setPicGoCloudUserInfoStatus(IPicGoCloudRequestStatus.IDLE)
|
||||
return
|
||||
}
|
||||
|
||||
if (runRes.shouldShowRestartPrompt) {
|
||||
isConflictDialogVisible.value = false
|
||||
await refreshAppStateAfterSync()
|
||||
await promptRestartIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSetEncryptionMethod = async (nextMode: IPicGoCloudEncryptionMethod) => {
|
||||
if (isE2EPreferenceUpdating.value) return
|
||||
if (isConfigSyncBusy.value) return
|
||||
|
||||
const currentMode = encryptionMethodValue.value
|
||||
if (nextMode === currentMode) return
|
||||
|
||||
const previousEncryptionMethod = configSyncState.value.encryptionMethod
|
||||
|
||||
// Only turning on E2E needs a warning confirmation.
|
||||
// If the user cancels, we persist "server-side encryption" (`encryptionMethod='sse'`) per product requirement,
|
||||
// rather than leaving it at AUTO.
|
||||
let modeToPersist = nextMode
|
||||
if (nextMode === IPicGoCloudEncryptionMethod.E2EE) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$T('PICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE'),
|
||||
$T('PICGO_CLOUD_E2E_ENABLE_WARNING_TITLE'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: $T('CONFIRM'),
|
||||
cancelButtonText: $T('CANCEL'),
|
||||
closeOnClickModal: false,
|
||||
closeOnPressEscape: false
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
modeToPersist = IPicGoCloudEncryptionMethod.SSE
|
||||
}
|
||||
}
|
||||
|
||||
// Optimistically update local UI state so the dropdown reflects the user's selection immediately.
|
||||
configSyncState.value = {
|
||||
...configSyncState.value,
|
||||
encryptionMethod: modeToPersist
|
||||
}
|
||||
|
||||
isE2EPreferenceUpdating.value = true
|
||||
const res = await invokeRPC<IPicGoCloudEncryptionMethod | undefined>(
|
||||
IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_SET_E2E_PREFERENCE,
|
||||
modeToPersist
|
||||
)
|
||||
isE2EPreferenceUpdating.value = false
|
||||
|
||||
if (!res.success) {
|
||||
// Restore previous selection on failure.
|
||||
configSyncState.value = {
|
||||
...configSyncState.value,
|
||||
encryptionMethod: previousEncryptionMethod
|
||||
}
|
||||
ElMessage.error(res.error)
|
||||
return
|
||||
}
|
||||
|
||||
configSyncState.value = {
|
||||
...configSyncState.value,
|
||||
encryptionMethod: res.data
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!store) return
|
||||
if (!hasAgreedToTermsAndPrivacy.value) return
|
||||
store.setPicGoCloudLoginStatus(IPicGoCloudLoginStatus.IN_PROGRESS)
|
||||
store.setPicGoCloudLoginError(null)
|
||||
|
||||
const res = await invokeRPC<IPicGoCloudUserInfo>(IRPCActionType.PICGO_CLOUD_LOGIN)
|
||||
if (!res.success) {
|
||||
store.setPicGoCloudLoginError(res.error)
|
||||
store.setPicGoCloudLoginStatus(IPicGoCloudLoginStatus.IDLE)
|
||||
return
|
||||
}
|
||||
|
||||
store.setPicGoCloudUserInfo(res.data)
|
||||
store.setPicGoCloudUserInfoStatus(IPicGoCloudRequestStatus.IDLE)
|
||||
store.setPicGoCloudUserInfoError(null)
|
||||
store.setPicGoCloudLoginStatus(IPicGoCloudLoginStatus.IDLE)
|
||||
await loadConfigSyncState()
|
||||
}
|
||||
|
||||
const handleDisposeLoginFlow = async () => {
|
||||
if (!store) return
|
||||
const res = await invokeRPC<boolean>(IRPCActionType.PICGO_CLOUD_DISPOSE_LOGIN_FLOW)
|
||||
if (!res.success) {
|
||||
store.setPicGoCloudLoginError(res.error)
|
||||
}
|
||||
store.setPicGoCloudLoginStatus(IPicGoCloudLoginStatus.IDLE)
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
if (!store) return
|
||||
const res = await invokeRPC<boolean>(IRPCActionType.PICGO_CLOUD_LOGOUT)
|
||||
if (!res.success) {
|
||||
store.setPicGoCloudLoginError(res.error)
|
||||
return
|
||||
}
|
||||
store.setPicGoCloudUserInfo(null)
|
||||
store.setPicGoCloudLoginError(null)
|
||||
|
||||
// Clear config-sync related UI state after logout.
|
||||
configSyncState.value = {
|
||||
sessionStatus: IPicGoCloudConfigSyncSessionStatus.IDLE,
|
||||
encryptionMethod: undefined
|
||||
}
|
||||
isConflictDialogVisible.value = false
|
||||
}
|
||||
|
||||
const pollTimer = ref<number | null>(null)
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollTimer.value === null) return
|
||||
window.clearInterval(pollTimer.value)
|
||||
pollTimer.value = null
|
||||
}
|
||||
|
||||
watch(isConfigSyncRunning, (running) => {
|
||||
if (!running) {
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
if (pollTimer.value !== null) return
|
||||
pollTimer.value = window.setInterval(() => {
|
||||
loadConfigSyncState()
|
||||
}, 1500)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'PicGoCloudPage'
|
||||
}
|
||||
</script>
|
||||
@@ -49,14 +49,14 @@ import { Reading } from '@element-plus/icons-vue'
|
||||
import { IConfig } from 'picgo'
|
||||
import { T as $T } from '@/i18n/index'
|
||||
import { enforceNumber, isLinux } from '~/universal/utils/common'
|
||||
import { onBeforeMount, reactive, ref } from 'vue'
|
||||
import { getConfig } from '@/utils/dataSender'
|
||||
import { computed, onBeforeMount, reactive, ref, watch, type DeepReadonly } from 'vue'
|
||||
import ButtonAreaSettings from './components/settings/buttonArea/ButtonAreaSettings.vue'
|
||||
import SwitchAreaSettings from './components/settings/switchArea/SwitchAreaSettings.vue'
|
||||
import CustomAreaSettings from './components/settings/customArea/CustomAreaSettings.vue'
|
||||
import SelectAreaSettings from './components/settings/selectArea/SelectAreaSettings.vue'
|
||||
import { openURL } from '@/utils/common'
|
||||
import { IStartupMode } from '#/types/enum'
|
||||
import { useStore } from '@/hooks/useStore'
|
||||
|
||||
const form = reactive<ISettingForm>({
|
||||
showUpdateTip: false,
|
||||
@@ -71,7 +71,7 @@ const form = reactive<ISettingForm>({
|
||||
autoCopyUrl: true,
|
||||
checkBetaUpdate: true,
|
||||
useBuiltinClipboard: false,
|
||||
language: 'zh-CN',
|
||||
language: 'en',
|
||||
logFileSizeLimit: 10,
|
||||
encodeOutputURL: true,
|
||||
showDockIcon: true,
|
||||
@@ -88,41 +88,50 @@ const form = reactive<ISettingForm>({
|
||||
})
|
||||
|
||||
const proxy = ref('')
|
||||
const store = useStore()
|
||||
const appConfig = computed(() => store?.state.appConfig ?? null)
|
||||
|
||||
onBeforeMount(() => {
|
||||
initData()
|
||||
store?.refreshAppConfig()
|
||||
})
|
||||
|
||||
async function initData () {
|
||||
const config = (await getConfig<IConfig>())!
|
||||
if (config !== undefined) {
|
||||
const settings = config.settings || {}
|
||||
const picBed = config.picBed
|
||||
form.showUpdateTip = settings.showUpdateTip || false
|
||||
form.autoStart = settings.autoStart || false
|
||||
form.rename = settings.rename || false
|
||||
form.autoRename = settings.autoRename || false
|
||||
form.uploadNotification = settings.uploadNotification || false
|
||||
form.notificationSound = settings.notificationSound === undefined ? true : settings.notificationSound
|
||||
form.miniWindowOnTop = settings.miniWindowOnTop || false
|
||||
form.logLevel = initLogLevel(settings.logLevel || [])
|
||||
form.autoCopyUrl = settings.autoCopyUrl === undefined ? true : settings.autoCopyUrl
|
||||
form.checkBetaUpdate = settings.checkBetaUpdate === undefined ? true : settings.checkBetaUpdate
|
||||
form.useBuiltinClipboard = settings.useBuiltinClipboard === undefined ? false : settings.useBuiltinClipboard
|
||||
form.language = settings.language ?? 'zh-CN'
|
||||
form.encodeOutputURL = settings.encodeOutputURL === undefined ? false : settings.encodeOutputURL
|
||||
form.customLink = settings.customLink || '$url'
|
||||
form.npmProxy = settings.npmProxy || ''
|
||||
form.npmRegistry = settings.npmRegistry || ''
|
||||
proxy.value = picBed.proxy || ''
|
||||
form.server = settings.server
|
||||
form.logFileSizeLimit = enforceNumber(settings.logFileSizeLimit) || 10
|
||||
form.showDockIcon = settings.showDockIcon === undefined ? true : settings.showDockIcon
|
||||
form.showMenubarIcon = settings.showMenubarIcon === undefined ? true : settings.showMenubarIcon
|
||||
form.startupMode = settings.startupMode || (isLinux ? IStartupMode.SHOW_MINI_WINDOW : IStartupMode.HIDE)
|
||||
const applyAppConfig = (config: DeepReadonly<IConfig> | null) => {
|
||||
if (!config) return
|
||||
const settings = config.settings || {}
|
||||
const picBed = config.picBed
|
||||
form.showUpdateTip = settings.showUpdateTip || false
|
||||
form.autoStart = settings.autoStart || false
|
||||
form.rename = settings.rename || false
|
||||
form.autoRename = settings.autoRename || false
|
||||
form.uploadNotification = settings.uploadNotification || false
|
||||
form.notificationSound = settings.notificationSound === undefined ? true : settings.notificationSound
|
||||
form.miniWindowOnTop = settings.miniWindowOnTop || false
|
||||
form.logLevel = initLogLevel(settings.logLevel ? [...settings.logLevel] : [])
|
||||
form.autoCopyUrl = settings.autoCopyUrl === undefined ? true : settings.autoCopyUrl
|
||||
form.checkBetaUpdate = settings.checkBetaUpdate === undefined ? true : settings.checkBetaUpdate
|
||||
form.useBuiltinClipboard = settings.useBuiltinClipboard === undefined ? false : settings.useBuiltinClipboard
|
||||
form.language = settings.language ?? 'en'
|
||||
form.encodeOutputURL = settings.encodeOutputURL === undefined ? false : settings.encodeOutputURL
|
||||
form.customLink = settings.customLink || '$url'
|
||||
form.npmProxy = settings.npmProxy || ''
|
||||
form.npmRegistry = settings.npmRegistry || ''
|
||||
proxy.value = picBed.proxy || ''
|
||||
const server = settings.server ?? {}
|
||||
form.server = {
|
||||
port: enforceNumber(server.port ?? 36677) || 36677,
|
||||
host: server.host || '127.0.0.1',
|
||||
enable: server.enable ?? true
|
||||
}
|
||||
form.logFileSizeLimit = enforceNumber(settings.logFileSizeLimit ?? 10) || 10
|
||||
form.showDockIcon = settings.showDockIcon === undefined ? true : settings.showDockIcon
|
||||
form.showMenubarIcon = settings.showMenubarIcon === undefined ? true : settings.showMenubarIcon
|
||||
form.startupMode = settings.startupMode || (isLinux ? IStartupMode.SHOW_MINI_WINDOW : IStartupMode.HIDE)
|
||||
}
|
||||
|
||||
watch(appConfig, (config) => {
|
||||
applyAppConfig(config)
|
||||
}, { immediate: true })
|
||||
|
||||
function initLogLevel (logLevel: string | string[]) {
|
||||
if (!Array.isArray(logLevel)) {
|
||||
if (logLevel && logLevel.length > 0) {
|
||||
@@ -135,7 +144,7 @@ function initLogLevel (logLevel: string | string[]) {
|
||||
}
|
||||
|
||||
function goConfigPage () {
|
||||
openURL('https://picgo.github.io/PicGo-Doc/guide/config.html#picgo设置')
|
||||
openURL('https://docs.picgo.app/gui/guide/config#picgo-setting')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -461,7 +461,7 @@ function _getSearchResult (val: string) {
|
||||
})
|
||||
.filter((item: INPMSearchResultObject) => {
|
||||
// filter out fake picgo plugins from picgo.net
|
||||
if (item.package.description.includes('picgo.net') || item.package.description.includes('PicGo官方')) {
|
||||
if (item.package.description?.includes('picgo.net') || item.package.description?.includes('PicGo官方')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -96,8 +96,8 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useIPC } from '@/hooks/useIPC'
|
||||
import { sendRPC, triggerRPC } from '@/utils/dataSender'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { sendRPC, invokeRPC } from '@/utils/dataSender'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { IToolboxItemType, IToolboxItemCheckStatus, IRPCActionType } from '~/universal/types/enum'
|
||||
import { T as $T } from '@/i18n'
|
||||
@@ -194,7 +194,12 @@ const handleFix = async () => {
|
||||
const status = fixList[key as IToolboxItemType].status
|
||||
return status === IToolboxItemCheckStatus.ERROR && !fixList[key as IToolboxItemType].hasNoFixMethod
|
||||
}).map(async key => {
|
||||
return triggerRPC<IToolboxCheckRes>(IRPCActionType.TOOLBOX_CHECK_FIX, key as IToolboxItemType)
|
||||
const res = await invokeRPC<IToolboxCheckRes>(IRPCActionType.TOOLBOX_CHECK_FIX, key as IToolboxItemType)
|
||||
if (!res.success) {
|
||||
ElMessage.warning(res.error)
|
||||
return null
|
||||
}
|
||||
return res.data
|
||||
}))
|
||||
|
||||
fixRes.filter(item => item !== null).forEach(item => {
|
||||
|
||||
@@ -103,16 +103,15 @@
|
||||
import { T as $T } from '@/i18n'
|
||||
import $bus from '@/utils/bus'
|
||||
import { getFilePath } from '@/utils/common'
|
||||
import { getConfig, saveConfig, sendToMain } from '@/utils/dataSender'
|
||||
import { saveConfig, sendToMain } from '@/utils/dataSender'
|
||||
import { CaretBottom, UploadFilled } from '@element-plus/icons-vue'
|
||||
import {
|
||||
IpcRendererEvent,
|
||||
ipcRenderer
|
||||
} from 'electron'
|
||||
import { ElMessage as $message, ElMessageBox } from 'element-plus'
|
||||
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { computed, onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import {
|
||||
GET_PICBEDS,
|
||||
LOG_INVALID_URL_LINES,
|
||||
SHOW_INPUT_BOX,
|
||||
SHOW_INPUT_BOX_RESPONSE,
|
||||
@@ -123,14 +122,30 @@ import {
|
||||
isUrl,
|
||||
parseNewlineSeparatedUrls
|
||||
} from '~/universal/utils/common'
|
||||
import { useStore } from '@/hooks/useStore'
|
||||
const dragover = ref(false)
|
||||
const progress = ref(0)
|
||||
const showProgress = ref(false)
|
||||
const showError = ref(false)
|
||||
const pasteStyle = ref('')
|
||||
const picBed = ref<IPicBedType[]>([])
|
||||
const picBedName = ref('')
|
||||
const configName = ref('')
|
||||
const store = useStore()
|
||||
const currentPicBedType = computed(() => {
|
||||
const config = store?.state.appConfig
|
||||
return config?.picBed?.uploader || config?.picBed?.current || store?.state.defaultPicBed || 'smms'
|
||||
})
|
||||
const picBedName = computed(() => {
|
||||
const currentType = currentPicBedType.value
|
||||
const list = store?.state.picBeds ?? []
|
||||
const match = list.find(item => item.type === currentType)
|
||||
return match?.name || currentType
|
||||
})
|
||||
const configName = computed(() => {
|
||||
const configEntry = store?.state.appConfig?.picBed?.[currentPicBedType.value] as IStringKeyMap | undefined
|
||||
if (configEntry && typeof configEntry._configName === 'string') {
|
||||
return configEntry._configName
|
||||
}
|
||||
return 'Default'
|
||||
})
|
||||
const $confirm = ElMessageBox.confirm
|
||||
onBeforeMount(() => {
|
||||
ipcRenderer.on('uploadProgress', (event: IpcRendererEvent, _progress: number) => {
|
||||
@@ -142,13 +157,11 @@ onBeforeMount(() => {
|
||||
showError.value = true
|
||||
}
|
||||
})
|
||||
getPasteStyle()
|
||||
getDefaultPicBed()
|
||||
store?.refreshAppConfig()
|
||||
store?.refreshPicBeds()
|
||||
ipcRenderer.on('syncPicBed', () => {
|
||||
getDefaultPicBed()
|
||||
store?.refreshAppConfig()
|
||||
})
|
||||
sendToMain(GET_PICBEDS)
|
||||
ipcRenderer.on(GET_PICBEDS, getPicBeds)
|
||||
$bus.on(SHOW_INPUT_BOX_RESPONSE, handleInputBoxValue)
|
||||
})
|
||||
|
||||
@@ -170,7 +183,6 @@ onBeforeUnmount(() => {
|
||||
$bus.off(SHOW_INPUT_BOX_RESPONSE)
|
||||
ipcRenderer.removeAllListeners('uploadProgress')
|
||||
ipcRenderer.removeAllListeners('syncPicBed')
|
||||
ipcRenderer.removeListener(GET_PICBEDS, getPicBeds)
|
||||
})
|
||||
|
||||
async function onDrop (e: DragEvent) {
|
||||
@@ -280,10 +292,15 @@ function ipcSendFiles (files: FileList) {
|
||||
sendToMain('uploadChoosedFiles', sendFiles)
|
||||
}
|
||||
|
||||
async function getPasteStyle () {
|
||||
pasteStyle.value = await getConfig('settings.pasteStyle') || 'markdown'
|
||||
const applyAppConfig = () => {
|
||||
const settings = store?.state.appConfig?.settings ?? {}
|
||||
pasteStyle.value = settings.pasteStyle || 'markdown'
|
||||
}
|
||||
|
||||
watch(() => store?.state.appConfig, () => {
|
||||
applyAppConfig()
|
||||
}, { immediate: true })
|
||||
|
||||
function handlePasteStyleChange (val: string | number | boolean | undefined) {
|
||||
saveConfig({
|
||||
'settings.pasteStyle': val
|
||||
@@ -329,22 +346,6 @@ async function handleInputBoxValue (val: string) {
|
||||
await uploadUrls(urls, invalidLines, () => openUrlInputBox(val))
|
||||
}
|
||||
|
||||
async function getDefaultPicBed () {
|
||||
const currentPicBed = await getConfig<string>('picBed.current')
|
||||
const currentConfigName = await getConfig<string>(`picBed.${currentPicBed}._configName`) || 'Default'
|
||||
picBed.value.forEach(item => {
|
||||
if (item.type === currentPicBed) {
|
||||
picBedName.value = item.name
|
||||
}
|
||||
})
|
||||
configName.value = currentConfigName
|
||||
}
|
||||
|
||||
function getPicBeds (event: IpcRendererEvent, picBeds: IPicBedType[]) {
|
||||
picBed.value = picBeds
|
||||
getDefaultPicBed()
|
||||
}
|
||||
|
||||
async function handleChangePicBed () {
|
||||
sendToMain(SHOW_UPLOAD_PAGE_MENU)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
>
|
||||
<div
|
||||
:class="`config-item ${defaultConfigId === item._id ? 'selected' : ''}`"
|
||||
@click="() => selectItem(item._id)"
|
||||
@click="() => selectItem(item._configName)"
|
||||
>
|
||||
<div class="config-name">
|
||||
{{ item._configName }}
|
||||
@@ -39,20 +39,20 @@
|
||||
<div class="operation-container">
|
||||
<el-icon
|
||||
class="el-icon-edit"
|
||||
@click="openEditPage(item._id)"
|
||||
@click.stop="openEditPage(item._id)"
|
||||
>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="el-icon-copy"
|
||||
@click.stop="() => copyConfig(item._id)"
|
||||
@click.stop="() => copyConfig(item._configName)"
|
||||
>
|
||||
<DocumentCopy />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="el-icon-delete"
|
||||
:class="curConfigList.length <= 1 ? 'disabled' : ''"
|
||||
@click.stop="() => deleteConfig(item._id)"
|
||||
@click.stop="() => deleteConfig(item._configName)"
|
||||
>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
@@ -96,28 +96,41 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Delete, DocumentCopy, Edit, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { saveConfig, triggerRPC } from '@/utils/dataSender'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { saveConfig, invokeRPC } from '@/utils/dataSender'
|
||||
import { showNotification } from '@/utils/notification'
|
||||
import dayjs from 'dayjs'
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
import { T as $T } from '@/i18n/index'
|
||||
import { useRouter, useRoute, onBeforeRouteUpdate } from 'vue-router'
|
||||
import { onBeforeMount, ref } from 'vue'
|
||||
import { computed, onBeforeMount, ref, watch } from 'vue'
|
||||
import { PICBEDS_PAGE, UPLOADER_CONFIG_PAGE } from '@/router/config'
|
||||
import { useStore } from '@/hooks/useStore'
|
||||
const $router = useRouter()
|
||||
const $route = useRoute()
|
||||
|
||||
const type = ref('')
|
||||
const curConfigList = ref<IStringKeyMap[]>([])
|
||||
const curConfigList = ref<IUploaderConfigListItem[]>([])
|
||||
const defaultConfigId = ref('')
|
||||
const store = useStore()
|
||||
const appConfig = computed(() => store?.state.appConfig ?? null)
|
||||
const $confirm = ElMessageBox.confirm
|
||||
const $prompt = ElMessageBox.prompt
|
||||
|
||||
async function selectItem (id: string) {
|
||||
await triggerRPC<void>(IRPCActionType.SELECT_UPLOADER, type.value, id)
|
||||
defaultConfigId.value = id
|
||||
type MessageBoxAction = 'confirm' | 'cancel' | 'close'
|
||||
|
||||
interface PromptBoxState {
|
||||
inputValue?: string
|
||||
confirmButtonLoading?: boolean
|
||||
}
|
||||
|
||||
async function selectItem (configName: string) {
|
||||
const res = await invokeRPC<string>(IRPCActionType.SELECT_UPLOADER, type.value, configName)
|
||||
if (!res.success) {
|
||||
ElMessage.warning(res.error)
|
||||
return
|
||||
}
|
||||
defaultConfigId.value = res.data
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate((to, from, next) => {
|
||||
@@ -131,12 +144,22 @@ onBeforeRouteUpdate((to, from, next) => {
|
||||
onBeforeMount(() => {
|
||||
type.value = $route.params.type as string
|
||||
getCurrentConfigList()
|
||||
store?.refreshAppConfig()
|
||||
})
|
||||
|
||||
watch(appConfig, () => {
|
||||
if (!type.value) return
|
||||
getCurrentConfigList()
|
||||
})
|
||||
|
||||
async function getCurrentConfigList () {
|
||||
const configList = await triggerRPC<IUploaderConfigItem>(IRPCActionType.GET_PICBED_CONFIG_LIST, type.value)
|
||||
curConfigList.value = configList?.configList ?? []
|
||||
defaultConfigId.value = configList?.defaultId ?? ''
|
||||
const configList = await invokeRPC<IUploaderConfigItem>(IRPCActionType.GET_PICBED_CONFIG_LIST, type.value)
|
||||
if (!configList.success) {
|
||||
ElMessage.warning(configList.error)
|
||||
return
|
||||
}
|
||||
curConfigList.value = configList.data?.configList ?? []
|
||||
defaultConfigId.value = configList.data?.defaultId ?? ''
|
||||
}
|
||||
|
||||
function openEditPage (configId: string) {
|
||||
@@ -156,7 +179,7 @@ function formatTime (time: number): string {
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
function deleteConfig (id: string) {
|
||||
function deleteConfig (configName: string) {
|
||||
if (curConfigList.value.length <= 1) {
|
||||
return
|
||||
}
|
||||
@@ -165,26 +188,49 @@ function deleteConfig (id: string) {
|
||||
cancelButtonText: $T('CANCEL'),
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await triggerRPC<IUploaderConfigItem | undefined>(IRPCActionType.DELETE_PICBED_CONFIG, type.value, id)
|
||||
if (!res) return
|
||||
curConfigList.value = res.configList
|
||||
defaultConfigId.value = res.defaultId
|
||||
const res = await invokeRPC<IUploaderConfigItem>(IRPCActionType.DELETE_PICBED_CONFIG, type.value, configName)
|
||||
if (!res.success) {
|
||||
ElMessage.warning(res.error)
|
||||
return
|
||||
}
|
||||
curConfigList.value = res.data.configList
|
||||
defaultConfigId.value = res.data.defaultId
|
||||
}).catch((e) => {
|
||||
console.log(e)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
function copyConfig (id: string) {
|
||||
$confirm($T('TIPS_COPY_UPLOADER_CONFIG'), $T('TIPS_NOTICE'), {
|
||||
function copyConfig (configName: string) {
|
||||
$prompt($T('TIPS_COPY_UPLOADER_CONFIG'), $T('TIPS_NOTICE'), {
|
||||
confirmButtonText: $T('CONFIRM'),
|
||||
cancelButtonText: $T('CANCEL'),
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await triggerRPC<IUploaderConfigItem | undefined>(IRPCActionType.COPY_UPLOADER_CONFIG, type.value, id)
|
||||
if (!res) return
|
||||
curConfigList.value = res.configList
|
||||
defaultConfigId.value = res.defaultId
|
||||
inputValue: `${configName} - Copy`,
|
||||
inputPlaceholder: $T('UPLOADER_CONFIG_PLACEHOLDER'),
|
||||
beforeClose: async (action: MessageBoxAction, instance: PromptBoxState, done: () => void) => {
|
||||
if (action !== 'confirm') {
|
||||
done()
|
||||
return
|
||||
}
|
||||
|
||||
const newConfigName = String(instance.inputValue ?? '').trim()
|
||||
if (!newConfigName) {
|
||||
ElMessage.warning($T('TIPS_UPLOADER_CONFIG_NAME_EMPTY'))
|
||||
return
|
||||
}
|
||||
|
||||
instance.confirmButtonLoading = true
|
||||
const res = await invokeRPC<IUploaderConfigItem>(IRPCActionType.COPY_UPLOADER_CONFIG, type.value, configName, newConfigName)
|
||||
instance.confirmButtonLoading = false
|
||||
|
||||
if (!res.success) {
|
||||
ElMessage.warning(res.error)
|
||||
return
|
||||
}
|
||||
curConfigList.value = res.data.configList
|
||||
defaultConfigId.value = res.data.defaultId
|
||||
done()
|
||||
}
|
||||
}).catch((e) => {
|
||||
console.log(e)
|
||||
return true
|
||||
|
||||
@@ -40,7 +40,7 @@ import pkg from 'root/package.json'
|
||||
import { compare } from 'compare-versions'
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
import { useVModel } from '@/hooks/useVModel'
|
||||
import { triggerRPC } from '@/utils/dataSender'
|
||||
import { invokeRPC } from '@/utils/dataSender'
|
||||
import { openURL } from '@/utils/common'
|
||||
|
||||
interface IProps {
|
||||
@@ -75,12 +75,12 @@ watch(() => dialogVisible.value, (value) => {
|
||||
})
|
||||
|
||||
async function checkUpdate () {
|
||||
const version = await triggerRPC<string>(IRPCActionType.GET_LATEST_VERSION, checkBetaUpdate.value)
|
||||
if (version) {
|
||||
latestVersion.value = version
|
||||
} else {
|
||||
latestVersion.value = $T('TIPS_NETWORK_ERROR')
|
||||
const res = await invokeRPC<string>(IRPCActionType.GET_LATEST_VERSION, checkBetaUpdate.value)
|
||||
if (!res.success) {
|
||||
latestVersion.value = res.error || $T('TIPS_NETWORK_ERROR')
|
||||
return
|
||||
}
|
||||
latestVersion.value = res.data
|
||||
}
|
||||
|
||||
function confirmCheckVersion () {
|
||||
|
||||
@@ -97,8 +97,6 @@ function confirmCustomLink () {
|
||||
dialogVisible.value = false
|
||||
sendToMain('updateCustomLink')
|
||||
updateProps()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { GET_PICBEDS } from '#/events/constants'
|
||||
import { saveConfig, sendToMain } from '@/utils/dataSender'
|
||||
import { useIPCOn } from '@/hooks/useIPC'
|
||||
import { useVModel } from '@/hooks/useVModel'
|
||||
import { IpcRendererEvent } from 'electron'
|
||||
|
||||
interface IProps {
|
||||
showPicBedList: string[]
|
||||
@@ -51,7 +52,7 @@ onBeforeMount(() => {
|
||||
useIPCOn(GET_PICBEDS, getPicBeds)
|
||||
})
|
||||
|
||||
function getPicBeds (event: Event, picBeds: IPicBedType[]) {
|
||||
function getPicBeds (event: IpcRendererEvent, picBeds: IPicBedType[]) {
|
||||
picBed.value = picBeds
|
||||
showPicBedList.value = picBed.value.map(item => {
|
||||
if (item.visible) {
|
||||
|
||||
@@ -43,36 +43,48 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
import { ref, onBeforeMount } from 'vue'
|
||||
import { computed, ref, onBeforeMount, watch } from 'vue'
|
||||
import { T as $T } from '@/i18n/index'
|
||||
import { sendToMain, triggerRPC } from '@/utils/dataSender'
|
||||
import { sendToMain, invokeRPC } from '@/utils/dataSender'
|
||||
import { showNotification } from '@/utils/notification'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ConfigForm from '@/components/ConfigForm.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
// import mixin from '@/utils/ConfirmButtonMixin'
|
||||
import {
|
||||
IpcRendererEvent
|
||||
} from 'electron'
|
||||
import { GET_PICBED_CONFIG } from '~/universal/events/constants'
|
||||
import { useIPCOn } from '@/hooks/useIPC'
|
||||
import { useStore } from '@/hooks/useStore'
|
||||
import { PICBEDS_PAGE } from '@/router/config'
|
||||
const type = ref('')
|
||||
const config = ref<IPicGoPluginConfig[]>([])
|
||||
const picBedName = ref('')
|
||||
const $route = useRoute()
|
||||
const $router = useRouter()
|
||||
const $configForm = ref<InstanceType<typeof ConfigForm> | null>(null)
|
||||
const store = useStore()
|
||||
const appConfig = computed(() => store?.state.appConfig ?? null)
|
||||
type.value = $route.params.type as string
|
||||
|
||||
useIPCOn(GET_PICBED_CONFIG, getPicBeds)
|
||||
|
||||
onBeforeMount(() => {
|
||||
sendToMain(GET_PICBED_CONFIG, $route.params.type)
|
||||
store?.refreshAppConfig()
|
||||
})
|
||||
|
||||
const handleConfirm = async () => {
|
||||
const result = (await $configForm.value?.validate()) || false
|
||||
if (result !== false) {
|
||||
await triggerRPC<void>(IRPCActionType.UPDATE_UPLOADER_CONFIG, type.value, result?._id, result)
|
||||
const configId = ($route.params.configId as string) || ''
|
||||
const res = await invokeRPC<boolean>(IRPCActionType.UPDATE_UPLOADER_CONFIG, type.value, configId, result)
|
||||
if (!res.success) {
|
||||
const message = res.error
|
||||
ElMessage.warning(message)
|
||||
return
|
||||
}
|
||||
showNotification({
|
||||
title: $T('SETTINGS_RESULT'),
|
||||
body: $T('TIPS_SET_SUCCEED')
|
||||
@@ -89,7 +101,7 @@ function getPicBeds (event: IpcRendererEvent, _config: IPicGoPluginConfig[], nam
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'PicbedsPage'
|
||||
name: PICBEDS_PAGE
|
||||
}
|
||||
</script>
|
||||
<style lang='stylus'>
|
||||
|
||||
@@ -10,4 +10,5 @@ export const PLUGIN_PAGE = 'PluginPage'
|
||||
export const SHORTKEY_PAGE = 'ShortkeyPage'
|
||||
export const URL_REWRITE_PAGE = 'UrlRewritePage'
|
||||
export const UPLOADER_CONFIG_PAGE = 'UploaderConfigPage'
|
||||
export const PICGO_CLOUD_PAGE = 'PicGoCloudPage'
|
||||
export const TOOLBOX_CONFIG_PAGE = 'ToolBoxPage'
|
||||
|
||||
@@ -52,6 +52,11 @@ export default createRouter({
|
||||
component: () => import(/* webpackChunkName: "Plugin" */ '@/pages/Plugin.vue'),
|
||||
name: config.PLUGIN_PAGE
|
||||
},
|
||||
{
|
||||
path: 'cloud',
|
||||
component: () => import(/* webpackChunkName: "PicGoCloud" */ '@/pages/PicGoCloud.vue'),
|
||||
name: config.PICGO_CLOUD_PAGE
|
||||
},
|
||||
{
|
||||
path: 'shortKey',
|
||||
component: () => import(/* webpackChunkName: "ShortkeyPage" */ '@/pages/ShortKey.vue'),
|
||||
|
||||
+107
-4
@@ -1,13 +1,56 @@
|
||||
import { reactive, InjectionKey, readonly, App, UnwrapRef, ref } from 'vue'
|
||||
import { saveConfig } from '@/utils/dataSender'
|
||||
import { reactive, InjectionKey, readonly, App, UnwrapRef, ref, type DeepReadonly } from 'vue'
|
||||
import { getConfig, getPicBeds, saveConfig } from '@/utils/dataSender'
|
||||
import type { IPicGoCloudUserInfo } from '#/types/cloud'
|
||||
import type { IConfig } from 'picgo'
|
||||
|
||||
export enum IPicGoCloudRequestStatus {
|
||||
IDLE = 'IDLE',
|
||||
LOADING = 'LOADING',
|
||||
ERROR = 'ERROR'
|
||||
}
|
||||
|
||||
export enum IPicGoCloudLoginStatus {
|
||||
IDLE = 'IDLE',
|
||||
IN_PROGRESS = 'IN_PROGRESS'
|
||||
}
|
||||
|
||||
export interface IPicGoCloudState {
|
||||
/**
|
||||
* PicGo Cloud auth tri-state:
|
||||
* - undefined: not loaded yet (first entry triggers auto load)
|
||||
* - null: loaded, but not logged in
|
||||
* - { user }: logged in
|
||||
*/
|
||||
userInfo: IPicGoCloudUserInfo | null | undefined;
|
||||
userInfoStatus: IPicGoCloudRequestStatus;
|
||||
userInfoError: string | null;
|
||||
loginStatus: IPicGoCloudLoginStatus;
|
||||
loginError: string | null;
|
||||
/**
|
||||
* Whether user has explicitly checked the acknowledgement before starting login
|
||||
* in the current app session.
|
||||
*/
|
||||
hasAgreedToTermsAndPrivacy: boolean;
|
||||
}
|
||||
|
||||
export interface IState {
|
||||
defaultPicBed: string;
|
||||
appConfig: IConfig | null;
|
||||
picBeds: IPicBedType[];
|
||||
picgoCloud: IPicGoCloudState;
|
||||
}
|
||||
|
||||
export interface IStore {
|
||||
state: UnwrapRef<IState>
|
||||
state: DeepReadonly<UnwrapRef<IState>>
|
||||
setDefaultPicBed: (type: string) => void;
|
||||
refreshAppConfig: () => Promise<void>;
|
||||
refreshPicBeds: () => Promise<void>;
|
||||
setPicGoCloudUserInfo: (userInfo: IPicGoCloudUserInfo | null | undefined) => void;
|
||||
setPicGoCloudUserInfoStatus: (status: IPicGoCloudRequestStatus) => void;
|
||||
setPicGoCloudUserInfoError: (error: string | null) => void;
|
||||
setPicGoCloudLoginStatus: (status: IPicGoCloudLoginStatus) => void;
|
||||
setPicGoCloudLoginError: (error: string | null) => void;
|
||||
setPicGoCloudHasAgreedToTermsAndPrivacy: (hasAgreed: boolean) => void;
|
||||
updateForceUpdateTime: () => void;
|
||||
}
|
||||
|
||||
@@ -15,7 +58,17 @@ export const storeKey: InjectionKey<IStore> = Symbol('store')
|
||||
|
||||
// state
|
||||
const state: IState = reactive({
|
||||
defaultPicBed: 'smms'
|
||||
defaultPicBed: 'smms',
|
||||
appConfig: null,
|
||||
picBeds: [],
|
||||
picgoCloud: {
|
||||
userInfo: undefined,
|
||||
userInfoStatus: IPicGoCloudRequestStatus.IDLE,
|
||||
userInfoError: null,
|
||||
loginStatus: IPicGoCloudLoginStatus.IDLE,
|
||||
loginError: null,
|
||||
hasAgreedToTermsAndPrivacy: false
|
||||
}
|
||||
})
|
||||
|
||||
const forceUpdateTime = ref<number>(Date.now())
|
||||
@@ -29,6 +82,48 @@ const setDefaultPicBed = (type: string) => {
|
||||
state.defaultPicBed = type
|
||||
}
|
||||
|
||||
const setAppConfig = (config: IConfig | null) => {
|
||||
state.appConfig = config
|
||||
if (config) {
|
||||
const picBed = config.picBed
|
||||
state.defaultPicBed = picBed.uploader || picBed.current || 'smms'
|
||||
}
|
||||
}
|
||||
|
||||
const refreshAppConfig = async (): Promise<void> => {
|
||||
const config = await getConfig<IConfig>()
|
||||
setAppConfig(config ?? null)
|
||||
}
|
||||
|
||||
const refreshPicBeds = async (): Promise<void> => {
|
||||
const picBeds = await getPicBeds()
|
||||
state.picBeds = picBeds
|
||||
}
|
||||
|
||||
const setPicGoCloudUserInfo = (userInfo: IPicGoCloudUserInfo | null | undefined) => {
|
||||
state.picgoCloud.userInfo = userInfo
|
||||
}
|
||||
|
||||
const setPicGoCloudUserInfoStatus = (status: IPicGoCloudRequestStatus) => {
|
||||
state.picgoCloud.userInfoStatus = status
|
||||
}
|
||||
|
||||
const setPicGoCloudUserInfoError = (error: string | null) => {
|
||||
state.picgoCloud.userInfoError = error
|
||||
}
|
||||
|
||||
const setPicGoCloudLoginStatus = (status: IPicGoCloudLoginStatus) => {
|
||||
state.picgoCloud.loginStatus = status
|
||||
}
|
||||
|
||||
const setPicGoCloudLoginError = (error: string | null) => {
|
||||
state.picgoCloud.loginError = error
|
||||
}
|
||||
|
||||
const setPicGoCloudHasAgreedToTermsAndPrivacy = (hasAgreed: boolean) => {
|
||||
state.picgoCloud.hasAgreedToTermsAndPrivacy = hasAgreed
|
||||
}
|
||||
|
||||
const updateForceUpdateTime = () => {
|
||||
forceUpdateTime.value = Date.now()
|
||||
}
|
||||
@@ -38,6 +133,14 @@ export const store = {
|
||||
app.provide(storeKey, {
|
||||
state: readonly(state),
|
||||
setDefaultPicBed,
|
||||
refreshAppConfig,
|
||||
refreshPicBeds,
|
||||
setPicGoCloudUserInfo,
|
||||
setPicGoCloudUserInfoStatus,
|
||||
setPicGoCloudUserInfoError,
|
||||
setPicGoCloudLoginStatus,
|
||||
setPicGoCloudLoginError,
|
||||
setPicGoCloudHasAgreedToTermsAndPrivacy,
|
||||
updateForceUpdateTime
|
||||
})
|
||||
app.provide('forceUpdateTime', forceUpdateTime)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PICGO_GET_CONFIG, PICGO_SAVE_CONFIG, RPC_ACTIONS } from '#/events/constants'
|
||||
import { GET_PICBEDS, PICGO_GET_CONFIG, PICGO_SAVE_CONFIG, RPC_ACTIONS } from '#/events/constants'
|
||||
import { IpcRendererEvent, ipcRenderer } from 'electron'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
@@ -30,25 +30,26 @@ export function getConfig<T> (key?: string): Promise<T | undefined> {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* trigger RPC action
|
||||
* TODO: create an isolate rpc handler
|
||||
*/
|
||||
export function triggerRPC<T> (action: IRPCActionType, ...args: any[]): Promise<T | null> {
|
||||
export function getPicBeds (): Promise<IPicBedType[]> {
|
||||
return new Promise((resolve) => {
|
||||
const callbackId = uuid()
|
||||
const callback = (event: IpcRendererEvent, data: T | null, returnActionType: IRPCActionType, returnCallbackId: string) => {
|
||||
if (returnCallbackId === callbackId && returnActionType === action) {
|
||||
resolve(data)
|
||||
ipcRenderer.removeListener(RPC_ACTIONS, callback)
|
||||
}
|
||||
}
|
||||
const data = getRawData(args)
|
||||
ipcRenderer.on(RPC_ACTIONS, callback)
|
||||
ipcRenderer.send(RPC_ACTIONS, action, data, callbackId)
|
||||
ipcRenderer.once(GET_PICBEDS, (_event: IpcRendererEvent, picBeds: IPicBedType[]) => {
|
||||
resolve(picBeds)
|
||||
})
|
||||
ipcRenderer.send(GET_PICBEDS)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke an RPC action and await its return value.
|
||||
*
|
||||
* This uses `ipcRenderer.invoke(RPC_ACTIONS, action, args)` which is backed by
|
||||
* `ipcMain.handle(RPC_ACTIONS, ...)` in the main process RPC server.
|
||||
*/
|
||||
export function invokeRPC<T> (action: IRPCActionType, ...args: any[]): Promise<IRPCResult<T>> {
|
||||
const data = getRawData(args)
|
||||
return ipcRenderer.invoke(RPC_ACTIONS, action, data) as Promise<IRPCResult<T>>
|
||||
}
|
||||
|
||||
/**
|
||||
* send a rpc request & do not need to wait for the response
|
||||
*
|
||||
|
||||
@@ -35,6 +35,7 @@ export const GET_RENAME_FILE_NAME = 'GET_RENAME_FILE_NAME'
|
||||
export const SHOW_MAIN_PAGE_QRCODE = 'SHOW_MAIN_PAGE_QRCODE'
|
||||
export const SHOW_MAIN_PAGE_DONATION = 'SHOW_MAIN_PAGE_DONATION'
|
||||
export const FORCE_UPDATE = 'FORCE_UPDATE'
|
||||
export const APP_CONFIG_UPDATED = 'APP_CONFIG_UPDATED'
|
||||
export const OPEN_WINDOW = 'OPEN_WINDOW'
|
||||
export const GET_PICBEDS = 'GET_PICBEDS'
|
||||
export const RPC_ACTIONS = 'RPC_ACTIONS'
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface IPicGoCloudUserInfo {
|
||||
user: string
|
||||
}
|
||||
|
||||
export enum IPicGoCloudErrorCode {
|
||||
LOGIN_TIMEOUT = 'PICGO_CLOUD_LOGIN_TIMEOUT',
|
||||
LOGIN_FAILED = 'PICGO_CLOUD_LOGIN_FAILED'
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
export enum IPicGoCloudConfigSyncSessionStatus {
|
||||
IDLE = 'IDLE',
|
||||
SYNCING = 'SYNCING',
|
||||
CONFLICT = 'CONFLICT'
|
||||
}
|
||||
|
||||
export enum IPicGoCloudConfigSyncRunStatus {
|
||||
SUCCESS = 'success',
|
||||
CONFLICT = 'conflict',
|
||||
FAILED = 'failed'
|
||||
}
|
||||
|
||||
export enum IPicGoCloudConfigSyncConflictChoice {
|
||||
LOCAL = 'LOCAL',
|
||||
CLOUD = 'CLOUD'
|
||||
}
|
||||
|
||||
export enum IPicGoCloudConfigSyncToastType {
|
||||
SUCCESS = 'success',
|
||||
ERROR = 'error',
|
||||
WARNING = 'warning',
|
||||
INFO = 'info'
|
||||
}
|
||||
|
||||
export enum IPicGoCloudEncryptionMethod {
|
||||
/**
|
||||
* AUTO means "follow remote state". It corresponds to `settings.picgoCloud.encryptionMethod` being `auto` or missing.
|
||||
*/
|
||||
AUTO = 'auto',
|
||||
/**
|
||||
* Server side encryption.
|
||||
* SSE corresponds to `settings.picgoCloud.encryptionMethod` being `sse`.
|
||||
*/
|
||||
SSE = 'sse',
|
||||
/**
|
||||
* End-to-end encryption.
|
||||
* E2EE corresponds to `settings.picgoCloud.encryptionMethod` being `e2ee`.
|
||||
*/
|
||||
E2EE = 'e2ee'
|
||||
}
|
||||
|
||||
export interface IPicGoCloudConfigSyncConflictItem {
|
||||
path: string
|
||||
localValue: unknown
|
||||
remoteValue: unknown
|
||||
}
|
||||
|
||||
export type IPicGoCloudConfigSyncResolution = Record<string, IPicGoCloudConfigSyncConflictChoice>
|
||||
|
||||
export interface IPicGoCloudConfigSyncState {
|
||||
sessionStatus: IPicGoCloudConfigSyncSessionStatus
|
||||
encryptionMethod?: IPicGoCloudEncryptionMethod
|
||||
/**
|
||||
* `updatedAt` in `config.snapshot.json` under `baseDir` (ISO string).
|
||||
* Used to display "last sync time" in the GUI.
|
||||
*/
|
||||
lastSyncedAt?: string
|
||||
conflicts?: IPicGoCloudConfigSyncConflictItem[]
|
||||
}
|
||||
|
||||
export interface IPicGoCloudConfigSyncRunResult {
|
||||
status: IPicGoCloudConfigSyncRunStatus
|
||||
message: string
|
||||
toastType: IPicGoCloudConfigSyncToastType
|
||||
state: IPicGoCloudConfigSyncState
|
||||
/**
|
||||
* When true, renderer SHOULD refresh auth state (treat as logged-out).
|
||||
*/
|
||||
authInvalidated?: boolean
|
||||
/**
|
||||
* When true, renderer SHOULD show a restart prompt after the flow succeeds.
|
||||
*/
|
||||
shouldShowRestartPrompt?: boolean
|
||||
}
|
||||
@@ -76,6 +76,17 @@ export enum IRPCActionType {
|
||||
SHOW_MENUBAR_ICON = 'SHOW_MENUBAR_ICON',
|
||||
SHOW_NOTIFICATION = 'SHOW_NOTIFICATION',
|
||||
|
||||
// picgo cloud rpc
|
||||
PICGO_CLOUD_GET_USER_INFO = 'PICGO_CLOUD_GET_USER_INFO',
|
||||
PICGO_CLOUD_LOGIN = 'PICGO_CLOUD_LOGIN',
|
||||
PICGO_CLOUD_LOGOUT = 'PICGO_CLOUD_LOGOUT',
|
||||
PICGO_CLOUD_DISPOSE_LOGIN_FLOW = 'PICGO_CLOUD_DISPOSE_LOGIN_FLOW',
|
||||
PICGO_CLOUD_CONFIG_SYNC_GET_STATE = 'PICGO_CLOUD_CONFIG_SYNC_GET_STATE',
|
||||
PICGO_CLOUD_CONFIG_SYNC_START = 'PICGO_CLOUD_CONFIG_SYNC_START',
|
||||
PICGO_CLOUD_CONFIG_SYNC_APPLY_RESOLUTION = 'PICGO_CLOUD_CONFIG_SYNC_APPLY_RESOLUTION',
|
||||
PICGO_CLOUD_CONFIG_SYNC_ABORT = 'PICGO_CLOUD_CONFIG_SYNC_ABORT',
|
||||
PICGO_CLOUD_CONFIG_SYNC_SET_E2E_PREFERENCE = 'PICGO_CLOUD_CONFIG_SYNC_SET_E2E_PREFERENCE',
|
||||
|
||||
// gallery and toolbox rpc
|
||||
UPDATE_GALLERY = 'UPDATE_GALLERY',
|
||||
GET_GALLERY_MENU_LIST = 'GET_GALLERY_MENU_LIST',
|
||||
|
||||
Vendored
-1
@@ -25,7 +25,6 @@ declare module 'vue' {
|
||||
saveConfig(data: IObj | string, value?: any): void
|
||||
getConfig<T>(key?: string): Promise<T | undefined>
|
||||
setDefaultPicBed(picBed: string): void
|
||||
triggerRPC<T> (action: import('~/universal/types/enum').IRPCActionType, ...args: any[]): Promise<T | null>
|
||||
defaultPicBed: string
|
||||
forceUpdate(): void
|
||||
sendToMain(channel: string, ...args: any[]): void
|
||||
|
||||
Vendored
+73
-1
@@ -4,7 +4,7 @@ interface ILocales {
|
||||
OPEN_MAIN_WINDOW: string
|
||||
CHOOSE_DEFAULT_PICBED: string
|
||||
OPEN_UPDATE_HELPER: string
|
||||
PRIVACY_AGREEMENT: string
|
||||
PRIVACY_TERMS_AGREEMENT: string
|
||||
RELOAD_APP: string
|
||||
UPLOAD_SUCCEED: string
|
||||
UPLOAD_FAILED: string
|
||||
@@ -32,6 +32,75 @@ interface ILocales {
|
||||
PICBEDS_SETTINGS: string
|
||||
PICGO_SETTINGS: string
|
||||
PLUGIN_SETTINGS: string
|
||||
PICGO_CLOUD_TITLE: string
|
||||
PICGO_CLOUD_ERROR_TITLE: string
|
||||
PICGO_CLOUD_NOT_LOGGED_IN: string
|
||||
PICGO_CLOUD_LOGIN: string
|
||||
PICGO_CLOUD_LOGOUT: string
|
||||
PICGO_CLOUD_CANCEL_LOGIN: string
|
||||
PICGO_CLOUD_RETRY: string
|
||||
PICGO_CLOUD_CONFIG_SYNC: string
|
||||
PICGO_CLOUD_LOGIN_IN_PROGRESS: string
|
||||
PICGO_CLOUD_LOGGED_IN_AS: string
|
||||
PICGO_CLOUD_OPEN: string
|
||||
PICGO_CLOUD_LOGIN_TIMEOUT: string
|
||||
PICGO_CLOUD_LOGIN_FAILED: string
|
||||
PICGO_CLOUD_AGREE_PREFIX: string
|
||||
PICGO_CLOUD_TERMS_OF_SERVICE: string
|
||||
PICGO_CLOUD_AGREE_AND: string
|
||||
PICGO_CLOUD_PRIVACY_POLICY: string
|
||||
PICGO_CLOUD_LOGIN_EXPIRED: string
|
||||
PICGO_CLOUD_ENCRYPTION_MODE_LABEL: string
|
||||
PICGO_CLOUD_ENCRYPTION_MODE_AUTO: string
|
||||
PICGO_CLOUD_ENCRYPTION_MODE_SERVER: string
|
||||
PICGO_CLOUD_ENCRYPTION_MODE_E2E: string
|
||||
PICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO: string
|
||||
PICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER: string
|
||||
PICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E: string
|
||||
PICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC: string
|
||||
PICGO_CLOUD_E2E_CHECKBOX_LABEL: string
|
||||
PICGO_CLOUD_E2E_ENABLE_WARNING_TITLE: string
|
||||
PICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE: string
|
||||
PICGO_CLOUD_REMOTE_E2E_AUTO_ENABLED: string
|
||||
PICGO_CLOUD_E2E_PIN_SETUP_TITLE: string
|
||||
PICGO_CLOUD_E2E_PIN_DECRYPT_TITLE: string
|
||||
PICGO_CLOUD_E2E_PIN_RETRY_TITLE: string
|
||||
PICGO_CLOUD_E2E_PIN_PLACEHOLDER: string
|
||||
PICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_SUCCESS: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_FAILED: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_ABORTED: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_STARTING: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_RESET_ALL: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_ABORT: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED: string
|
||||
PICGO_CLOUD_LAST_SYNC_TIME: string
|
||||
PICGO_CLOUD_LAST_SYNC_TIME_NONE: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_RESTART_NOW: string
|
||||
PICGO_CLOUD_CONFIG_SYNC_RESTART_LATER: string
|
||||
INPUT_BOX_CONFIRM_MISMATCH: string
|
||||
PICGO_SPONSOR_TEXT: string
|
||||
ALIPAY: string
|
||||
WECHATPAY: string
|
||||
@@ -270,6 +339,9 @@ interface ILocales {
|
||||
TIPS_FIND_NEW_VERSION: string
|
||||
TIPS_DELETE_UPLOADER_CONFIG: string
|
||||
TIPS_COPY_UPLOADER_CONFIG: string
|
||||
TIPS_UPLOADER_CONFIG_NAME_EMPTY: string
|
||||
TIPS_UPLOADER_CONFIG_NOT_FOUND: string
|
||||
TIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST: string
|
||||
PRIVACY: string
|
||||
PRIVACY_TIPS: string
|
||||
QUIT: string
|
||||
|
||||
Vendored
+10
-6
@@ -1,9 +1,13 @@
|
||||
|
||||
type IRPCResult<T> =
|
||||
| { success: true, data: T }
|
||||
| { success: false, error: string }
|
||||
|
||||
type IGetUploaderConfigListArgs = [type: string]
|
||||
type IDeleteUploaderConfigArgs = [type: string, id: string]
|
||||
type ISelectUploaderConfigArgs = [type: string, id: string]
|
||||
type IUpdateUploaderConfigArgs = [type: string, id: string, config: IStringKeyMap]
|
||||
type ICopyUploaderConfigArgs = [type: string, id: string]
|
||||
type IDeleteUploaderConfigArgs = [type: string, configName: string]
|
||||
type ISelectUploaderConfigArgs = [type: string, configName: string]
|
||||
type IUpdateUploaderConfigArgs = [type: string, configId: string, config: IStringKeyMap]
|
||||
type ICopyUploaderConfigArgs = [type: string, configName: string, newConfigName: string]
|
||||
type IGetLatestVersionArgs = [isCheckBetaVersion: boolean]
|
||||
type IToolboxCheckArgs = [type: import('./enum').IToolboxItemType]
|
||||
type IOpenFileArgs = [filePath: string]
|
||||
@@ -21,14 +25,14 @@ interface IRPCServer {
|
||||
|
||||
type IRPCRoutes = Map<import('./enum').IRPCActionType, IRPCHandler<any>>
|
||||
|
||||
type IRPCHandler<T> = (args: any[], event: import('electron').IpcMainEvent) => Promise<T>
|
||||
type IRPCHandler<T> = (args: any[], event: import('electron').IpcMainEvent | import('electron').IpcMainInvokeEvent) => Promise<T>
|
||||
|
||||
interface IRPCRouter {
|
||||
add<T>(action: import('./enum').IRPCActionType, handler: IRPCHandler<T>): IRPCRouter
|
||||
routes: () => IRPCRoutes
|
||||
}
|
||||
|
||||
type IToolboxChecker<T = any> = (event: import('electron').IpcMainEvent) => Promise<T>
|
||||
type IToolboxChecker<T = any> = (event: import('electron').IpcMainEvent | import('electron').IpcMainInvokeEvent) => Promise<T>
|
||||
|
||||
type IToolboxCheckerMap<T extends import('./enum').IToolboxItemType> = {
|
||||
[type in T]: IToolboxChecker
|
||||
|
||||
Vendored
+9
-1
@@ -252,7 +252,15 @@ interface IShowInputBoxOption {
|
||||
value?: string
|
||||
title: string
|
||||
placeholder: string
|
||||
inputType?: 'text' | 'textarea'
|
||||
inputType?: 'text' | 'textarea' | 'password'
|
||||
/**
|
||||
* Optional confirm input rendered in the same dialog.
|
||||
* Commonly used for password/PIN setup to avoid user typos.
|
||||
*/
|
||||
confirm?: {
|
||||
value?: string
|
||||
placeholder?: string
|
||||
}
|
||||
/**
|
||||
* default to 400
|
||||
*/
|
||||
|
||||
+24
-4
@@ -1,4 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const colors = require('tailwindcss/colors')
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
'./public/index.html',
|
||||
@@ -7,10 +9,28 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
blue: '#49B1F5',
|
||||
green: '#44B363',
|
||||
red: '#F15140',
|
||||
yellow: '#F1BE48'
|
||||
// Keep Tailwind's full color scales so utilities like `bg-blue-100` work.
|
||||
// Override the DEFAULT/500 tone to match PicGo's brand colors.
|
||||
blue: {
|
||||
...colors.blue,
|
||||
DEFAULT: '#49B1F5',
|
||||
500: '#49B1F5'
|
||||
},
|
||||
green: {
|
||||
...colors.green,
|
||||
DEFAULT: '#44B363',
|
||||
500: '#44B363'
|
||||
},
|
||||
red: {
|
||||
...colors.red,
|
||||
DEFAULT: '#F15140',
|
||||
500: '#F15140'
|
||||
},
|
||||
yellow: {
|
||||
...colors.yellow,
|
||||
DEFAULT: '#F1BE48',
|
||||
500: '#F1BE48'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { resolve } from 'path'
|
||||
|
||||
const alias = {
|
||||
'@': resolve(__dirname, 'src/renderer'),
|
||||
'~': resolve(__dirname, 'src'),
|
||||
'#': resolve(__dirname, 'src/universal'),
|
||||
root: resolve(__dirname, '.'),
|
||||
apis: resolve(__dirname, 'src/main/apis'),
|
||||
'@core': resolve(__dirname, 'src/main/apis/core')
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
resolve: { alias },
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['src/__tests__/**/*.spec.ts']
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user