Repository: Molunerfinn/PicGo Branch: dev Commit: 43a21de38097 Files: 219 Total size: 690.1 KB Directory structure: gitextract_p8hvu2ck/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── config.yml │ │ └── feature_request.yml │ └── workflows/ │ ├── issue-duplicate-detection.yml │ └── main.yml ├── .gitignore ├── .husky/ │ ├── commit-msg │ ├── pre-commit │ └── pre-push ├── .node-version ├── .npmrc ├── .travis.deprecated.yml ├── .vscode/ │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── AGENTS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CONTRIBUTING_EN.md ├── FAQ.md ├── LICENSE ├── README.md ├── README_zh-CN.md ├── babel.config.js ├── build/ │ ├── entitlements.mac.plist │ ├── icons/ │ │ └── icon.icns │ └── installer.nsh ├── changelog/ │ ├── 2.4.0.md │ ├── 2.4.1.md │ ├── 2.4.2.md │ ├── 2.4.3.md │ ├── 2.5.1.md │ ├── 2.5.2.md │ ├── 2.5.3.md │ └── gen_changelog.md ├── electron-builder.config.ts ├── electron.vite.config.ts ├── eslint.config.js ├── package.json ├── pnpm-workspace.yaml ├── postcss.config.js ├── public/ │ ├── Upload pictures with PicGo.workflow/ │ │ └── Contents/ │ │ ├── Info.plist │ │ ├── Resources/ │ │ │ └── background.color │ │ └── document.wflow │ ├── i18n/ │ │ ├── en.yml │ │ ├── zh-CN.yml │ │ └── zh-TW.yml │ ├── index.html │ ├── linux.sh │ ├── mac.applescript │ ├── windows.ps1 │ ├── windows10.ps1 │ └── wsl.sh ├── scripts/ │ ├── config.js │ ├── cos-link.js │ ├── gen-i18n-types.js │ ├── merge-artifacts.js │ ├── notarize.js │ ├── update-win-yaml.js │ └── upload-dist.js ├── src/ │ ├── __tests__/ │ │ ├── main/ │ │ │ ├── cloud-config-sync.spec.ts │ │ │ ├── getLatestVersion.spec.ts │ │ │ └── server.spec.ts │ │ ├── renderer/ │ │ │ ├── store/ │ │ │ │ └── appConfig.spec.ts │ │ │ └── utils/ │ │ │ └── dataSender.spec.ts │ │ └── universal/ │ │ └── utils/ │ │ └── common.spec.ts │ ├── background.ts │ ├── main/ │ │ ├── apis/ │ │ │ ├── README.md │ │ │ ├── app/ │ │ │ │ ├── remoteNotice/ │ │ │ │ │ └── index.ts │ │ │ │ ├── shortKey/ │ │ │ │ │ ├── builtin.ts │ │ │ │ │ ├── shortKeyHandler.ts │ │ │ │ │ └── shortKeyService.ts │ │ │ │ ├── system/ │ │ │ │ │ └── index.ts │ │ │ │ ├── uploader/ │ │ │ │ │ ├── apis.ts │ │ │ │ │ └── index.ts │ │ │ │ └── window/ │ │ │ │ ├── constants.ts │ │ │ │ ├── windowList.ts │ │ │ │ └── windowManager.ts │ │ │ ├── core/ │ │ │ │ ├── bus/ │ │ │ │ │ ├── apis.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── datastore/ │ │ │ │ │ ├── dbChecker.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── picgo/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── logger.ts │ │ │ │ └── utils/ │ │ │ │ └── localLogger.ts │ │ │ └── gui/ │ │ │ └── index.ts │ │ ├── events/ │ │ │ ├── busEventList.ts │ │ │ ├── ipcList.ts │ │ │ ├── picgoCoreIPC.ts │ │ │ ├── remotes/ │ │ │ │ ├── menu.ts │ │ │ │ └── picBedListMenu.ts │ │ │ └── rpc/ │ │ │ ├── index.ts │ │ │ ├── router.ts │ │ │ ├── routes/ │ │ │ │ ├── cloud.ts │ │ │ │ ├── config.ts │ │ │ │ ├── galleryToolbox/ │ │ │ │ │ ├── builtIn/ │ │ │ │ │ │ ├── changeURL.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── menuListManager.ts │ │ │ │ ├── system.ts │ │ │ │ ├── toolbox/ │ │ │ │ │ ├── checkClipboardUpload.ts │ │ │ │ │ ├── checkFile.ts │ │ │ │ │ ├── checkProxy.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ └── version.ts │ │ │ └── utils.ts │ │ ├── i18n/ │ │ │ └── index.ts │ │ ├── lifeCycle/ │ │ │ ├── errorHandler.ts │ │ │ ├── fixPath.ts │ │ │ └── index.ts │ │ ├── migrate/ │ │ │ └── index.ts │ │ ├── server/ │ │ │ ├── handler.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ └── utils/ │ │ ├── appConfigNotifier.ts │ │ ├── beforeOpen.ts │ │ ├── cleanupFormUploaderFiles.ts │ │ ├── common.ts │ │ ├── constants.ts │ │ ├── dataReport.ts │ │ ├── deviceId.ts │ │ ├── env.ts │ │ ├── getLatestVersion.ts │ │ ├── getMacOSVersion.ts │ │ ├── getPicBeds.ts │ │ ├── handleArgv.ts │ │ ├── handleI18n.ts │ │ ├── handleUploaderConfig.ts │ │ ├── pasteTemplate.ts │ │ ├── privacyManager.ts │ │ └── updateChecker.ts │ ├── preload/ │ │ └── index.ts │ ├── renderer/ │ │ ├── App.vue │ │ ├── assets/ │ │ │ ├── .gitkeep │ │ │ ├── css/ │ │ │ │ └── tailwind.css │ │ │ └── fonts/ │ │ │ └── iconfont.css │ │ ├── components/ │ │ │ ├── ConfigForm.vue │ │ │ ├── ToolboxHandler.vue │ │ │ ├── ToolboxStatusIcon.vue │ │ │ ├── dialog/ │ │ │ │ ├── ConfigFormDialog.vue │ │ │ │ ├── ConfirmDialog.vue │ │ │ │ └── InputBoxDialog.vue │ │ │ ├── form/ │ │ │ │ └── BaseConfigForm.vue │ │ │ ├── picgoCloud/ │ │ │ │ └── ConfigSyncConflictDialog.vue │ │ │ └── settings/ │ │ │ ├── ButtonFormItem.vue │ │ │ ├── SelectFormItem.vue │ │ │ └── SwitchFormItem.vue │ │ ├── hooks/ │ │ │ ├── useATagClick.ts │ │ │ ├── useConfigForm.ts │ │ │ ├── useIPC.ts │ │ │ ├── useOS.ts │ │ │ ├── useStore.ts │ │ │ ├── useVModel.ts │ │ │ └── useVModelValues.ts │ │ ├── i18n/ │ │ │ └── index.ts │ │ ├── index.html │ │ ├── layouts/ │ │ │ └── Main.vue │ │ ├── main.ts │ │ ├── pages/ │ │ │ ├── Gallery.vue │ │ │ ├── MiniPage.vue │ │ │ ├── PicGoCloud.vue │ │ │ ├── PicGoSetting.vue │ │ │ ├── Plugin.vue │ │ │ ├── RenamePage.vue │ │ │ ├── ShortKey.vue │ │ │ ├── Toolbox.vue │ │ │ ├── TrayPage.vue │ │ │ ├── Upload.vue │ │ │ ├── UploaderConfigPage.vue │ │ │ ├── UrlRewrite.vue │ │ │ ├── components/ │ │ │ │ ├── gallery/ │ │ │ │ │ └── GalleryToolbar.vue │ │ │ │ └── settings/ │ │ │ │ ├── buttonArea/ │ │ │ │ │ ├── ButtonAreaSettings.vue │ │ │ │ │ ├── CheckUpdateDialog.vue │ │ │ │ │ ├── CustomLinkDialog.vue │ │ │ │ │ ├── LogSettingDialog.vue │ │ │ │ │ ├── ProxySettingDialog.vue │ │ │ │ │ └── ServerSettingsDialog.vue │ │ │ │ ├── customArea/ │ │ │ │ │ ├── ChoosePicBed.vue │ │ │ │ │ └── CustomAreaSettings.vue │ │ │ │ ├── selectArea/ │ │ │ │ │ └── SelectAreaSettings.vue │ │ │ │ └── switchArea/ │ │ │ │ └── SwitchAreaSettings.vue │ │ │ └── picbeds/ │ │ │ └── index.vue │ │ ├── router/ │ │ │ ├── config.ts │ │ │ └── index.ts │ │ ├── store/ │ │ │ └── index.ts │ │ └── utils/ │ │ ├── LS.ts │ │ ├── analytics.ts │ │ ├── bus.ts │ │ ├── common.ts │ │ ├── dataSender.ts │ │ ├── db.ts │ │ ├── key-binding.ts │ │ ├── mainMixin.ts │ │ ├── mixin.ts │ │ ├── notification.ts │ │ ├── static.ts │ │ └── uploader.ts │ └── universal/ │ ├── events/ │ │ └── constants.ts │ ├── i18n/ │ │ └── index.ts │ ├── types/ │ │ ├── cloud.ts │ │ ├── cloudConfigSync.ts │ │ ├── electron.d.ts │ │ ├── enum.ts │ │ ├── extra-vue.d.ts │ │ ├── global.d.ts │ │ ├── i18n.d.ts │ │ ├── rpc.d.ts │ │ ├── shims-module.d.ts │ │ ├── shims-tsx.d.ts │ │ ├── types.d.ts │ │ └── view.d.ts │ └── utils/ │ ├── common.ts │ ├── static.ts │ └── staticPath.ts ├── tailwind.config.js ├── tsconfig.json └── vitest.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: ["Molunerfinn"] ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: "✨ Feature Request" description: 功能请求 / Feature request title: "[Feature]: " labels: ["feature request"] assignees: - molunerfinn body: - type: markdown attributes: value: |+ ## PicGo Issue 模板 请依照该模板来提交,否则将会被关闭。 **提问之前请注意你看过 FAQ、文档以及那些被关闭的 issues。否则同样的提问也会被关闭!** Please submit according to this template, otherwise it will be closed. **Before asking a question, please note that you have read the FAQ, Doc, and those closed issues. Otherwise the same question will also be closed! ** - type: checkboxes id: read attributes: label: 前置阅读 | Pre-reading description: 我已经自行查找、阅读以下内容(阅读了请打勾) | I have searched and read the following on my own (Please tick after reading) options: - label: "[文档/Doc](https://docs.picgo.app/gui/)" required: true - label: "[Issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed)" required: true - label: "[FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md)" required: true - type: input id: version attributes: label: PicGo的版本 | PicGo Version placeholder: 例如 v2.3.0-beta.1 validations: required: true - type: dropdown id: platform attributes: label: 系统信息 | System Information options: - Windows - Mac - Mac(arm64) - Linux - All validations: required: true - type: textarea id: reproduce attributes: label: 功能请求 | Feature request description: 详细描述你所预想的功能或者是现有功能的改进 | Describe in detail the features you envision or improvements to existing features validations: required: true - type: markdown attributes: value: | 最后,喜欢 PicGo 的话不妨给它点个 star~ 如果可以的话,请我喝杯咖啡?首页有赞助二维码,谢谢你的支持! Finally, if you like PicGo, give it a star~ Buy me a cup of coffee if you can? There is a sponsorship QR code on the homepage, thank you for your support! ================================================ FILE: .github/workflows/issue-duplicate-detection.yml ================================================ name: Issue Duplicate Detection on: issues: types: - opened - edited - reopened permissions: contents: read issues: write jobs: detect-duplicate-issues: runs-on: ubuntu-latest steps: - name: Run Warp Agent duplicate detector uses: warpdotdev/warp-agent-action@v1 id: duplicate_detector env: GH_TOKEN: ${{ github.token }} with: warp_api_key: ${{ secrets.WARP_API_KEY }} profile: ${{ vars.WARP_AGENT_PROFILE || '' }} prompt: | You are triaging duplicate GitHub issues for this repository. Repository: ${{ github.repository }} Target issue number: #${{ github.event.issue.number }} Target issue URL: ${{ github.event.issue.html_url }} Target issue title: ${{ github.event.issue.title }} Target issue body: ${{ github.event.issue.body }} Use the GitHub CLI with GH_TOKEN for all operations. Workflow requirements: 1. Gather issue context from title/body/error messages/symptoms/components. - Run: gh issue view ${{ github.event.issue.number }} --repo ${{ github.repository }} --json number,title,body,url,state,labels 2. Search with multiple strategies: - title keywords - error message fragments - symptom words - component/module names Example command pattern: gh issue list --repo ${{ github.repository }} --state all --search "" 3. Inspect every candidate in detail: gh issue view --repo ${{ github.repository }} --json number,title,body,url,state 4. Duplicate threshold: - only mark duplicate when confidence >= 90% - same root cause + very similar symptoms/errors/components 5. Exclusions: - never include pull requests - never include the current issue itself (#${{ github.event.issue.number }}) 6. If confidence is insufficient or no duplicates exist, exit without commenting. 7. If duplicates exist, create or update exactly one comment on issue #${{ github.event.issue.number }}: - first line must be: - include markdown bullet list with title + link: - [Issue title](${{ github.server_url }}/${{ github.repository }}/issues/123) 8. Before posting, check existing comments on the target issue: - if marker comment exists, update that comment - otherwise create a new comment 9. Do not comment on any other issue. Expected comment format: Detected potentially duplicate issues: - [Duplicate issue title](${{ github.server_url }}/${{ github.repository }}/issues/123) - [Another duplicate issue](${{ github.server_url }}/${{ github.repository }}/issues/456) ================================================ FILE: .github/workflows/main.yml ================================================ name: Build & Release on: push: tags: - v* workflow_dispatch: inputs: release_tag: description: "GitHub release tag to publish to (optional, defaults to current branch like dev)" required: false default: "" type: string build_os: description: "Build for specific OS: Windows, macOS, Linux, All" required: true default: "All" type: choice options: - Windows - macOS - Linux - All test_upload_dist: description: "Test upload-dist.js script" required: true default: false type: boolean test_upload_dist_to_dev: description: "Test upload-dist.js script to dev folder" required: true default: false type: boolean skip_mac_notarize: description: "Skip Mac Notarization (true/false)" required: true default: false type: boolean win_signing_mode: description: "Windows Signing Mode: release-signing(default) or test-signing" required: true default: "release-signing" type: choice options: - release-signing - test-signing env: NODE_VERSION: 22.x jobs: # ============== macOS Builds ============== build-macos: name: Build macOS (${{ matrix.arch }}) if: github.event.inputs.build_os == 'macOS' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v') runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - runner: macos-15-intel target: dmg arch: x64 - runner: macos-latest target: dmg arch: arm64 steps: - name: Check out git repository uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10.29.2 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm cache-dependency-path: pnpm-lock.yaml - name: Clean workspace run: rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build macOS App (${{ matrix.arch }}) run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --mac ${{ matrix.target }} --${{ matrix.arch }} --publish never env: GH_TOKEN: ${{ secrets.GH_TOKEN }} CSC_LINK: ${{ secrets.MAC_CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} SKIP_NOTARIZE: ${{ inputs.skip_mac_notarize }} - name: Upload Artifact uses: actions/upload-artifact@v4 with: name: PicGo-macOS-${{ matrix.arch }} path: dist/*.* # ============== Windows Builds ============== build-windows: name: Build Windows (${{ matrix.arch }}) if: github.event.inputs.build_os == 'Windows' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v') runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - runner: windows-latest arch: x64-ia32 build_arch: --x64 --ia32 target: nsis - runner: windows-11-arm arch: arm64 build_arch: --arm64 target: nsis steps: - name: Check out git repository uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10.29.2 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm cache-dependency-path: pnpm-lock.yaml - name: Clean workspace run: | if (Test-Path dist) { Remove-Item -Recurse -Force dist } if (Test-Path dist_electron) { Remove-Item -Recurse -Force dist_electron } if (Test-Path node_modules) { Remove-Item -Recurse -Force node_modules } if (Test-Path "$env:LOCALAPPDATA\electron-builder") { Remove-Item "$env:LOCALAPPDATA\electron-builder" -Recurse -Force -ErrorAction SilentlyContinue } if (Test-Path "$env:LOCALAPPDATA\electron") { Remove-Item "$env:LOCALAPPDATA\electron" -Recurse -Force -ErrorAction SilentlyContinue } - name: Install dependencies run: pnpm install --frozen-lockfile # 1. Build (Unsigned) - name: Build Windows App (${{ matrix.arch }}) run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --win ${{matrix.target}} ${{ matrix.build_arch }} --publish never env: GH_TOKEN: ${{ secrets.GH_TOKEN }} # 2. Upload Unsigned Artifact to SignPath (Intermediate Step) - name: Upload Unsigned Artifact for Signing id: upload-unsigned uses: actions/upload-artifact@v4 with: name: unsigned-${{ matrix.arch }} path: dist/*.exe retention-days: 1 if-no-files-found: error - name: Notification for Signing Start shell: bash env: NOTIFY_URL: ${{ secrets.SIGN_NOTIFICATION }} run: curl "$NOTIFY_URL" # 3. Submit to SignPath and Wait - name: Sign Artifact with SignPath uses: signpath/github-action-submit-signing-request@v2 env: SIGNPATH_SIGNING_POLICY_SLUG: | ${{ (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') || inputs.win_signing_mode == 'release-signing') && 'release-signing' || 'test-signing' }} ARTIFACT_SLUG: | ${{ (matrix.arch == 'x64-ia32') && 'PicGo-Windows' || (matrix.arch == 'arm64') && 'PicGo-Windows-ARM64' || '' }} with: api-token: "${{ secrets.SIGNPATH_API_TOKEN }}" organization-id: "${{ secrets.SIGNPATH_ORGANIZATION_ID }}" project-slug: "${{ secrets.SIGNPATH_PROJECT_SLUG }}" signing-policy-slug: "${{ env.SIGNPATH_SIGNING_POLICY_SLUG }}" github-artifact-id: "${{ steps.upload-unsigned.outputs.artifact-id }}" artifact-configuration-slug: "${{ env.ARTIFACT_SLUG }}" wait-for-completion: true output-artifact-directory: "signed-artifact" # 4. Replace Unsigned with Signed & Fix Blockmap (Critical for Auto-Update) - name: Replace Unsigned with Signed & Update latest.yml shell: powershell run: | # 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-Windows-${{ matrix.arch }} path: dist/*.* # ============== Linux Builds ============== build-linux: name: Build Linux (${{ matrix.arch }}) if: github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v') runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - runner: ubuntu-latest arch: x64 target: AppImage deb snap - runner: ubuntu-24.04-arm arch: arm64 target: AppImage deb steps: - name: Check out git repository uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10.29.2 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm cache-dependency-path: pnpm-lock.yaml - name: Clean workspace run: rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron - name: Install dependencies run: pnpm install --frozen-lockfile - name: Install Linux dependencies run: | sudo apt-get update sudo apt-get install -y libfuse2 - name: Build Linux App (${{ matrix.arch }}) run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --linux ${{matrix.target}} --${{ matrix.arch }} --publish never env: GH_TOKEN: ${{ secrets.GH_TOKEN }} - name: Upload Artifact uses: actions/upload-artifact@v4 with: name: PicGo-Linux-${{ matrix.arch }} path: dist/*.* # ============== Release ============== release: name: Merge & Release needs: [build-macos, build-windows, build-linux] runs-on: ubuntu-latest permissions: contents: write steps: - name: Check out git repository uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10.29.2 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm cache-dependency-path: pnpm-lock.yaml - name: Install dependencies run: pnpm install --frozen-lockfile - name: Download all artifacts uses: actions/download-artifact@v4 with: path: artifacts - 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 }} PICGO_ENV_S3_ACCOUNT_ID: ${{ secrets.PICGO_ENV_S3_ACCOUNT_ID }} PICGO_ENV_S3_LEGACY_ACCOUNT_ID: ${{ secrets.PICGO_ENV_S3_LEGACY_ACCOUNT_ID }} PICGO_ENV_S3_LEGACY_SECRET_ID: ${{ secrets.PICGO_ENV_S3_LEGACY_SECRET_ID }} PICGO_ENV_S3_LEGACY_SECRET_KEY: ${{ secrets.PICGO_ENV_S3_LEGACY_SECRET_KEY }} - name: Publish GitHub Workflow Release if: github.event_name == 'workflow_dispatch' uses: softprops/action-gh-release@v2 continue-on-error: true with: token: ${{ secrets.GH_TOKEN }} tag_name: ${{ github.event.inputs.release_tag || github.ref_name }} draft: true prerelease: false files: | dist/*.exe dist/*.dmg dist/*.zip dist/*.AppImage dist/*.deb dist/*.snap dist/*.tar.gz dist/*.yml dist/*.blockmap - name: Publish GitHub Release if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v2 continue-on-error: true with: token: ${{ secrets.GH_TOKEN }} generate_release_notes: true draft: true prerelease: false files: | dist/*.exe dist/*.dmg dist/*.zip dist/*.AppImage dist/*.deb dist/*.snap dist/*.tar.gz dist/*.yml dist/*.blockmap ================================================ FILE: .gitignore ================================================ .DS_Store dist/electron/* dist/web/* build/* !build/icons !build/installer.nsh !build/entitlements.mac.plist coverage node_modules/ npm-debug.log npm-debug.log.* thumbs.db !.gitkeep yarn-error.log docs/dist/ # local env files .env.local .env.*.local dist_electron/ test.js .env scripts/*.yml #Electron-builder output /dist_electron .serena/ dist/* test.js specs/ .cache/ openspec/ bug* ================================================ FILE: .husky/commit-msg ================================================ pnpm commitlint ${1} ================================================ FILE: .husky/pre-commit ================================================ pnpm check ================================================ FILE: .husky/pre-push ================================================ pnpm test ================================================ FILE: .node-version ================================================ 22 ================================================ FILE: .npmrc ================================================ shamefully-hoist=true ================================================ FILE: .travis.deprecated.yml ================================================ # Commented sections below can be used to run tests on the CI server # https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing osx_image: xcode8.3 sudo: required dist: trusty language: c matrix: include: - os: osx - os: linux env: CC=clang CXX=clang++ npm_config_clang=1 compiler: clang cache: directories: - node_modules - "$HOME/.electron" - "$HOME/.cache" addons: apt: packages: - libgnome-keyring-dev - icnsutils #- xvfb before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install git-lfs; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi install: #- export DISPLAY=':99.0' #- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & - nvm install 10 - curl -o- -L https://yarnpkg.com/install.sh | bash - source ~/.bashrc - npm install -g xvfb-maybe - yarn script: #- xvfb-maybe node_modules/.bin/karma start test/unit/karma.conf.js #- yarn run pack && xvfb-maybe node_modules/.bin/mocha test/e2e - npm run release # - yarn run build:docs before_script: - git lfs pull branches: only: - master # after_script: # - cd docs/dist # - git init # - git config user.name "Molunerfinn" # - git config user.email "marksz@teamsz.xyz" # - git add . # - git commit -m "Travis build docs" # - git push --force --quiet "https://${GH_TOKEN}@github.com/Molunerfinn/PicGo.git" master:gh-pages ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "name": "Electron: Main", "type": "node", "request": "launch", "protocol": "inspector", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", "windows": { "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" }, "preLaunchTask": "electron-debug", "args": [ "--remote-debugging-port=9223", "./dist" ], "outFiles": [ "${workspaceFolder}/dist/**/*.js" ] }, { "name": "Electron: Renderer", "type": "chrome", "request": "attach", "port": 9223, "urlFilter": "http://localhost:*", "timeout": 30000, "webRoot": "${workspaceFolder}/src", "sourceMapPathOverrides": { "webpack:///./src/*": "${webRoot}/*" } } ], "compounds": [ { "name": "Electron: All", "configurations": [ "Electron: Main", "Electron: Renderer" ] } ] } ================================================ FILE: .vscode/settings.json ================================================ { "eslint.enable": true, "eslint.alwaysShowStatus": true, "eslint.format.enable": true, "eslint.validate": [ "javascript", "javascriptreact", "typescript", "vue", "typescriptreact" ], "[stylus]": { "editor.formatOnSave": true }, "stylusSupremacy.insertSemicolons": false, "stylusSupremacy.insertBraces": false, "stylusSupremacy.insertNewLineBetweenSelectors": true, "stylusSupremacy.insertParenthesisAroundIfCondition": false, "stylusSupremacy.alwaysUseNoneOverZero": true, "stylusSupremacy.alwaysUseZeroWithoutUnit": true, "stylusSupremacy.sortProperties": "grouped", "stylusSupremacy.quoteChar": "\"", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", "source.organizeImports": "never" }, "prettier.enable": false, "[typescript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, } ================================================ FILE: .vscode/tasks.json ================================================ { // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { "label": "electron-debug", "type": "process", "command": "./node_modules/.bin/vue-cli-service", "windows": { "command": "./node_modules/.bin/vue-cli-service.cmd" }, "isBackground": true, "args": ["electron:serve", "--debug"], "problemMatcher": { "owner": "custom", "pattern": { "regexp": "" }, "background": { "beginsPattern": "Starting development server\\.\\.\\.", "endsPattern": "Not launching electron as debug argument was passed\\." } } } ] } ================================================ FILE: AGENTS.md ================================================ # Repository Guidelines ## Project Structure & Module Organization PicGo is an Electron + Vue 3 desktop client. Source lives in `src/`: `src/main` for main-process and IPC logic, `src/renderer` for Vue views, and `src/universal` for shared helpers (`types/`, `events/constants.ts`). `background.ts` wires Electron Builder. Static assets and locale YAML files stay in `public/` (add languages under `public/i18n/`), while `docs/` hosts user-facing guides. Automation scripts live in `scripts/`, and legacy tests sit under `test/unit` (Karma) and `test/e2e` (Spectron). ## Build, Test, and Development Commands - `pnpm install` — install dependencies; `npm install` is unsupported. Only run this when the user explicitly asks/coordinates it. - Always add/remove dependencies with `pnpm` (never edit package.json versions by hand then install). - `pnpm dev` — electron-vite dev server for main/preload/renderer. - `pnpm build` — electron-vite build outputs to `dist/main`, `dist/preload`, `dist/renderer`; `pnpm preview` for preview mode. - Packaging config lives in `electron-builder.yml` (read by electron-builder via package.json `build` field/extraResources); set `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/` if downloads are slow. - `pnpm lint` / `pnpm lint:fix` — run or auto-fix ESLint (Standard, TypeScript, Vue rules). - `pnpm lint:dpdm` — fail fast on circular dependencies in `src/`. - `pnpm check` — run `tsc` + `lint` (run once before finishing a task). - Before completing a task, always run `pnpm check` and resolve any issues it reports. - `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 ` ================================================ FILE: src/renderer/assets/.gitkeep ================================================ ================================================ FILE: src/renderer/assets/css/tailwind.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; ================================================ FILE: src/renderer/assets/fonts/iconfont.css ================================================ @font-face {font-family: "iconfont"; src: url('iconfont.eot?t=1523001890286'); /* IE9*/ src: url('iconfont.eot?t=1523001890286#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAApcAAsAAAAADqAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAARAAAAFZW7kmaY21hcAAAAYAAAACMAAAB5Jz6bNVnbHlmAAACDAAABisAAAfkf7GmJ2hlYWQAAAg4AAAALwAAADYQ+dpBaGhlYQAACGgAAAAcAAAAJAfeA4lobXR4AAAIhAAAABQAAAAgH+kAAGxvY2EAAAiYAAAAEgAAABII+gbebWF4cAAACKwAAAAfAAAAIAEYAMxuYW1lAAAIzAAAAUUAAAJtPlT+fXBvc3QAAAoUAAAASAAAAF2Rted1eJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2Bk/ss4gYGVgYOpk+kMAwNDP4RmfM1gxMjBwMDEwMrMgBUEpLmmMDgwVDxfzdzwv4EhhrmBoQEozAiSAwAxwA0deJzFkdEJwzAMRE9ymoRiSv+TETpK6BQZwvSr+2QvZYz0ZLmFTpAzz/gOGRkLwAVAIg/SAfKGwPViKjVPuNa8w5M+Y4TyXExttmXfjoNpMbHp574SVmfccOdy12Ngv8TbStvjNMl5rf+V6742N5DS4BOtwX+DaeA1NgU+O5sDn6Etgc9x3wLoBwU0H794nG1VXYwcRxHu6p7unt+enf/9m9293buZde5u97w/M3Zs7xof2AoJPsLJ4JMR52AUEUgiJcLxQ2znEEIkEEQiJGOQIl0QCDlKpCgP5MXipAiJl5N44S0IAvjVQkGKBUIs9OzFEkjslGqner7pmar6vhpEEfr3n8htUkYe6qLD6JPoswgBW4a2wDEspKMeXoZggQaRL0jaSRd4p90jJyBqMz8cZKMkYpzZIKABw4VBlvZwCuPRBB+DQRgDVGrVTXep7pJXQC+njW/PPo1/CkGzU7cnq7OHVqb+oOWpV0zXrbjuyyqjVMVYsQU8FYUa1XQ2+xm1q8Ht5iHcBLOSVh/Zslo197EXR0/HS5EGsLMDXq0lfjF1qo60q9XQcyu8ZKnlqtVZ9OHKHaPsmXHyFyR/IHP9DXmP5GhdBgJ4A6IwS8csyXvAkyxqQNCAfAIyTEeZ9HkSRtkgmgAPmUQH83tiYOEBCKO9GcW3XirZ8MUJLrVwtaK0O62zgWLrG8+Tcki5A8dP5t/ITx4Hh9OwTK6eNV2wnRdvYTojOZ3t3bzTJ7a+KSY0rYOvAU3JGphhQ6UXri+diLRpn6iHbPuQSvpTLTqxdP0C1Q+1ntdt0r9zc29W5MXmed0jWyhEPfQw+gK6hJ6UGfqsnaR5RybDxz0YZVPZpOgwbyfjUZEh6xSL+bBYDHzGi/y5mJu8mmdunoWc8QnIdXpQrXnWRF6VJzIUkN6nAflIhAIA8EuPq5ppaurVn5MbhsDKtK9QQ8iFM9vYvtJIRj1Cnjj/4Ksbb/6TQfSvM0fOYyArF3vThdmPX7ilKLde2Cn87voWxlvrpwp/LozjlTjGC4ZtG8qTPyDyMSIQGJS3vtWwDUYPnyIY7FBg/JWHFcM+rWjidHZs90i3D+SNndJ3oOrw/indrNVVcWkDP3d+6zLGl7fOPze7CZuTySbM/W5ztSkNIVwUlXyEP0Cvob0DpgiwD/64wHNPAj8KZWGGgyyfKAUdRknao2lSKCDpQ/Ix3iapLJe0ZJRPyHAQNkHWawqyZH0oliV0LLswIVOYSM3ILaU1YVBsOcjmmLlJXBFAgwSSiwfvIoXHWmnRWAnOpjgL4XfNMyFXmKgtW1xlinfE1lvML5l0fXnoXvr97aeZs8itmpmugKYQsLnhirrgbFEjRjSo+HpDCybR5742XrBj3n3QUAittAdR/WhtrChO4HBDjWhiV5dMZlK93F4JAu61PSte9a2KZmCTVayKzhlRMYms0lo5wrxW/RSMa4stGzChhiq6PjXLq7O7Xs3fI3WdcuGwNn6f1koKlnsebdiVqARlTY1ZPU7a3vb6A7ZKACuawhy99aWhc1hjPNZc1yxToli44J3GlRIvHYk2Hh/JCPi4ay1bfrfu1ip9InxXxQb3adPwBCWgLOpuq1lmFMtxwyPHjG1HngLTbVUQtWG7S4aBsaYqs10eJN16ydZLoeZWFQwFrua/qwSUEEpBDhVF6m+XfEAuIhOV0SKaILQk2zicwCiRg5RBiKIM5QlKGYruK0dqLGey1ccg8EM5LbN8XHBJdjxpc/g1lKzjluNYx01ndoGCvb8PNqWzD/f3Zx/m9O4779ylc39PEFwyLsyBsGWUMBEvmy3zWRmBU3GklzfQ/97g/p3S/80JDXGjeMwNYYTO65ZVzEjJ/b/iX6I1yfyMFYOvXTAw6hXan7NTWh5KysohmRwwsQCEw8WMvO+VFfzHf2x8QnRjunMZcGhd+y5zTH9j8/PbluWDGrZyXVxa9mtC0hbt48t9UMR7wl47ybazHarYj7x76tFXGkcbGMdrR39icd1tf9+xHMu9tmGYf74/x39LfkVOo20ZJGky/wB1Pn4XKUt5yK+SfOEsTbK5kKTgChxnbnFxLpjhfIjLJJLikJWX048daKtTCKvA5RkmUK7oPntWq9i60X2o6vsYjDLFnhss6UIFJap2Lo4D68uU6tfunXti+XvVuubzZ7SqMHSJDzyJjyjxPD/RBFfC2hz+GGPa9b+f+2of/8GRfVF/ZJiStkmnNl2wqFn1mowGK55uPvDo4mofnG+qLul85uuvzt5+xi/9D7xt/l+4o3TOPvVD2JDl+g9+5y00AHicY2BkYGAA4vdhFhPj+W2+MnCzMIDAtbfeSgj6/wIWBuYEIJeDgQkkCgAriQovAHicY2BkYGBu+N/AEMPCAAJAkpEBFXAAAEcOAnF4nGNhYGBgfsnAwMKAHQMAGtcBCQAAAAAAdgDiAYoCogMIA1oD8gAAeJxjYGRgYOBgOMDAxgACTEDMBYQMDP/BfAYAHGwB5QB4nGWPTU7DMBCFX/oHpBKqqGCH5AViASj9EatuWFRq911036ZOmyqJI8et1ANwHo7ACTgC3IA78EgnmzaWx9+8eWNPANzgBx6O3y33kT1cMjtyDRe4F65TfxBukF+Em2jjVbhF/U3YxzOmwm10YXmD17hi9oR3YQ8dfAjXcI1P4Tr1L+EG+Vu4iTv8CrfQ8erCPuZeV7iNRy/2x1YvnF6p5UHFockikzm/gple75KFrdLqnGtbxCZTg6BfSVOdaVvdU+zXQ+ciFVmTqgmrOkmMyq3Z6tAFG+fyUa8XiR6EJuVYY/62xgKOcQWFJQ6MMUIYZIjK6Og7VWb0r7FDwl57Vj3N53RbFNT/c4UBAvTPXFO6stJ5Ok+BPV8bUnV0K27LnpQ0kV7NSRKyQl7WtlRC6gE2ZVeOEXpc0Yk/KGdI/wAJWm7IAAAAeJxtyMEOQDAQBNCZslX+UhE2kbaSbvD3JK7e8cHhM+BfoGPDlkLPjoE9ePlV62ZRzkVjljrdlryVPY+zHJrUxMpbwANFHA6a') format('woff'), url('iconfont.ttf?t=1523001890286') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ url('iconfont.svg?t=1523001890286#iconfont') format('svg'); /* iOS 4.1- */ } [class*=" el-icon-ui"], [class^=el-icon-ui] { font-family:"iconfont" !important; font-size:16px; font-style:normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .el-icon-ui-github:before { content: "\e7ab"; } .el-icon-ui-weibo:before { content: "\e61c"; } .el-icon-ui-tcyun:before { content: "\e64c"; } .el-icon-ui-upload:before { content: "\e61b"; } .el-icon-ui-qiniu:before { content: "\e601"; } .el-icon-ui-upyun:before { content: "\e602"; } ================================================ FILE: src/renderer/components/ConfigForm.vue ================================================ ================================================ FILE: src/renderer/components/ToolboxHandler.vue ================================================ ================================================ FILE: src/renderer/components/ToolboxStatusIcon.vue ================================================ ================================================ FILE: src/renderer/components/dialog/ConfigFormDialog.vue ================================================ ================================================ FILE: src/renderer/components/dialog/ConfirmDialog.vue ================================================ ================================================ FILE: src/renderer/components/dialog/InputBoxDialog.vue ================================================ ================================================ FILE: src/renderer/components/form/BaseConfigForm.vue ================================================ ================================================ FILE: src/renderer/components/picgoCloud/ConfigSyncConflictDialog.vue ================================================ ================================================ FILE: src/renderer/components/settings/ButtonFormItem.vue ================================================ ================================================ FILE: src/renderer/components/settings/SelectFormItem.vue ================================================ ================================================ FILE: src/renderer/components/settings/SwitchFormItem.vue ================================================ ================================================ FILE: src/renderer/hooks/useATagClick.ts ================================================ import { openURL } from '@/utils/common' import { onMounted, onUnmounted } from 'vue' export function useATagClick () { const handleATagClick = (e: MouseEvent) => { if (e.target instanceof HTMLAnchorElement) { if (e.target.href) { e.preventDefault() openURL(e.target.href) } } } onMounted(() => { document.addEventListener('click', handleATagClick) }) onUnmounted(() => { document.removeEventListener('click', handleATagClick) }) } ================================================ FILE: src/renderer/hooks/useConfigForm.ts ================================================ import { cloneDeep, union } from 'lodash' /** * * @param configList origin config list * @param formModel el-form form model for default value * @param currentConfig current config * @param resetConfigForm reset form model -> clear default value * @returns transformed config list */ export const useConfigForm = () => { return (configList: IPicGoPluginConfig[], formModel: IStringKeyMap, currentConfig?: IStringKeyMap, resetConfigForm?: boolean) => { if (configList.length > 0) { return cloneDeep(configList).map((item) => { // if (!configId) return item if (resetConfigForm) return item let defaultValue = item.default !== undefined ? item.default : item.type === 'checkbox' ? [] : null if (item.type === 'checkbox') { const defaults = item.choices?.filter((i: any) => { return i.checked }).map((i: any) => i.value) || [] defaultValue = union(defaultValue, defaults) } if (currentConfig && currentConfig[item.name] !== undefined) { defaultValue = currentConfig[item.name] } formModel[item.name] = defaultValue return item }) } return [] } } ================================================ FILE: src/renderer/hooks/useIPC.ts ================================================ import { ipcRenderer } from 'electron' import { onUnmounted } from 'vue' import { IRPCActionType } from '~/universal/types/enum' export const useIPCOn = (channel: string, listener: IpcRendererListener) => { ipcRenderer.on(channel, listener) onUnmounted(() => { ipcRenderer.removeListener(channel, listener) }) } export const useIPCOnce = (channel: string, listener: IpcRendererListener) => { ipcRenderer.once(channel, listener) onUnmounted(() => { ipcRenderer.removeListener(channel, listener) }) } /** * will auto removeListener when component unmounted */ export const useIPC = () => { return { on: (channel: IRPCActionType, listener: IpcRendererListener) => useIPCOn(channel, listener), once: (channel: IRPCActionType, listener: IpcRendererListener) => useIPCOnce(channel, listener) } } ================================================ FILE: src/renderer/hooks/useOS.ts ================================================ import { onBeforeMount, ref } from 'vue' export const useOS = () => { const os = ref('') onBeforeMount(() => { os.value = process.platform }) return os } ================================================ FILE: src/renderer/hooks/useStore.ts ================================================ import { inject } from 'vue' import { storeKey } from '@/store' export const useStore = () => { return inject(storeKey) ?? null } ================================================ FILE: src/renderer/hooks/useVModel.ts ================================================ import { computed, getCurrentInstance } from 'vue' export type VModelObject = object /** * v-model for single prop */ export function useVModel (props: T, key: K) { const vm = getCurrentInstance() return computed({ get: () => props[key], set: (value) => { vm?.emit(`update:${key as string}`, value) } }) } ================================================ FILE: src/renderer/hooks/useVModelValues.ts ================================================ import { getCurrentInstance, reactive, UnwrapNestedRefs, watch } from 'vue' /** * v-model for multiple props */ export function useVModelValues (props: T, keys: Array) { const vm = getCurrentInstance() const obj = {} as T keys.forEach(key => { obj[key] = props[key] }) const mutableValue = reactive(obj) as T watch(() => props, (val) => { keys.forEach(key => { mutableValue[key] = val[key] }) }, { deep: true }) function updateProps () { for (const key of keys) { vm?.emit(`update:${key as string}`, mutableValue[key]) } } return [mutableValue as UnwrapNestedRefs, updateProps] as const } ================================================ FILE: src/renderer/i18n/index.ts ================================================ import { ipcRenderer } from 'electron' import { ObjectAdapter, I18n } from '@picgo/i18n' import { GET_CURRENT_LANGUAGE, SET_CURRENT_LANGUAGE, FORCE_UPDATE, GET_LANGUAGE_LIST } from '#/events/constants' import bus from '@/utils/bus' import { builtinI18nList } from '#/i18n' export class I18nManager { private i18n: I18n | null = null private i18nFileList: II18nItem[] = builtinI18nList private getLanguageList () { ipcRenderer.send(GET_LANGUAGE_LIST) ipcRenderer.once(GET_LANGUAGE_LIST, (event, list: II18nItem[]) => { this.i18nFileList = list }) } private getCurrentLanguage () { ipcRenderer.send(GET_CURRENT_LANGUAGE) ipcRenderer.once(GET_CURRENT_LANGUAGE, (event, lang: string, locales: ILocales) => { this.setLocales(lang, locales) bus.emit(FORCE_UPDATE) }) } private setLocales (lang: string, locales: ILocales) { const objectAdapter = new ObjectAdapter({ [lang]: locales }) this.i18n = new I18n({ adapter: objectAdapter, defaultLanguage: lang }) } constructor () { this.getCurrentLanguage() this.getLanguageList() ipcRenderer.on(SET_CURRENT_LANGUAGE, (event, lang: string, locales: ILocales) => { this.setLocales(lang, locales) bus.emit(FORCE_UPDATE) }) } T (key: ILocalesKey, args: IStringKeyMap = {}): string { return this.i18n?.translate(key, args) || key } setCurrentLanguage (lang: string) { ipcRenderer.send(SET_CURRENT_LANGUAGE, lang) } get languageList () { return this.i18nFileList } } const i18nManager = new I18nManager() const T = (key: ILocalesKey, args: IStringKeyMap = {}): string => { return i18nManager.T(key, args) } export { i18nManager, T } ================================================ FILE: src/renderer/index.html ================================================ PicGo
================================================ FILE: src/renderer/layouts/Main.vue ================================================ ================================================ FILE: src/renderer/main.ts ================================================ import './assets/css/tailwind.css' import { createApp } from 'vue' import { webFrame } from 'electron' import App from './App.vue' import router from './router' import ElementUI from 'element-plus' import 'element-plus/dist/index.css' import VueLazyLoad from 'vue3-lazyload' import axios from 'axios' import { mainMixin } from './utils/mainMixin' 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 } from '@/utils/dataSender' import { store } from '@/store' import vue3PhotoPreview from 'vue3-photo-preview' import 'vue3-photo-preview/dist/index.css' import { getRendererStaticFileUrl } from './utils/static' webFrame.setVisualZoomLevelLimits(1, 1) const app = createApp(App) app.config.globalProperties.$builtInPicBed = [ 'smms', 'imgur', 'qiniu', 'tcyun', 'upyun', 'aliyun', 'github' ] app.config.globalProperties.$$db = db app.config.globalProperties.$http = axios app.config.globalProperties.$T = T app.config.globalProperties.$i18n = i18nManager app.config.globalProperties.getConfig = getConfig app.config.globalProperties.saveConfig = saveConfig app.config.globalProperties.sendToMain = sendToMain app.mixin(mainMixin) app.mixin(dragMixin) app.use(VueLazyLoad, { error: getRendererStaticFileUrl('unknown-file-type.svg') }) app.use(ElementUI) app.use(router) app.use(store) app.use(vue3PhotoPreview) app.mount('#app') initTalkingData() ================================================ FILE: src/renderer/pages/Gallery.vue ================================================ ================================================ FILE: src/renderer/pages/MiniPage.vue ================================================ ================================================ FILE: src/renderer/pages/PicGoCloud.vue ================================================ ================================================ FILE: src/renderer/pages/PicGoSetting.vue ================================================ ================================================ FILE: src/renderer/pages/Plugin.vue ================================================ ================================================ FILE: src/renderer/pages/RenamePage.vue ================================================ ================================================ FILE: src/renderer/pages/ShortKey.vue ================================================ ================================================ FILE: src/renderer/pages/Toolbox.vue ================================================ ================================================ FILE: src/renderer/pages/TrayPage.vue ================================================ ================================================ FILE: src/renderer/pages/Upload.vue ================================================ ================================================ FILE: src/renderer/pages/UploaderConfigPage.vue ================================================ ================================================ FILE: src/renderer/pages/UrlRewrite.vue ================================================ ================================================ FILE: src/renderer/pages/components/gallery/GalleryToolbar.vue ================================================ ================================================ FILE: src/renderer/pages/components/settings/buttonArea/ButtonAreaSettings.vue ================================================ ================================================ FILE: src/renderer/pages/components/settings/buttonArea/CheckUpdateDialog.vue ================================================ ================================================ FILE: src/renderer/pages/components/settings/buttonArea/CustomLinkDialog.vue ================================================ ================================================ FILE: src/renderer/pages/components/settings/buttonArea/LogSettingDialog.vue ================================================ ================================================ FILE: src/renderer/pages/components/settings/buttonArea/ProxySettingDialog.vue ================================================ ================================================ FILE: src/renderer/pages/components/settings/buttonArea/ServerSettingsDialog.vue ================================================ ================================================ FILE: src/renderer/pages/components/settings/customArea/ChoosePicBed.vue ================================================ ================================================ FILE: src/renderer/pages/components/settings/customArea/CustomAreaSettings.vue ================================================ ================================================ FILE: src/renderer/pages/components/settings/selectArea/SelectAreaSettings.vue ================================================ ================================================ FILE: src/renderer/pages/components/settings/switchArea/SwitchAreaSettings.vue ================================================ ================================================ FILE: src/renderer/pages/picbeds/index.vue ================================================ ================================================ FILE: src/renderer/router/config.ts ================================================ export const GALLERY_PAGE = 'GalleryPage' export const TRAY_PAGE = 'TrayPage' export const RENAME_PAGE = 'RenamePage' export const MINI_PAGE = 'MiniPage' export const MAIN_PAGE = 'MainPage' export const UPLOAD_PAGE = 'UploadPage' export const PICBEDS_PAGE = 'PicbedsPage' export const SETTING_PAGE = 'SettingPage' 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' ================================================ FILE: src/renderer/router/index.ts ================================================ import { createRouter, createWebHashHistory } from 'vue-router' import * as config from './config' export default createRouter({ history: createWebHashHistory(), routes: [ { path: '/', name: config.TRAY_PAGE, component: () => import(/* webpackChunkName: "tray" */ '@/pages/TrayPage.vue') }, { path: '/rename-page', name: config.RENAME_PAGE, component: () => import(/* webpackChunkName: "RenamePage" */ '@/pages/RenamePage.vue') }, { path: '/mini-page', name: config.MINI_PAGE, component: () => import(/* webpackChunkName: "MiniPage" */ '@/pages/MiniPage.vue') }, { path: '/main-page', name: config.MAIN_PAGE, component: () => import(/* webpackChunkName: "SettingPage" */ '@/layouts/Main.vue'), children: [ { path: 'upload', component: () => import(/* webpackChunkName: "Upload" */ '@/pages/Upload.vue'), name: config.UPLOAD_PAGE }, { path: 'picbeds/:type/:configId?', component: () => import(/* webpackChunkName: "Other" */ '@/pages/picbeds/index.vue'), name: config.PICBEDS_PAGE }, { path: 'gallery', component: () => import(/* webpackChunkName: "GalleryView" */ '@/pages/Gallery.vue'), name: config.GALLERY_PAGE, meta: { keepAlive: true } }, { path: 'setting', component: () => import(/* webpackChunkName: "setting" */ '@/pages/PicGoSetting.vue'), name: config.SETTING_PAGE }, { path: 'plugin', 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'), name: config.SHORTKEY_PAGE }, { path: 'urlRewrite', component: () => import(/* webpackChunkName: "UrlRewritePage" */ '@/pages/UrlRewrite.vue'), name: config.URL_REWRITE_PAGE }, { path: 'uploader-config-page/:type', component: () => import(/* webpackChunkName: "Other" */ '@/pages/UploaderConfigPage.vue'), name: config.UPLOADER_CONFIG_PAGE } ] }, { path: '/toolbox-page', name: config.TOOLBOX_CONFIG_PAGE, component: () => import(/* webpackChunkName: "ToolboxPage" */ '@/pages/Toolbox.vue') }, { path: '/:pathMatch(.*)*', redirect: '/' } ] }) ================================================ FILE: src/renderer/store/index.ts ================================================ 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: DeepReadonly> setDefaultPicBed: (type: string) => void; refreshAppConfig: () => Promise; refreshPicBeds: () => Promise; 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; } export const storeKey: InjectionKey = Symbol('store') // state const state: IState = reactive({ defaultPicBed: 'smms', appConfig: null, picBeds: [], picgoCloud: { userInfo: undefined, userInfoStatus: IPicGoCloudRequestStatus.IDLE, userInfoError: null, loginStatus: IPicGoCloudLoginStatus.IDLE, loginError: null, hasAgreedToTermsAndPrivacy: false } }) const forceUpdateTime = ref(Date.now()) // methods const setDefaultPicBed = (type: string) => { saveConfig({ 'picBed.current': type, 'picBed.uploader': type }) 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 => { const config = await getConfig() setAppConfig(config ?? null) } const refreshPicBeds = async (): Promise => { 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() } export const store = { install (app: App) { app.provide(storeKey, { state: readonly(state), setDefaultPicBed, refreshAppConfig, refreshPicBeds, setPicGoCloudUserInfo, setPicGoCloudUserInfoStatus, setPicGoCloudUserInfoError, setPicGoCloudLoginStatus, setPicGoCloudLoginError, setPicGoCloudHasAgreedToTermsAndPrivacy, updateForceUpdateTime }) app.provide('forceUpdateTime', forceUpdateTime) } } ================================================ FILE: src/renderer/utils/LS.ts ================================================ class LS { get (name: string) { if (localStorage.getItem(name)) { return JSON.parse(localStorage.getItem(name) as string) } else { return {} } } set (name: string, value: any) { return localStorage.setItem(name, JSON.stringify(value)) } } export default new LS() ================================================ FILE: src/renderer/utils/analytics.ts ================================================ import { REGISTER_DEVICE_ID, TALKING_DATA_APPID, TALKING_DATA_DEVICE_ID_EVENT, TALKING_DATA_EVENT } from '~/universal/events/constants' import pkg from 'root/package.json' import { ipcRenderer } from 'electron' import { handleTalkingDataEvent } from './common' const { version } = pkg export const initTalkingData = () => { setTimeout(() => { const talkingDataScript = document.createElement('script') talkingDataScript.src = `https://jic.talkingdata.com/app/h5/v1?appid=${TALKING_DATA_APPID}&vn=${version}&vc=${version}` const head = document.getElementsByTagName('head')[0] head.appendChild(talkingDataScript) }, 0) } ipcRenderer.on(TALKING_DATA_EVENT, (_, data: ITalkingDataOptions) => { handleTalkingDataEvent(data) }) // 0:ANONYMOUS,匿名账号; // 1:REGISTERED,自有帐户显性注册; ipcRenderer.on(TALKING_DATA_DEVICE_ID_EVENT, (_, deviceId: string) => { window.TDAPP.register({ profileId: deviceId, profileType: 1 }) window.TDAPP.login({ profileId: deviceId, profileType: 1 }) ipcRenderer.send(REGISTER_DEVICE_ID) }) ================================================ FILE: src/renderer/utils/bus.ts ================================================ import mitt from 'mitt' import { SHOW_INPUT_BOX, SHOW_INPUT_BOX_RESPONSE, FORCE_UPDATE } from '~/universal/events/constants' type IEvent ={ [SHOW_INPUT_BOX_RESPONSE]: string [SHOW_INPUT_BOX]: IShowInputBoxOption, [FORCE_UPDATE]: void } const emitter = mitt() export default emitter ================================================ FILE: src/renderer/utils/common.ts ================================================ import { isReactive, isRef, toRaw, unref } from 'vue' import { sendToMain } from './dataSender' import { OPEN_URL, PICGO_OPEN_FILE } from '~/universal/events/constants' import { webUtils } from 'electron' const isDevelopment = process.env.NODE_ENV !== 'production' export const handleTalkingDataEvent = (data: ITalkingDataOptions) => { const { EventId, Label = '', MapKv = {} } = data MapKv.from = window.location.href window.TDAPP.onEvent(EventId, Label, MapKv) if (isDevelopment) { console.log('talkingData', data) } } export const trimValues = (obj: IStringKeyMap) => { const newObj = {} as IStringKeyMap Object.keys(obj).forEach(key => { newObj[key] = typeof obj[key] === 'string' ? obj[key].trim() : obj[key] }) return newObj } /** * get raw data from reactive or ref */ export const getRawData = (args: any): any => { if (args === null) return args if (Array.isArray(args)) { const data = args.map((item: any) => { if (isRef(item)) { return getRawData(unref(item)) } if (isReactive(item)) { return getRawData(toRaw(item)) } return getRawData(item) }) return data } if (typeof args === 'object') { const data = {} as IStringKeyMap Object.keys(args).forEach(key => { const item = args[key] if (isRef(item)) { data[key] = getRawData(unref(item)) } else if (isReactive(item)) { data[key] = getRawData(toRaw(item)) } else { data[key] = getRawData(item) } }) return data } return args } export const openFile = (fileName: string) => { sendToMain(PICGO_OPEN_FILE, fileName) } export const openURL = (url: string) => { sendToMain(OPEN_URL, url) } export const getFilePath = (file: File) => { return webUtils.getPathForFile(file) } ================================================ FILE: src/renderer/utils/dataSender.ts ================================================ 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' import { getRawData } from './common' export async function saveConfig (_config: IObj | string, value?: any) { let config if (typeof _config === 'string') { config = { [_config]: getRawData(value) } } else { config = getRawData(_config) } await ipcRenderer.invoke(PICGO_SAVE_CONFIG, config) } export function getConfig (key?: string): Promise { return new Promise((resolve) => { const callbackId = uuid() const callback = (event: IpcRendererEvent, config: T | undefined, returnCallbackId: string) => { if (returnCallbackId === callbackId) { resolve(config) ipcRenderer.removeListener(PICGO_GET_CONFIG, callback) } } ipcRenderer.on(PICGO_GET_CONFIG, callback) ipcRenderer.send(PICGO_GET_CONFIG, key, callbackId) }) } export function getPicBeds (): Promise { return new Promise((resolve) => { 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 (action: IRPCActionType, ...args: any[]): Promise> { const data = getRawData(args) return ipcRenderer.invoke(RPC_ACTIONS, action, data) as Promise> } /** * send a rpc request & do not need to wait for the response * * or the response will be handled by other listener */ export function sendRPC (action: IRPCActionType, ...args: any[]): void { const data = getRawData(args) ipcRenderer.send(RPC_ACTIONS, action, data) } /** * @deprecated will be replaced by sendRPC in the future */ export function sendToMain (channel: string, ...args: any[]) { const data = getRawData(args) ipcRenderer.send(channel, ...data) } ================================================ FILE: src/renderer/utils/db.ts ================================================ import { PICGO_GET_BY_ID_DB, PICGO_GET_DB, PICGO_INSERT_DB, PICGO_INSERT_MANY_DB, PICGO_REMOVE_BY_ID_DB, PICGO_UPDATE_BY_ID_DB } from '#/events/constants' import { IGalleryDB } from '#/types/extra-vue' import { IFilter, IGetResult, IObject, IResult } from '@picgo/store/dist/types' import { IpcRendererEvent, ipcRenderer } from 'electron' import { v4 as uuid } from 'uuid' import { getRawData } from './common' export class GalleryDB implements IGalleryDB { async get (filter?: IFilter): Promise> { const res = await this.msgHandler>(PICGO_GET_DB, filter) return res } async insert (value: T): Promise> { const res = await this.msgHandler>(PICGO_INSERT_DB, value) return res } async insertMany (value: T[]): Promise[]> { const res = await this.msgHandler[]>(PICGO_INSERT_MANY_DB, value) return res } async updateById (id: string, value: IObject): Promise { const res = await this.msgHandler(PICGO_UPDATE_BY_ID_DB, id, value) return res } async getById (id: string): Promise | undefined> { const res = await this.msgHandler | undefined>(PICGO_GET_BY_ID_DB, id) return res } async removeById (id: string): Promise { const res = await this.msgHandler(PICGO_REMOVE_BY_ID_DB, id) return res } private msgHandler (method: string, ...args: any[]): Promise { return new Promise((resolve) => { const callbackId = uuid() const callback = (event: IpcRendererEvent, data: T, returnCallbackId: string) => { if (returnCallbackId === callbackId) { resolve(data) ipcRenderer.removeListener(method, callback) } } const data = getRawData(args) ipcRenderer.on(method, callback) ipcRenderer.send(method, ...data, callbackId) }) } } export default new GalleryDB() ================================================ FILE: src/renderer/utils/key-binding.ts ================================================ import keycode from 'keycode' const isSpecialKey = (keyCode: number) => { const keyArr = [ 16, // Shift 17, // Ctrl 18, // Alt 91, // Left Meta 93 // Right Meta ] return keyArr.includes(keyCode) } const keyDetect = (event: KeyboardEvent) => { // TODO: remove process const meta = process.platform === 'darwin' ? 'Cmd' : 'Super' const specialKey = { Ctrl: event.ctrlKey, Shift: event.shiftKey, Alt: event.altKey, [meta]: event.metaKey } const pressKey = [] for (const i in specialKey) { if (specialKey[i]) { pressKey.push(i) } } if (!isSpecialKey(event.keyCode)) { pressKey.push(keycode(event.keyCode).toUpperCase()) } return pressKey } export default keyDetect ================================================ FILE: src/renderer/utils/mainMixin.ts ================================================ import { ComponentOptions } from 'vue' import { FORCE_UPDATE, GET_PICBEDS } from '~/universal/events/constants' import bus from '~/renderer/utils/bus' import { ipcRenderer } from 'electron' export const mainMixin: ComponentOptions = { inject: ['forceUpdateTime'], created () { // FIXME: may be memory leak this?.$watch('forceUpdateTime', (newVal: number, oldVal: number) => { if (oldVal !== newVal) { this?.$forceUpdate() } }) }, methods: { forceUpdate () { bus.emit(FORCE_UPDATE) }, getPicBeds () { ipcRenderer.send(GET_PICBEDS) } } } ================================================ FILE: src/renderer/utils/mixin.ts ================================================ import { ComponentOptions } from 'vue' export const dragMixin: ComponentOptions = { mounted () { this.disableDragEvent() }, methods: { disableDragEvent () { window.addEventListener('dragenter', this.disableDrag, false) window.addEventListener('dragover', this.disableDrag) window.addEventListener('drop', this.disableDrag) }, disableDrag (e: DragEvent) { const dropzone = document.getElementById('upload-area') if (dropzone === null || !dropzone.contains(e.target)) { e.preventDefault() e.dataTransfer!.effectAllowed = 'none' e.dataTransfer!.dropEffect = 'none' } } }, beforeUnmount () { window.removeEventListener('dragenter', this.disableDrag, false) window.removeEventListener('dragover', this.disableDrag) window.removeEventListener('drop', this.disableDrag) } } ================================================ FILE: src/renderer/utils/notification.ts ================================================ import { ipcRenderer } from 'electron' import { v4 as uuid } from 'uuid' import { sendRPC } from './dataSender' import { PICGO_NOTIFICATION_CLICKED } from '~/universal/events/constants' import { IRPCActionType } from '~/universal/types/enum' const notificationCallbacks = new Map void>() const MAX_CALLBACK_LIMIT = 10 const handleNotificationClick = (_event: Electron.IpcRendererEvent, id: string) => { const callback = notificationCallbacks.get(id) if (!callback) return try { callback() } finally { notificationCallbacks.delete(id) } } // HMR Protection: Remove existing listener before adding a new one ipcRenderer.removeAllListeners(PICGO_NOTIFICATION_CLICKED) ipcRenderer.on(PICGO_NOTIFICATION_CLICKED, handleNotificationClick) interface NotificationOptions { title: string body: string callback?: () => void } export const showNotification = (options: NotificationOptions) => { const id = uuid() if (options.callback) { if (notificationCallbacks.size >= MAX_CALLBACK_LIMIT) { const oldestId = notificationCallbacks.keys().next().value if (oldestId) { notificationCallbacks.delete(oldestId) } } notificationCallbacks.set(id, options.callback) } sendRPC(IRPCActionType.SHOW_NOTIFICATION, options.title, options.body, id) } ================================================ FILE: src/renderer/utils/static.ts ================================================ export const getRendererStaticFileUrl = (fileName: string) => { return import.meta.env.BASE_URL + fileName } ================================================ FILE: src/renderer/utils/uploader.ts ================================================ import { v4 as uuid } from 'uuid' export const completeUploaderMetaConfig = (originData: IStringKeyMap): IStringKeyMap => { return Object.assign({ _configName: 'Default' }, originData, { _id: uuid(), _createdAt: Date.now(), _updatedAt: Date.now() }) } ================================================ FILE: src/universal/events/constants.ts ================================================ export const SHOW_INPUT_BOX = 'SHOW_INPUT_BOX' export const SHOW_INPUT_BOX_RESPONSE = 'SHOW_INPUT_BOX_RESPONSE' export const LOG_INVALID_URL_LINES = 'LOG_INVALID_URL_LINES' export const TOGGLE_SHORTKEY_MODIFIED_MODE = 'TOGGLE_SHORTKEY_MODIFIED_MODE' export const TALKING_DATA_APPID = '7E6832BCE3F1438696579E541DFEBFDA' export const TALKING_DATA_EVENT = 'TALKING_DATA_EVENT' export const TALKING_DATA_DEVICE_ID_EVENT = 'TALKING_DATA_DEVICE_ID_EVENT' export const SHOW_PRIVACY_MESSAGE = 'SHOW_PRIVACY_MESSAGE' export const PICGO_SAVE_CONFIG = 'PICGO_SAVE_CONFIG' export const PICGO_GET_CONFIG = 'PICGO_GET_CONFIG' export const PICGO_GET_DB = 'PICGO_GET_DB' export const PICGO_INSERT_DB = 'PICGO_INSERT_DB' export const PICGO_INSERT_MANY_DB = 'PICGO_INSERT_MANY_DB' export const PICGO_UPDATE_BY_ID_DB = 'PICGO_UPDATE_BY_ID_DB' export const PICGO_GET_BY_ID_DB = 'PICGO_GET_BY_ID_DB' export const PICGO_REMOVE_BY_ID_DB = 'PICGO_REMOVE_BY_ID_DB' export const PICGO_OPEN_FILE = 'PICGO_OPEN_FILE' export const OPEN_DEVTOOLS = 'OPEN_DEVTOOLS' export const SHOW_MINI_PAGE_MENU = 'SHOW_MINI_PAGE_MENU' export const SHOW_MAIN_PAGE_MENU = 'SHOW_MAIN_PAGE_MENU' export const SHOW_UPLOAD_PAGE_MENU = 'SHOW_UPLOAD_PAGE_MENU' export const SHOW_PLUGIN_PAGE_MENU = 'SHOW_PLUGIN_PAGE_MENU' export const MINIMIZE_WINDOW = 'MINIMIZE_WINDOW' export const CLOSE_WINDOW = 'CLOSE_WINDOW' export const OPEN_USER_STORE_FILE = 'OPEN_USER_STORE_FILE' export const OPEN_URL = 'OPEN_URL' export const PICGO_CONFIG_PLUGIN = 'PICGO_CONFIG_PLUGIN' export const PICGO_HANDLE_PLUGIN_ING = 'PICGO_HANDLE_PLUGIN_ING' export const PICGO_HANDLE_PLUGIN_DONE = 'PICGO_HANDLE_PLUGIN_DONE' export const PICGO_TOGGLE_PLUGIN = 'PICGO_TOGGLE_PLUGIN' export const PASTE_TEXT = 'PASTE_TEXT' export const SET_MINI_WINDOW_POS = 'SET_MINI_WINDOW_POS' export const RENAME_FILE_NAME = 'RENAME_FILE_NAME' 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' export const PICGO_NOTIFICATION_CLICKED = 'PICGO_NOTIFICATION_CLICKED' export const GET_PICBED_CONFIG = 'GET_PICBED_CONFIG' export const REGISTER_DEVICE_ID = 'REGISTER_DEVICE_ID' // i18n export const GET_CURRENT_LANGUAGE = 'GET_CURRENT_LANGUAGE' export const GET_LANGUAGE_LIST = 'GET_LANGUAGE_LIST' export const SET_CURRENT_LANGUAGE = 'SET_CURRENT_LANGUAGE' ================================================ FILE: src/universal/i18n/index.ts ================================================ export const builtinI18nList: II18nItem[] = [{ label: '简体中文', value: 'zh-CN' }, { label: '繁體中文', value: 'zh-TW' }, { label: 'English', value: 'en' }] ================================================ FILE: src/universal/types/cloud.ts ================================================ export interface IPicGoCloudUserInfo { user: string } export enum IPicGoCloudErrorCode { LOGIN_TIMEOUT = 'PICGO_CLOUD_LOGIN_TIMEOUT', LOGIN_FAILED = 'PICGO_CLOUD_LOGIN_FAILED' } ================================================ FILE: src/universal/types/cloudConfigSync.ts ================================================ 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 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 } ================================================ FILE: src/universal/types/electron.d.ts ================================================ // https://stackoverflow.com/questions/45420448/how-to-import-external-type-into-global-d-ts-file declare type BrowserWindow = import('electron').BrowserWindow declare type IWindowList = import('./enum').IWindowList declare interface IWindowListItem { isValid: boolean multiple: boolean options: () => IBrowserWindowOptions, callback: (window: BrowserWindow, windowManager: IWindowManager) => void } declare interface IWindowManager { create: (name: IWindowList) => BrowserWindow | null get: (name: IWindowList) => BrowserWindow | null has: (name: IWindowList) => boolean // delete: (name: IWindowList) => void deleteById: (id: number) => void getAvailableWindow: () => BrowserWindow } type IpcRendererListener = (event: import('electron').IpcRendererEvent, ...args: any[]) => void ================================================ FILE: src/universal/types/enum.ts ================================================ export enum IChalkType { success = 'green', info = 'blue', warn = 'yellow', error = 'red' } export enum IPicGoHelperType { afterUploadPlugins = 'afterUploadPlugins', beforeTransformPlugins = 'beforeTransformPlugins', beforeUploadPlugins = 'beforeUploadPlugins', uploader = 'uploader', transformer = 'transformer' } export enum IPasteStyle { MARKDOWN = 'markdown', HTML = 'HTML', URL = 'URL', UBB = 'UBB', CUSTOM = 'Custom' } export enum IWindowList { SETTING_WINDOW = 'SETTING_WINDOW', TRAY_WINDOW = 'TRAY_WINDOW', MINI_WINDOW = 'MINI_WINDOW', RENAME_WINDOW = 'RENAME_WINDOW', TOOLBOX_WINDOW = 'TOOLBOX_WINDOW' } export enum IRemoteNoticeActionType { OPEN_URL = 'OPEN_URL', SHOW_NOTICE = 'SHOW_NOTICE', // notification SHOW_DIALOG = 'SHOW_DIALOG', // dialog notice COMMON = 'COMMON', VOID = 'VOID', // do nothing SHOW_MESSAGE_BOX = 'SHOW_MESSAGE_BOX' } export enum IRemoteNoticeTriggerHook { APP_START = 'APP_START', SETTING_WINDOW_OPEN = 'SETTING_WINDOW_OPEN', } export enum IRemoteNoticeTriggerCount { ONCE = 'ONCE', // default ALWAYS = 'ALWAYS' } /** * renderer trigger action from main */ export enum IRPCActionType { // config rpc GET_PICBED_CONFIG_LIST = 'GET_PICBED_CONFIG_LIST', DELETE_PICBED_CONFIG = 'DELETE_PICBED_CONFIG', CHANGE_CURRENT_UPLOADER = 'CHANGE_CURRENT_UPLOADER', SELECT_UPLOADER = 'SELECT_UPLOADER', UPDATE_UPLOADER_CONFIG = 'UPDATE_UPLOADER_CONFIG', COPY_UPLOADER_CONFIG = 'COPY_UPLOADER_CONFIG', // version rpc GET_LATEST_VERSION = 'GET_LATEST_VERSION', // toolbox rpc TOOLBOX_CHECK = 'TOOLBOX_CHECK', TOOLBOX_CHECK_RES = 'TOOLBOX_CHECK_RES', TOOLBOX_CHECK_FIX = 'TOOLBOX_CHECK_FIX', // system rpc RELOAD_APP = 'RELOAD_APP', OPEN_FILE = 'OPEN_FILE', COPY_TEXT = 'COPY_TEXT', SHOW_DOCK_ICON = 'SHOW_DOCK_ICON', 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', OPEN_CONFIG_DIALOG = 'OPEN_CONFIG_DIALOG', } export enum IToolboxItemType { IS_CONFIG_FILE_BROKEN = 'IS_CONFIG_FILE_BROKEN', IS_GALLERY_FILE_BROKEN = 'IS_GALLERY_FILE_BROKEN', HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD = 'HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD', HAS_PROBLEM_WITH_PROXY = 'HAS_PROBLEM_WITH_PROXY', } export enum IToolboxItemCheckStatus { INIT = 'init', LOADING = 'loading', SUCCESS = 'success', ERROR = 'error', } export enum IStartupMode { SHOW_MAIN_WINDOW = 'SHOW_SETTING_WINDOW', SHOW_MINI_WINDOW = 'SHOW_MINI_WINDOW', HIDE = 'HIDE' } ================================================ FILE: src/universal/types/extra-vue.d.ts ================================================ import axios from 'axios' import { IObject, IResult, IGetResult, IFilter } from '@picgo/store/dist/types' interface IGalleryDB { get(filter?: IFilter): Promise> insert (value: T): Promise> insertMany (value: T[]): Promise[]> updateById (id: string, value: IObject): Promise getById (id: string): Promise | undefined> removeById (id: string): Promise } declare module 'vue/types/vue' { interface Vue { } } declare module 'vue' { interface ComponentCustomProperties { $http: typeof axios $builtInPicBed: string[] $$db: IGalleryDB $T: typeof import('~/renderer/i18n/index').T $i18n: import('~/renderer/i18n/index').I18nManager saveConfig(data: IObj | string, value?: any): void getConfig(key?: string): Promise setDefaultPicBed(picBed: string): void defaultPicBed: string forceUpdate(): void sendToMain(channel: string, ...args: any[]): void } interface GlobalComponents { PhotoProvider: typeof import('vue3-photo-preview').PhotoProvider PhotoConsumer: typeof import('vue3-photo-preview').PhotoConsumer PhotoSlider: typeof import('vue3-photo-preview').PhotoSlider } } ================================================ FILE: src/universal/types/global.d.ts ================================================ // https://stackoverflow.com/questions/35074713/extending-typescript-global-object-in-node-js/44387594#44387594 declare var PICGO_GUI_VERSION: string declare var PICGO_CORE_VERSION: string declare var notificationList: IAppNotification[] declare module 'epipebomb' { export default function epipebomb(stream: NodeJS.Process['stdout'], callback: () => void): void } // 扩展原生 IncomingMessage,添加 multer 字段 declare module 'http' { interface IncomingMessage { files?: { path: string; originalname: string; mimetype: string; size: number; [key: string]: any; }[]; body?: any; } } ================================================ FILE: src/universal/types/i18n.d.ts ================================================ interface ILocales { LANG_DISPLAY_LABEL: string ABOUT: string OPEN_MAIN_WINDOW: string CHOOSE_DEFAULT_PICBED: string OPEN_UPDATE_HELPER: string PRIVACY_TERMS_AGREEMENT: string RELOAD_APP: string UPLOAD_SUCCEED: string UPLOAD_FAILED: string UPLOAD_PROGRESS: string OPERATION_SUCCEED: string OPERATION_FAILED: string UPLOADING: string QUICK_UPLOAD: string UPLOAD_BY_CLIPBOARD: string HIDE_WINDOW: string SPONSOR_PICGO: string SHOW_PICBED_QRCODE: string PICBED_QRCODE: string ENABLE: string DISABLE: string CONFIG_THING: string FIND_NEW_VERSION: string NO_MORE_NOTICE: string SHOW_DEVTOOLS: string CURRENT_PICBED: string OPEN_TOOLBOX: string CHOOSE_YOUR_DEFAULT_PICBED: string UPLOAD_AREA: string GALLERY: string 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 CHOOSE_PICBED: string COPY_PICBED_CONFIG: string COPY_PICBED_CONFIG_SUCCEED: string INPUT: string CANCEL: string CONFIRM: string CHOOSE_SHOWED_PICBED: string CHOOSE_PASTE_FORMAT: string SEARCH: string COPY: string DELETE: string SELECT_ALL: string CHANGE_IMAGE_URL: string CHANGE_IMAGE_URL_SUCCEED: string COPY_LINK_SUCCEED: string BATCH_COPY_LINK_SUCCEED: string FILE_RENAME: string COPY_FILE_PATH: string OPEN_FILE_PATH: string SUCCESS: string FAILED: string SETTINGS: string SETTINGS_OPEN_CONFIG_FILE: string SETTINGS_CLICK_TO_OPEN: string SETTINGS_SET_LOG_FILE: string SETTINGS_CLICK_TO_SET: string SETTINGS_CLICK_TO_CHECK: string SETTINGS_SET_SHORTCUT: string SETTINGS_URL_REWRITE: string SETTINGS_CUSTOM_LINK_FORMAT: string SETTINGS_SET_PROXY_AND_MIRROR: string SETTINGS_SET_SERVER: string SETTINGS_CHECK_UPDATE: string SETTINGS_OPEN_UPDATE_HELPER: string SETTINGS_OPEN: string SETTINGS_CLOSE: string SETTINGS_ACCEPT_BETA_UPDATE: string SETTINGS_LAUNCH_ON_BOOT: string SETTINGS_RENAME_BEFORE_UPLOAD: string SETTINGS_TIMESTAMP_RENAME: string SETTINGS_OPEN_UPLOAD_TIPS: string SETTINGS_NOTIFICATION_SOUND: string SETTINGS_MINI_WINDOW_ON_TOP: string SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: string SETTINGS_TIPS_PLACEHOLDER_URL: string SETTINGS_TIPS_PLACEHOLDER_FILENAME: string SETTINGS_TIPS_PLACEHOLDER_EXTNAME: string SETTINGS_TIPS_SUCH_AS: string SETTINGS_UPLOAD_PROXY: string SETTINGS_PLUGIN_INSTALL_PROXY: string SETTINGS_PLUGIN_INSTALL_MIRROR: string SETTINGS_CURRENT_VERSION: string SETTINGS_NEWEST_VERSION: string SETTINGS_GETING: string SETTINGS_TIPS_HAS_NEW_VERSION: string SETTINGS_LOG_FILE: string SETTINGS_LOG_LEVEL: string SETTINGS_LOG_FILE_SIZE: string SETTINGS_SET_PICGO_SERVER: string SETTINGS_TIPS_SERVER_NOTICE: string SETTINGS_ENABLE_SERVER: string SETTINGS_SET_SERVER_HOST: string SETTINGS_SET_SERVER_PORT: string SETTINGS_TIP_PLACEHOLDER_HOST: string SETTINGS_TIP_PLACEHOLDER_PORT: string SETTINGS_LOG_LEVEL_ALL: string SETTINGS_LOG_LEVEL_SUCCESS: string SETTINGS_LOG_LEVEL_ERROR: string SETTINGS_LOG_LEVEL_INFO: string SETTINGS_LOG_LEVEL_WARN: string SETTINGS_LOG_LEVEL_NONE: string SETTINGS_RESULT: string SETTINGS_DEFAULT_PICBED: string SETTINGS_SET_DEFAULT_PICBED: string SETTINGS_NOT_CONFIG_OPTIONS: string SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: string SETTINGS_CHOOSE_LANGUAGE: string BUILTIN_CLIPBOARD_TIPS: string UPLOADER_CONFIG_NAME: string URL_REWRITE_HELP: string URL_REWRITE_ADD_RULE: string URL_REWRITE_EDIT_RULE: string URL_REWRITE_EMPTY: string URL_REWRITE_ORDER: string URL_REWRITE_MATCH: string URL_REWRITE_REPLACE: string URL_REWRITE_FLAGS: string URL_REWRITE_ENABLED: string URL_REWRITE_ACTIONS: string URL_REWRITE_MOVE_UP: string URL_REWRITE_MOVE_DOWN: string URL_REWRITE_EDIT: string URL_REWRITE_DELETE: string URL_REWRITE_DELETE_CONFIRM: string URL_REWRITE_MATCH_TIPS: string URL_REWRITE_MATCH_PLACEHOLDER: string URL_REWRITE_REPLACE_TIPS: string URL_REWRITE_REPLACE_PLACEHOLDER: string URL_REWRITE_OPTIONS: string URL_REWRITE_RULE_ENABLED: string URL_REWRITE_FLAG_GLOBAL_LABEL: string URL_REWRITE_FLAG_GLOBAL_DESC: string URL_REWRITE_FLAG_IGNORE_CASE_LABEL: string URL_REWRITE_FLAG_IGNORE_CASE_DESC: string URL_REWRITE_MATCH_REQUIRED: string URL_REWRITE_REPLACE_REQUIRED: string URL_REWRITE_INVALID_REGEX: string URL_REWRITE_PREVIEW_TITLE: string URL_REWRITE_PREVIEW_TIPS: string URL_REWRITE_PREVIEW_PLACEHOLDER: string URL_REWRITE_PREVIEW_RUN: string URL_REWRITE_PREVIEW_OUTPUT: string URL_REWRITE_PREVIEW_INPUT_REQUIRED: string URL_REWRITE_PREVIEW_RULE_INVALID: string URL_REWRITE_PREVIEW_MATCHED_RULE: string URL_REWRITE_PREVIEW_NO_MATCH: string UPLOADER_CONFIG_PLACEHOLDER: string SELECTED_SETTING_HINT: string SETTINGS_ENCODE_OUTPUT_URL: string SETTINGS_SHOW_DOCK_ICON: string SETTINGS_SHOW_MENUBAR_ICON: string SETTINGS_SHOW_MENUBAR_ICON_TIPS: string SETTINGS_STARTUP_MODE: string SETTINGS_STARTUP_MODE_MAIN_WINDOW: string SETTINGS_STARTUP_MODE_MINI_WINDOW: string SETTINGS_STARTUP_MODE_HIDE: string SHORTCUT_NAME: string SHORTCUT_BIND: string SHORTCUT_STATUS: string SHORTCUT_ENABLED: string SHORTCUT_DISABLED: string SHORTCUT_SOURCE: string SHORTCUT_HANDLE: string SHORTCUT_ENABLE: string SHORTCUT_DISABLE: string SHORTCUT_EDIT: string SHORTCUT_CHANGE_UPLOAD: string GALLERY_URL_REWRITE_TITLE: string GALLERY_URL_REWRITE_RESULT_TITLE: string GALLERY_URL_REWRITE_WARN_NO_SELECTION: string GALLERY_URL_REWRITE_APPLY_GLOBAL_RULES: string GALLERY_URL_REWRITE_GLOBAL_RULES_COUNT: string GALLERY_URL_REWRITE_TEMP_RULE_TIPS: string GALLERY_URL_REWRITE_TEMP_RULE_REQUIRED: string GALLERY_URL_REWRITE_NO_RULES_TO_APPLY: string GALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: string GALLERY_URL_REWRITE_APPLY_AND_SAVE: string GALLERY_URL_REWRITE_APPLY_ONLY: string GALLERY_URL_REWRITE_NO_CHANGES: string GALLERY_URL_REWRITE_EMPTY_RESULT_WARN: string WAIT_TO_UPLOAD: string ALREADY_UPLOAD: string PICTURE_UPLOAD: string DRAG_FILE_TO_HERE: string CLICK_TO_UPLOAD: string LINK_FORMAT: string CUSTOM: string CLIPBOARD_PICTURE: string TIPS_DRAG_VALID_PICTURE_OR_URL: string TIPS_INPUT_URL: string TIPS_HTTP_PREFIX: string TIPS_INPUT_VALID_URL: string PLUGIN_SEARCH_PLACEHOLDER: string PLUGIN_INSTALL: string PLUGIN_INSTALLING: string PLUGIN_INSTALLED: string PLUGIN_DOING_SOMETHING: string PLUGIN_LIST: string PLUGIN_IMPORT_LOCAL: string TIPS_REMOVE_LINK: string TIPS_WILL_REMOVE_CHOOSED_IMAGES: string TIPS_MUST_CONTAINS_URL: string TIPS_NETWORK_ERROR: string TIPS_NEED_RELOAD: string TIPS_PLEASE_CHOOSE_LOG_LEVEL: string TIPS_SET_SUCCEED: string TIPS_PLUGIN_NOT_GUI_IMPLEMENT: string TIPS_CLICK_NOTIFICATION_TO_RELOAD: string TIPS_GET_PLUGIN_LIST_FAILED: string PLUGIN_INSTALL_SUCCEED: string PLUGIN_INSTALL_FAILED: string PLUGIN_UNINSTALL_SUCCEED: string PLUGIN_UNINSTALL_FAILED: string PLUGIN_UPDATE_SUCCEED: string PLUGIN_UPDATE_FAILED: string PLUGIN_IMPORT_SUCCEED: string PLUGIN_IMPORT_FAILED: string ENABLE_PLUGIN: string DISABLE_PLUGIN: string UNINSTALL_PLUGIN: string UPDATE_PLUGIN: string TOOLBOX: string TOOLBOX_TITLE: string TOOLBOX_SUB_TITLE: string TOOLBOX_CHECK_CONFIG_FILE_BROKEN: string TOOLBOX_CHECK_GALLERY_FILE_BROKEN: string TOOLBOX_CHECK_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: string TOOLBOX_CHECK_PROBLEM_WITH_PROXY: string TOOLBOX_FIX_DONE_NEED_RELOAD: string TOOLBOX_CANT_AUTO_FIX: string TOOLBOX_START_SCAN: string TOOLBOX_RE_SCAN: string TOOLBOX_START_FIX: string TOOLBOX_SUCCESS_TIPS: string TOOLBOX_CHECK_CONFIG_FILE_PATH_TIPS: string TOOLBOX_CHECK_CONFIG_FILE_BROKEN_TIPS: string TOOLBOX_CHECK_GALLERY_FILE_PATH_TIPS: string TOOLBOX_CHECK_GALLERY_FILE_BROKEN_TIPS: string TOOLBOX_CHECK_PROXY_SUCCESS_TIPS: string TOOLBOX_CHECK_PROXY_NO_PROXY_TIPS: string TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_CORRECT: string TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_WORKING: string TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_TIPS: string TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS: string TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS: string TIPS_NOTICE: string TIPS_WARNING: string TIPS_ERROR: string TIPS_SKIPPED_INVALID_URLS: string TIPS_TOO_MANY_URLS_CONFIRM: string TIPS_NO_VALID_URLS: string TIPS_INSTALL_NODE_AND_RELOAD_PICGO: string TIPS_PLUGIN_REMOVE_GALLERY_ITEM: string TIPS_PLUGIN_OVERWRITE_GALLERY: string TIPS_UPLOAD_NOT_PICTURES: string TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: string TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: string TIPS_PICGO_BACKUP_FILE_VERSION: string TIPS_CUSTOM_CONFIG_FILE_PATH_ERROR: string TIPS_SHORTCUT_MODIFIED_SUCCEED: string TIPS_SHORTCUT_MODIFIED_CONFLICT: string TIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED: string 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 } type ILocalesKey = keyof ILocales ================================================ FILE: src/universal/types/rpc.d.ts ================================================ type IRPCResult = | { success: true, data: T } | { success: false, error: string } type IGetUploaderConfigListArgs = [type: 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] type ICopyTextArgs = [text: string] type IShowDockIconArgs = [visible: boolean] type IShowMenubarIconArgs = [visible: boolean] type IShowNotificationArgs = [title: string, body: string, id?: string] type IGetGalleryMenuListArgs = [selectedList: IGalleryItem[]] interface IRPCServer { start: () => void stop: () => void use: (routes: IRPCRoutes) => void } type IRPCRoutes = Map> type IRPCHandler = (args: any[], event: import('electron').IpcMainEvent | import('electron').IpcMainInvokeEvent) => Promise interface IRPCRouter { add(action: import('./enum').IRPCActionType, handler: IRPCHandler): IRPCRouter routes: () => IRPCRoutes } type IToolboxChecker = (event: import('electron').IpcMainEvent | import('electron').IpcMainInvokeEvent) => Promise type IToolboxCheckerMap = { [type in T]: IToolboxChecker } type IToolboxFixMap = { [type in T]: IToolboxChecker } type IToolboxCheckRes = { type: import('./enum').IToolboxItemType status: import('./enum').IToolboxItemCheckStatus, msg?: string value?: any } ================================================ FILE: src/universal/types/shims-module.d.ts ================================================ declare module '*.vue' { import { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component } ================================================ FILE: src/universal/types/shims-tsx.d.ts ================================================ import Vue, { VNode } from 'vue' declare global { interface ElectronApi { getFilePath: (file: File) => string } namespace JSX { // tslint:disable no-empty-interface interface Element extends VNode {} // tslint:disable no-empty-interface interface ElementClass extends Vue {} interface IntrinsicElements { [elem: string]: any } } interface Window { electronApi: ElectronApi TDAPP: { onEvent: (EventId: string, Label?: string, MapKv?: IStringKeyMap) => void register: (opt: { profileId: string, profileType: number, }) => void login: (opt: { profileId: string, profileType: number, }) => void } } } ================================================ FILE: src/universal/types/types.d.ts ================================================ // global type FN = (...args: any) => any interface IObj { [propName: string]: any } interface IObjT { [propName: string]: T } declare interface ErrnoException extends Error { errno?: number | string; code?: string; path?: string; syscall?: string; stack?: string; } declare namespace NodeJS { interface ProcessEnv { STATIC_PATH?: string } } declare const __static: string declare type ILogType = 'success' | 'info' | 'warn' | 'error' // Server type routeHandler = (ctx: IServerCTX) => Promise type IHttpResponse = import('http').ServerResponse interface IServerCTX { response: IHttpResponse [propName: string]: any } interface IServerConfig { port: number | string host: string enable: boolean } // Image && PicBed interface ImgInfo { buffer?: Buffer base64Image?: string fileName?: string width?: number height?: number extname?: string imgUrl?: string id?: string type?: string originImgUrl?: string [propName: string]: any } interface IGalleryItem extends ImgInfo { src: string key: string intro: string } interface IPicBedType { type: string name: string visible: boolean } // Config Settings interface IShortKeyConfig { enable: boolean key: string // 按键 name: string label: string from?: string } interface IPluginShortKeyConfig { key: string name: string label: string handle: IShortKeyHandler } interface IShortKeyConfigs { [propName: string]: IShortKeyConfig } interface IOldShortKeyConfigs { upload: string } interface IKeyCommandType { key: string, command: string } // Main process interface IBrowserWindowOptions { height: number, width: number, show: boolean, fullscreenable: boolean, resizable: boolean, webPreferences: { preload?: string nodeIntegration: boolean, nodeIntegrationInWorker: boolean, contextIsolation: boolean, backgroundThrottling: boolean webSecurity?: boolean }, vibrancy?: string | any, frame?: boolean center?: boolean title?: string titleBarStyle?: string | any backgroundColor?: string autoHideMenuBar?: boolean transparent?: boolean icon?: string skipTaskbar?: boolean alwaysOnTop?: boolean } interface IFileWithPath { path: string name?: string } interface IBounds { x: number y: number } // PicGo Types type ICtx = import('picgo').PicGo interface IPicGoPlugin { name: string fullName: string author: string description: string logo: string version: string | number gui: boolean config: { plugin: IPluginMenuConfig uploader: IPluginMenuConfig transformer: IPluginMenuConfig [index: string]: IPluginMenuConfig } | { [propName: string]: any } enabled?: boolean homepage: string guiMenu?: any[] ing: boolean hasInstall?: boolean } interface IPicGoPluginConfig { name: string type: string required: boolean default?: any alias?: string choices?: { name?: string value?: any }[] /** support markdown */ tips?: string [propName: string]: any } interface IPicGoPluginShowConfigDialogOption { title: string config: IPicGoPluginConfig[] tips?: string confirmText?: string cancelText?: string /** * default to 500 */ width?: number } interface IPicGoPluginOriginConfig { name: string type: string required: boolean default?: any alias?: string choices?: { name?: string value?: any }[] | (() => { name?: string value?: any }[]) [propName: string]: any } interface IPluginMenuConfig { name: string fullName?: string config: any[] } interface INPMSearchResult { data: { objects: INPMSearchResultObject[] } } interface INPMSearchResultObject { package: { name: string scope: string version: string description: string keywords: string[] maintainers: Array<{ email: string username: string }> links: { npm: string homepage: string } } } type IDispose = () => void // GuiApi interface IGuiApi { showInputBox: (options: IShowInputBoxOption) => Promise showFileExplorer: (options: IShowFileExplorerOption) => Promise upload: (input: IUploadOption) => Promise showNotification: (options?: IShowNotificationOption) => void showMessageBox: (options?: IShowMessageBoxOption) => Promise showConfigDialog: (options: IPicGoPluginShowConfigDialogOption) => Promise galleryDB: import('@picgo/store').DBStore } interface IShowInputBoxOption { value?: string title: string placeholder: string 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 */ width?: number } type IShowFileExplorerOption = IObj type IUploadOption = string[] | ImgInfo[] interface IShowNotificationOption { title: string body: string /** * will log text */ text?: string } interface IPrivateShowNotificationOption extends IShowNotificationOption{ /** * click notification to copy the body */ clickToCopy?: boolean copyContent?: string // something to copy callback?: () => void } interface IShowMessageBoxOption { title: string message: string type: import('electron').MessageBoxOptions['type'] buttons: string[] } interface IShowMessageBoxResult { result: number checkboxChecked: boolean } interface IShortKeyHandlerObj { handle: IShortKeyHandler key: string label: string } type IShortKeyHandler = (ctx: ICtx, guiApi?: IGuiApi) => Promise interface shortKeyHandlerMap { from: string handle: IShortKeyHandler } // PicBeds interface IAliYunConfig { accessKeyId: string accessKeySecret: string, bucket: string, area: string, path: string, customUrl: string options: string } interface IGitHubConfig { repo: string, token: string, path: string, customUrl: string, branch: string } interface IImgurConfig { clientId: string, proxy: string } interface IQiniuConfig { accessKey: string, secretKey: string, bucket: string, url: string, area: string, options: string, path: string } interface ISMMSConfig { token: string } interface ITcYunConfig { secretId: string, secretKey: string, bucket: string, appId: string, area: string, path: string, customUrl: string, version: 'v4' | 'v5', options: string } interface IUpYunConfig { bucket: string, operator: string, password: string, options: string, path: string } type ILoggerType = string | Error | boolean | number | undefined interface IAppNotification { title: string body: string text?: string } interface ITalkingDataOptions { EventId: string Label?: string MapKv?: IStringKeyMap } interface IAnalyticsData { fromClipboard: boolean type: string count: number duration?: number // 耗时 } interface IStringKeyMap { [propName: string]: any } type ILogArgvType = string | number type ILogArgvTypeWithError = ILogArgvType | Error interface IMiniWindowPos { x: number, y: number, height: number, width: number } type PromiseResType = T extends Promise ? R : T // type ILocalesKey = import('#/i18n/zh-CN').ILocalesKey interface II18nItem { label: string value: string } interface IRemoteNotice { version: number list: Array<{ versions: string[] // matched picgo version actions: IRemoteNoticeAction[] versionMatch?: 'exact' | 'gte' | 'lte' }> } interface IRemoteNoticeAction { type: import('#/types/enum').IRemoteNoticeActionType // trigger time hooks: import('#/types/enum').IRemoteNoticeTriggerHook[] id: string // trigger count: always or once; default: once triggerCount: import('#/types/enum').IRemoteNoticeTriggerCount data?: { title?: string content?: string desc?: string // action desc buttons?: IRemoteNoticeButton[] url?: string copyToClipboard?: string options: any // for other case } } interface IRemoteNoticeButton { label: string labelEN?: string type: 'confirm' | 'cancel' | 'other' action: IRemoteNoticeAction } interface IRemoteNoticeLocalCountStorage { [id: string]: true | number } interface IUploaderListItemMetaInfo { _id: string _configName: string _updatedAt: number _createdAt: number } interface IUploaderConfig { [picBedType: string]: IUploaderConfigItem } interface IUploaderConfigItem { configList: IUploaderConfigListItem[] defaultId: string } type IUploaderConfigListItem = IStringKeyMap & IUploaderListItemMetaInfo type ISwitchValueType = boolean | string | number ================================================ FILE: src/universal/types/view.d.ts ================================================ interface ISettingForm { showUpdateTip: boolean showPicBedList: string[] autoStart: boolean rename: boolean autoRename: boolean uploadNotification: boolean notificationSound: boolean miniWindowOnTop: boolean logLevel: string[] autoCopyUrl: boolean checkBetaUpdate: boolean useBuiltinClipboard: boolean language: string logFileSizeLimit: number encodeOutputURL: boolean showDockIcon: boolean showMenubarIcon: boolean customLink: string npmProxy: string npmRegistry: string server: { port: number host: string enable: boolean } startupMode: import('#/types/enum').IStartupMode } interface IShortKeyMap { [propName: string]: string } interface IToolboxItem { title: string status: import('#/types/enum').IToolboxItemCheckStatus msg?: string value?: any // for handler hasNoFixMethod?: boolean handler?: (value: any) => Promise | void handlerText?: string } type IToolboxMap = { [id in import('#/types/enum').IToolboxItemType]: IToolboxItem } interface IFormInstance { validate: () => Promise } ================================================ FILE: src/universal/utils/common.ts ================================================ export const isUrl = (url: string): boolean => (url.startsWith('http://') || url.startsWith('https://')) export interface IParseNewlineSeparatedUrlsResult { urls: string[] invalidLines: string[] } export interface IParseNewlineSeparatedUrlsOptions { source?: 'plain' | 'uri-list' } export const parseNewlineSeparatedUrls = ( input: string, options: IParseNewlineSeparatedUrlsOptions = {} ): IParseNewlineSeparatedUrlsResult => { const source = options.source || 'plain' const urls: string[] = [] const invalidLines: string[] = [] const seen = new Set() const splitConcatenatedUrls = (line: string): string[] => { const indexes: number[] = [] const re = /https?:\/\//g let match: RegExpExecArray | null = null while ((match = re.exec(line))) { const index = match.index if (index === 0) { indexes.push(index) continue } const prevChar = line[index - 1] if (prevChar === '=' || prevChar === '?' || prevChar === '&' || prevChar === '#') continue indexes.push(index) } if (indexes.length <= 1) return [line] return indexes.map((startIndex, i) => { const endIndex = indexes[i + 1] || line.length return line.slice(startIndex, endIndex) }) } const normalizedInput = input .split('\u0000').join('\n') .replace(/\r\n/g, '\n') .replace(/\r/g, '\n') normalizedInput.split('\n').forEach((rawLine) => { const line = rawLine.trim() if (!line) return if (source === 'uri-list' && line.startsWith('#')) return if (!isUrl(line)) { invalidLines.push(rawLine) return } const parts = splitConcatenatedUrls(line) parts.forEach((part) => { if (seen.has(part)) return urls.push(part) seen.add(part) }) }) return { urls, invalidLines } } export const extractHttpUrlsFromText = (text: string): string[] => { const urls: string[] = [] const seen = new Set() const matches = text.match(/https?:\/\/[^\s<>"']+/g) || [] matches.forEach((match) => { const maybeUrl = match.replace(/[)\]}>.,;:!?]+$/, '') if (seen.has(maybeUrl)) return if (!isUrl(maybeUrl)) return urls.push(maybeUrl) seen.add(maybeUrl) }) return urls } export const isUrlEncode = (url: string): boolean => { url = url || '' try { return url !== decodeURI(url) } catch (e) { // if some error caught, try to let it go return false } } export const handleUrlEncode = (url: string): string => { if (!isUrlEncode(url)) { url = encodeURI(url) } return url } /** * streamline the full plugin name to a simple one * for example: * 1. picgo-plugin-xxx -> xxx * 2. @xxx/picgo-plugin-yyy -> yyy * @param name pluginFullName */ export const handleStreamlinePluginName = (name: string) => { if (/^@[^/]+\/picgo-plugin-/.test(name)) { return name.replace(/^@[^/]+\/picgo-plugin-/, '') } else { return name.replace(/picgo-plugin-/, '') } } /** * for just simple clone an object */ export const simpleClone = (obj: any) => { return JSON.parse(JSON.stringify(obj)) } export const enforceNumber = (num: number | string) => { return isNaN(Number(num)) ? 0 : Number(num) } export const isDev = process.env.NODE_ENV === 'development' export const trimValues = (obj: IStringKeyMap) => { const newObj = {} as IStringKeyMap Object.keys(obj).forEach(key => { newObj[key] = typeof obj[key] === 'string' ? obj[key].trim() : obj[key] }) return newObj } export const isMacOS = process.platform === 'darwin' export const isWindows = process.platform === 'win32' export const isLinux = process.platform === 'linux' ================================================ FILE: src/universal/utils/static.ts ================================================ export const CLIPBOARD_IMAGE_FOLDER = 'picgo-clipboard-images' export const FORM_IMAGE_FOLDER = 'picgo-form-images' export const RELEASE_URL = 'https://api.github.com/repos/Molunerfinn/PicGo/releases' export const RELEASE_URL_BACKUP = 'https://release.picgo.app' export const STABLE_RELEASE_URL = 'https://github.com/Molunerfinn/PicGo/releases/latest' export const BETA_RELEASE_URL = 'https://github.com/Molunerfinn/PicGo/releases' ================================================ FILE: src/universal/utils/staticPath.ts ================================================ import path from 'path' const staticBasePath = process.env.STATIC_PATH || path.join(process.cwd(), 'public') export const getStaticPath = (...segments: string[]) => path.join(staticBasePath, ...segments) ================================================ FILE: tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ const colors = require('tailwindcss/colors') module.exports = { content: [ './public/index.html', './src/**/*.{js,ts,jsx,tsx,vue}' ], theme: { extend: { colors: { // 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' } } } }, plugins: [], corePlugins: { // due to https://github.com/tailwindlabs/tailwindcss/issues/6602 - buttons disappear preflight: false } } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "esnext", // https://github.com/TypeStrong/ts-loader/issues/1061 "module": "esnext", "strict": true, "jsx": "preserve", "importHelpers": true, "moduleResolution": "bundler", "resolveJsonModule": true, "esModuleInterop": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "skipLibCheck": true, "sourceMap": true, "baseUrl": ".", "types": [ "vite/client", "element-plus/global", "vue3-photo-preview", "electron" ], "typeRoots": [ "./src/universal/types/", "./node_modules/@types", "./node_modules" ], "paths": { "@/*": [ "src/renderer/*" ], "~/*": [ "src/*" ], "root/*": [ "./*" ], "#/*": [ "src/universal/*" ], "apis/*": [ "src/main/apis/*" ], "@core/*": [ "src/main/apis/core/*" ] }, "lib": [ "esnext", "dom", "dom.iterable", "scripthost" ] }, "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx", "electron.vite.config.ts", "electron-builder.config.ts" ], "exclude": [ "node_modules" ], "vueCompilerOptions": { "target": 3, } } ================================================ FILE: vitest.config.ts ================================================ 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'] } })