[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [\"Molunerfinn\"]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: \"✨ Feature Request\"\ndescription: 功能请求 / Feature request\ntitle: \"[Feature]: \"\nlabels: [\"feature request\"]\nassignees:\n  - molunerfinn\nbody:\n  - type: markdown\n    attributes:\n      value: |+\n        ## PicGo Issue 模板\n\n        请依照该模板来提交，否则将会被关闭。\n        **提问之前请注意你看过 FAQ、文档以及那些被关闭的 issues。否则同样的提问也会被关闭！**\n\n        Please submit according to this template, otherwise it will be closed.\n        **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! **\n\n  - type: checkboxes\n    id: read\n    attributes:\n      label: 前置阅读 | Pre-reading\n      description: 我已经自行查找、阅读以下内容（阅读了请打勾） | I have searched and read the following on my own (Please tick after reading)\n      options:\n        - label: \"[文档/Doc](https://docs.picgo.app/gui/)\"\n          required: true\n        - label: \"[Issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed)\"\n          required: true\n        - label: \"[FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md)\"\n          required: true\n  - type: input\n    id: version\n    attributes:\n      label: PicGo的版本 | PicGo Version\n      placeholder: 例如 v2.3.0-beta.1\n    validations:\n      required: true\n  - type: dropdown\n    id: platform\n    attributes:\n      label: 系统信息 | System Information\n      options:\n        - Windows\n        - Mac\n        - Mac(arm64)\n        - Linux\n        - All\n    validations:\n      required: true\n  - type: textarea\n    id: reproduce\n    attributes:\n      label: 功能请求 | Feature request\n      description: 详细描述你所预想的功能或者是现有功能的改进 | Describe in detail the features you envision or improvements to existing features\n    validations:\n      required: true\n  - type: markdown\n    attributes:\n      value: |\n        最后，喜欢 PicGo 的话不妨给它点个 star~\n        如果可以的话，请我喝杯咖啡？首页有赞助二维码，谢谢你的支持！ \n        Finally, if you like PicGo, give it a star~\n        Buy me a cup of coffee if you can? There is a sponsorship QR code on the homepage, thank you for your support!\n"
  },
  {
    "path": ".github/workflows/issue-duplicate-detection.yml",
    "content": "name: Issue Duplicate Detection\n\non:\n  issues:\n    types:\n      - opened\n      - edited\n      - reopened\n\npermissions:\n  contents: read\n  issues: write\n\njobs:\n  detect-duplicate-issues:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Run Warp Agent duplicate detector\n        uses: warpdotdev/warp-agent-action@v1\n        id: duplicate_detector\n        env:\n          GH_TOKEN: ${{ github.token }}\n        with:\n          warp_api_key: ${{ secrets.WARP_API_KEY }}\n          profile: ${{ vars.WARP_AGENT_PROFILE || '' }}\n          prompt: |\n            You are triaging duplicate GitHub issues for this repository.\n\n            Repository: ${{ github.repository }}\n            Target issue number: #${{ github.event.issue.number }}\n            Target issue URL: ${{ github.event.issue.html_url }}\n            Target issue title: ${{ github.event.issue.title }}\n            Target issue body:\n            ${{ github.event.issue.body }}\n\n            Use the GitHub CLI with GH_TOKEN for all operations.\n\n            Workflow requirements:\n            1. Gather issue context from title/body/error messages/symptoms/components.\n               - Run:\n                 gh issue view ${{ github.event.issue.number }} --repo ${{ github.repository }} --json number,title,body,url,state,labels\n            2. Search with multiple strategies:\n               - title keywords\n               - error message fragments\n               - symptom words\n               - component/module names\n               Example command pattern:\n               gh issue list --repo ${{ github.repository }} --state all --search \"<query>\"\n            3. Inspect every candidate in detail:\n               gh issue view <candidate_number> --repo ${{ github.repository }} --json number,title,body,url,state\n            4. Duplicate threshold:\n               - only mark duplicate when confidence >= 90%\n               - same root cause + very similar symptoms/errors/components\n            5. Exclusions:\n               - never include pull requests\n               - never include the current issue itself (#${{ github.event.issue.number }})\n            6. If confidence is insufficient or no duplicates exist, exit without commenting.\n            7. If duplicates exist, create or update exactly one comment on issue #${{ github.event.issue.number }}:\n               - first line must be: <!-- issue-duplicate-detector -->\n               - include markdown bullet list with title + link:\n                 - [Issue title](${{ github.server_url }}/${{ github.repository }}/issues/123)\n            8. Before posting, check existing comments on the target issue:\n               - if marker comment exists, update that comment\n               - otherwise create a new comment\n            9. Do not comment on any other issue.\n\n            Expected comment format:\n            <!-- issue-duplicate-detector -->\n            Detected potentially duplicate issues:\n            - [Duplicate issue title](${{ github.server_url }}/${{ github.repository }}/issues/123)\n            - [Another duplicate issue](${{ github.server_url }}/${{ github.repository }}/issues/456)\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Build & Release\n\non:\n  push:\n    tags:\n      - v*\n  workflow_dispatch:\n    inputs:\n      release_tag:\n        description: \"GitHub release tag to publish to (optional, defaults to current branch like dev)\"\n        required: false\n        default: \"\"\n        type: string\n      build_os:\n        description: \"Build for specific OS: Windows, macOS, Linux, All\"\n        required: true\n        default: \"All\"\n        type: choice\n        options:\n          - Windows\n          - macOS\n          - Linux\n          - All\n      test_upload_dist:\n        description: \"Test upload-dist.js script\"\n        required: true\n        default: false\n        type: boolean\n      test_upload_dist_to_dev:\n        description: \"Test upload-dist.js script to dev folder\"\n        required: true\n        default: false\n        type: boolean\n      skip_mac_notarize:\n        description: \"Skip Mac Notarization (true/false)\"\n        required: true\n        default: false\n        type: boolean\n      win_signing_mode:\n        description: \"Windows Signing Mode: release-signing(default) or test-signing\"\n        required: true\n        default: \"release-signing\"\n        type: choice\n        options:\n          - release-signing\n          - test-signing\n\nenv:\n  NODE_VERSION: 22.x\n\njobs:\n  # ============== macOS Builds ==============\n  build-macos:\n    name: Build macOS (${{ matrix.arch }})\n    if: github.event.inputs.build_os == 'macOS' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')\n    runs-on: ${{ matrix.runner }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - runner: macos-15-intel\n            target: dmg\n            arch: x64\n          - runner: macos-latest\n            target: dmg\n            arch: arm64\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 10.29.2\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: pnpm\n          cache-dependency-path: pnpm-lock.yaml\n\n      - name: Clean workspace\n        run: rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Build macOS App (${{ matrix.arch }})\n        run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --mac ${{ matrix.target }} --${{ matrix.arch }} --publish never\n        env:\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n          CSC_LINK: ${{ secrets.MAC_CSC_LINK }}\n          CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}\n          APPLE_ID: ${{ secrets.APPLE_ID }}\n          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}\n          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}\n          SKIP_NOTARIZE: ${{ inputs.skip_mac_notarize }}\n\n      - name: Upload Artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: PicGo-macOS-${{ matrix.arch }}\n          path: dist/*.*\n\n  # ============== Windows Builds ==============\n  build-windows:\n    name: Build Windows (${{ matrix.arch }})\n    if: github.event.inputs.build_os == 'Windows' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')\n    runs-on: ${{ matrix.runner }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - runner: windows-latest\n            arch: x64-ia32\n            build_arch: --x64 --ia32\n            target: nsis\n          - runner: windows-11-arm\n            arch: arm64\n            build_arch: --arm64\n            target: nsis\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 10.29.2\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: pnpm\n          cache-dependency-path: pnpm-lock.yaml\n\n      - name: Clean workspace\n        run: |\n          if (Test-Path dist) { Remove-Item -Recurse -Force dist }\n          if (Test-Path dist_electron) { Remove-Item -Recurse -Force dist_electron }\n          if (Test-Path node_modules) { Remove-Item -Recurse -Force node_modules }\n          if (Test-Path \"$env:LOCALAPPDATA\\electron-builder\") {\n            Remove-Item \"$env:LOCALAPPDATA\\electron-builder\" -Recurse -Force -ErrorAction SilentlyContinue\n          }\n          if (Test-Path \"$env:LOCALAPPDATA\\electron\") {\n            Remove-Item \"$env:LOCALAPPDATA\\electron\" -Recurse -Force -ErrorAction SilentlyContinue\n          }\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      # 1. Build (Unsigned)\n      - name: Build Windows App (${{ matrix.arch }})\n        run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --win ${{matrix.target}} ${{ matrix.build_arch }} --publish never\n        env:\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n\n      # 2. Upload Unsigned Artifact to SignPath (Intermediate Step)\n      - name: Upload Unsigned Artifact for Signing\n        id: upload-unsigned\n        uses: actions/upload-artifact@v4\n        with:\n          name: unsigned-${{ matrix.arch }}\n          path: dist/*.exe\n          retention-days: 1\n          if-no-files-found: error\n\n      - name: Notification for Signing Start\n        shell: bash\n        env:\n          NOTIFY_URL: ${{ secrets.SIGN_NOTIFICATION }}\n        run: curl \"$NOTIFY_URL\"\n\n      # 3. Submit to SignPath and Wait\n      - name: Sign Artifact with SignPath\n        uses: signpath/github-action-submit-signing-request@v2\n        env:\n          SIGNPATH_SIGNING_POLICY_SLUG: |\n            ${{ (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') || inputs.win_signing_mode == 'release-signing') \n              && 'release-signing' \n              || 'test-signing' }}\n          ARTIFACT_SLUG: |\n            ${{ (matrix.arch == 'x64-ia32') \n              && 'PicGo-Windows' \n              || (matrix.arch == 'arm64') \n              && 'PicGo-Windows-ARM64' \n              || '' }}\n        with:\n          api-token: \"${{ secrets.SIGNPATH_API_TOKEN }}\"\n          organization-id: \"${{ secrets.SIGNPATH_ORGANIZATION_ID }}\"\n          project-slug: \"${{ secrets.SIGNPATH_PROJECT_SLUG }}\"\n          signing-policy-slug: \"${{ env.SIGNPATH_SIGNING_POLICY_SLUG }}\"\n          github-artifact-id: \"${{ steps.upload-unsigned.outputs.artifact-id }}\"\n          artifact-configuration-slug: \"${{ env.ARTIFACT_SLUG }}\"\n          wait-for-completion: true\n          output-artifact-directory: \"signed-artifact\"\n\n      # 4. Replace Unsigned with Signed & Fix Blockmap (Critical for Auto-Update)\n      - name: Replace Unsigned with Signed & Update latest.yml\n        shell: powershell\n        run: |\n          # Move signed artifacts to dist folder, overwriting existing ones\n          Move-Item -Path \"signed-artifact\\*.exe\" -Destination \"dist\\\" -Force\n          Write-Host \"✅ Signed artifacts moved to dist folder.\"\n\n          # Run the Node.js script to update latest.yml\n          node scripts/update-win-yaml.js\n\n      - name: Upload Artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: PicGo-Windows-${{ matrix.arch }}\n          path: dist/*.*\n\n  # ============== Linux Builds ==============\n  build-linux:\n    name: Build Linux (${{ matrix.arch }})\n    if: github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')\n    runs-on: ${{ matrix.runner }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - runner: ubuntu-latest\n            arch: x64\n            target: AppImage deb snap\n          - runner: ubuntu-24.04-arm\n            arch: arm64\n            target: AppImage deb\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 10.29.2\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: pnpm\n          cache-dependency-path: pnpm-lock.yaml\n\n      - name: Clean workspace\n        run: rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Install Linux dependencies\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y libfuse2\n\n      - name: Build Linux App (${{ matrix.arch }})\n        run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --linux ${{matrix.target}} --${{ matrix.arch }} --publish never\n        env:\n          GH_TOKEN: ${{ secrets.GH_TOKEN }}\n\n      - name: Upload Artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: PicGo-Linux-${{ matrix.arch }}\n          path: dist/*.*\n\n  # ============== Release ==============\n  release:\n    name: Merge & Release\n    needs: [build-macos, build-windows, build-linux]\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 10.29.2\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: pnpm\n          cache-dependency-path: pnpm-lock.yaml\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Download all artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: artifacts\n\n      - name: List artifacts\n        run: ls -laR artifacts/\n\n      - name: Merge artifacts and yml files\n        run: node scripts/merge-artifacts.js\n\n      - name: List dist\n        run: ls -la dist/\n\n      - name: Upload to release.picgo.app\n        if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.test_upload_dist || github.event.inputs.test_upload_dist_to_dev\n        run: |\n          ARGS=\"--all\"\n\n          if [[ \"${{ github.event.inputs.test_upload_dist_to_dev }}\" == \"true\" ]]; then\n            ARGS=\"$ARGS --dev\"\n            echo \"🚧 Test Upload Mode: ON\"\n          fi\n\n          node scripts/upload-dist.js $ARGS\n        env:\n          PICGO_ENV_S3_SECRET_ID: ${{ secrets.PICGO_ENV_S3_SECRET_ID }}\n          PICGO_ENV_S3_SECRET_KEY: ${{ secrets.PICGO_ENV_S3_SECRET_KEY }}\n          PICGO_ENV_S3_ACCOUNT_ID: ${{ secrets.PICGO_ENV_S3_ACCOUNT_ID }}\n          PICGO_ENV_S3_LEGACY_ACCOUNT_ID: ${{ secrets.PICGO_ENV_S3_LEGACY_ACCOUNT_ID }}\n          PICGO_ENV_S3_LEGACY_SECRET_ID: ${{ secrets.PICGO_ENV_S3_LEGACY_SECRET_ID }}\n          PICGO_ENV_S3_LEGACY_SECRET_KEY: ${{ secrets.PICGO_ENV_S3_LEGACY_SECRET_KEY }}\n\n      - name: Publish GitHub Workflow Release\n        if: github.event_name == 'workflow_dispatch'\n        uses: softprops/action-gh-release@v2\n        continue-on-error: true\n        with:\n          token: ${{ secrets.GH_TOKEN }}\n          tag_name: ${{ github.event.inputs.release_tag || github.ref_name }}\n          draft: true\n          prerelease: false\n          files: |\n            dist/*.exe\n            dist/*.dmg\n            dist/*.zip\n            dist/*.AppImage\n            dist/*.deb\n            dist/*.snap\n            dist/*.tar.gz\n            dist/*.yml\n            dist/*.blockmap\n\n      - name: Publish GitHub Release\n        if: startsWith(github.ref, 'refs/tags/v')\n        uses: softprops/action-gh-release@v2\n        continue-on-error: true\n        with:\n          token: ${{ secrets.GH_TOKEN }}\n          generate_release_notes: true\n          draft: true\n          prerelease: false\n          files: |\n            dist/*.exe\n            dist/*.dmg\n            dist/*.zip\n            dist/*.AppImage\n            dist/*.deb\n            dist/*.snap\n            dist/*.tar.gz\n            dist/*.yml\n            dist/*.blockmap\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\ndist/electron/*\ndist/web/*\nbuild/*\n!build/icons\n!build/installer.nsh\n!build/entitlements.mac.plist\ncoverage\nnode_modules/\nnpm-debug.log\nnpm-debug.log.*\nthumbs.db\n!.gitkeep\nyarn-error.log\ndocs/dist/\n# local env files\n.env.local\n.env.*.local\ndist_electron/\ntest.js\n.env\nscripts/*.yml\n\n#Electron-builder output\n/dist_electron\n.serena/\ndist/*\ntest.js\nspecs/\n.cache/\nopenspec/\nbug*"
  },
  {
    "path": ".husky/commit-msg",
    "content": "pnpm commitlint ${1}\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "pnpm check\n"
  },
  {
    "path": ".husky/pre-push",
    "content": "pnpm test"
  },
  {
    "path": ".node-version",
    "content": "22\n"
  },
  {
    "path": ".npmrc",
    "content": "shamefully-hoist=true"
  },
  {
    "path": ".travis.deprecated.yml",
    "content": "# Commented sections below can be used to run tests on the CI server\n# https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing\nosx_image: xcode8.3\nsudo: required\ndist: trusty\nlanguage: c\nmatrix:\n  include:\n  - os: osx\n  - os: linux\n    env: CC=clang CXX=clang++ npm_config_clang=1\n    compiler: clang\ncache:\n  directories:\n  - node_modules\n  - \"$HOME/.electron\"\n  - \"$HOME/.cache\"\naddons:\n  apt:\n    packages:\n    - libgnome-keyring-dev\n    - icnsutils\n    #- xvfb\nbefore_install:\n- if [[ \"$TRAVIS_OS_NAME\" == \"osx\" ]]; then brew install git-lfs; fi\n- if [[ \"$TRAVIS_OS_NAME\" == \"linux\" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi\ninstall:\n#- export DISPLAY=':99.0'\n#- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &\n- nvm install 10\n- curl -o- -L https://yarnpkg.com/install.sh | bash\n- source ~/.bashrc\n- npm install -g xvfb-maybe\n- yarn\nscript:\n#- xvfb-maybe node_modules/.bin/karma start test/unit/karma.conf.js\n#- yarn run pack && xvfb-maybe node_modules/.bin/mocha test/e2e\n- npm run release\n# - yarn run build:docs\nbefore_script:\n- git lfs pull\nbranches:\n  only:\n  - master\n# after_script:\n#   - cd docs/dist\n#   - git init\n#   - git config user.name \"Molunerfinn\"\n#   - git config user.email \"marksz@teamsz.xyz\"\n#   - git add .\n#   - git commit -m \"Travis build docs\"\n#   - git push  --force --quiet \"https://${GH_TOKEN}@github.com/Molunerfinn/PicGo.git\" master:gh-pages\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Electron: Main\",\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"protocol\": \"inspector\",\n      \"runtimeExecutable\": \"${workspaceRoot}/node_modules/.bin/electron\",\n      \"windows\": {\n        \"runtimeExecutable\": \"${workspaceRoot}/node_modules/.bin/electron.cmd\"\n      },\n      \"preLaunchTask\": \"electron-debug\",\n      \"args\": [\n        \"--remote-debugging-port=9223\",\n        \"./dist\"\n      ],\n      \"outFiles\": [\n        \"${workspaceFolder}/dist/**/*.js\"\n      ]\n    },\n    {\n      \"name\": \"Electron: Renderer\",\n      \"type\": \"chrome\",\n      \"request\": \"attach\",\n      \"port\": 9223,\n      \"urlFilter\": \"http://localhost:*\",\n      \"timeout\": 30000,\n      \"webRoot\": \"${workspaceFolder}/src\",\n      \"sourceMapPathOverrides\": {\n        \"webpack:///./src/*\": \"${webRoot}/*\"\n      }\n    }\n  ],\n  \"compounds\": [\n    {\n      \"name\": \"Electron: All\",\n      \"configurations\": [\n        \"Electron: Main\",\n        \"Electron: Renderer\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"eslint.enable\": true,\n  \"eslint.alwaysShowStatus\": true,\n  \"eslint.format.enable\": true,\n  \"eslint.validate\": [\n    \"javascript\",\n    \"javascriptreact\",\n    \"typescript\",\n    \"vue\",\n    \"typescriptreact\"\n  ],\n  \"[stylus]\": {\n    \"editor.formatOnSave\": true\n  },\n  \"stylusSupremacy.insertSemicolons\": false,\n  \"stylusSupremacy.insertBraces\": false,\n  \"stylusSupremacy.insertNewLineBetweenSelectors\": true,\n  \"stylusSupremacy.insertParenthesisAroundIfCondition\": false,\n  \"stylusSupremacy.alwaysUseNoneOverZero\": true,\n  \"stylusSupremacy.alwaysUseZeroWithoutUnit\": true,\n  \"stylusSupremacy.sortProperties\": \"grouped\",\n  \"stylusSupremacy.quoteChar\": \"\\\"\",\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\",\n    \"source.organizeImports\": \"never\"\n  },\n  \"prettier.enable\": false,\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"dbaeumer.vscode-eslint\"\n  },\n}"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n  // See https://go.microsoft.com/fwlink/?LinkId=733558\n  // for the documentation about the tasks.json format\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"label\": \"electron-debug\",\n      \"type\": \"process\",\n      \"command\": \"./node_modules/.bin/vue-cli-service\",\n      \"windows\": {\n        \"command\": \"./node_modules/.bin/vue-cli-service.cmd\"\n      },\n      \"isBackground\": true,\n      \"args\": [\"electron:serve\", \"--debug\"],\n      \"problemMatcher\": {\n        \"owner\": \"custom\",\n        \"pattern\": {\n          \"regexp\": \"\"\n        },\n        \"background\": {\n          \"beginsPattern\": \"Starting development server\\\\.\\\\.\\\\.\",\n          \"endsPattern\": \"Not launching electron as debug argument was passed\\\\.\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "AGENTS.md",
    "content": "# Repository Guidelines\n\n## Project Structure & Module Organization\nPicGo 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).\n\n## Build, Test, and Development Commands\n- `pnpm install` — install dependencies; `npm install` is unsupported. Only run this when the user explicitly asks/coordinates it.\n- Always add/remove dependencies with `pnpm` (never edit package.json versions by hand then install).\n- `pnpm dev` — electron-vite dev server for main/preload/renderer.\n- `pnpm build` — electron-vite build outputs to `dist/main`, `dist/preload`, `dist/renderer`; `pnpm preview` for preview mode.\n- 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.\n- `pnpm lint` / `pnpm lint:fix` — run or auto-fix ESLint (Standard, TypeScript, Vue rules).\n- `pnpm lint:dpdm` — fail fast on circular dependencies in `src/`.\n- `pnpm check` — run `tsc` + `lint` (run once before finishing a task).\n- Before completing a task, always run `pnpm check` and resolve any issues it reports.\n- `pnpm gen-i18n` — regenerate typed locales after touching `public/i18n/*.yml`.\n\n## Coding Style & Naming Conventions\nFollow 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.\nStatic 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.\n- Do not use `as any` under any circumstances; keep typings explicit and safe.\n- Avoid `as any` in tests as well; build concrete typed stubs (e.g., `IpcMainInvokeEvent`) instead.\n- Do not prefix method calls with `void` (e.g. use `store?.refreshPicBeds()` rather than `void store?.refreshPicBeds()`).\n- If a renderer → main request mutates persisted config/state without using `saveConfig`, call `notifyAppConfigUpdated()` in main to inform renderers.\n- Prefer enums over union types for discrete value sets (e.g., encryption methods). Avoid introducing new string literal union types.\n- Renderer page/component styles should prefer Tailwind utility classes; avoid adding new Vue `<style>` blocks unless there's no reasonable Tailwind equivalent.\n- New renderer ↔ main request/response APIs should be implemented via RPC routes (see `src/main/events/rpc/routes/system.ts`) with `RPCRouter` + `IRPCActionType` rather than adding ad-hoc IPC modules (e.g. `picgoCloudIPC`).\n  - For request/response semantics in renderer, prefer `invokeRPC` (backed by `ipcMain.handle(RPC_ACTIONS, ...)` in `src/main/events/rpc/index.ts`).\n\n## Testing Guidelines\nPlace renderer unit specs in `test/unit/specs` with the `.spec.js` suffix; Karma picks them up via `require.context`. Run them with `npx karma start test/unit/karma.conf.js --single-run` and ensure new renderer folders are covered. Spectron e2e cases live in `test/e2e/specs`; build first (`pnpm build`), then run `npx mocha test/e2e/index.js` so Spectron can launch `dist/electron/main.js`. Document any test data, IPC stubs, or fixtures you add to keep suites reproducible.\n\n## Commit & Pull Request Guidelines\nCommits follow the PicGo conventional preset enforced by Husky (`pnpm lint:dpdm` + Commitlint). Stage your changes and run `pnpm cz` to craft messages that pass CI. Pull requests should explain the change, link related issues, and attach UI screenshots or recordings. Note how you validated the work (dev server, build, Karma, Spectron) and call out migration or configuration steps reviewers must perform.\n\n## Internationalization Tips\nAdd locales by creating `public/i18n/<locale>.yml`, exposing its `LANG_DISPLAY_LABEL`, and registering it in `src/universal/i18n/index.ts`. After editing `public/i18n/*.yml`, run `pnpm gen-i18n` to regenerate TS typings and keep them in sync.\n- Any user-facing copy (UI text, error messages, warnings, prompts, tips, notifications, etc.) MUST use i18n keys. Do not hardcode strings in code.\n  - Renderer: use `$T('KEY')` from `src/renderer/i18n/index.ts`.\n  - Main process: use `T('KEY')` from `src/main/i18n/index.ts`.\n  - Add new keys to all locales under `public/i18n/` (at least `en.yml`, `zh-CN.yml`, `zh-TW.yml`) and run `pnpm gen-i18n`.\n\n## Serena MCP & Context7 Tools\nWhen starting work or if you hit issues, try checking MCP for Serena or Context7 tooling. If available, use those tools to navigate, edit, or fetch docs efficiently.\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## :tada: 2.5.3 (2026-03-06)\n\n\n### :bug: Bug Fixes\n\n* **plugin:** refresh plugin config dialog state ([#1395](https://github.com/Molunerfinn/PicGo/issues/1395)) ([0e452e1](https://github.com/Molunerfinn/PicGo/commit/0e452e1)), closes [#1394](https://github.com/Molunerfinn/PicGo/issues/1394)\n* **update:** correct latest version lookup with beta channel ([#1396](https://github.com/Molunerfinn/PicGo/issues/1396)) ([#1397](https://github.com/Molunerfinn/PicGo/issues/1397)) ([c7ca0de](https://github.com/Molunerfinn/PicGo/commit/c7ca0de))\n\n\n### :package: Chore\n\n* add oz agent for issues ([#1392](https://github.com/Molunerfinn/PicGo/issues/1392)) ([f05ab63](https://github.com/Molunerfinn/PicGo/commit/f05ab63))\n\n\n### :pencil: Documentation\n\n* **custom:** update README ([#1390](https://github.com/Molunerfinn/PicGo/issues/1390)) ([5d0f016](https://github.com/Molunerfinn/PicGo/commit/5d0f016))\n* update docs ([ac9a00b](https://github.com/Molunerfinn/PicGo/commit/ac9a00b))\n* update sponsor ([87d92d5](https://github.com/Molunerfinn/PicGo/commit/87d92d5))\n\n\n\n## :tada: 2.5.2 (2026-02-10)\n\n\n### :bug: Bug Fixes\n\n* s.ee upload error ([a1f76ca](https://github.com/Molunerfinn/PicGo/commit/a1f76ca)), closes [#1385](https://github.com/Molunerfinn/PicGo/issues/1385)\n\n\n### :pencil: Documentation\n\n* add 2.5.1 docs ([5008a92](https://github.com/Molunerfinn/PicGo/commit/5008a92))\n* update readme for s.ee ([d0defee](https://github.com/Molunerfinn/PicGo/commit/d0defee))\n\n\n\n## :tada: 2.5.1 (2026-02-10)\n\n\n### :sparkles: Features\n\n* windows signature ([#1386](https://github.com/Molunerfinn/PicGo/issues/1386)) ([781cf30](https://github.com/Molunerfinn/PicGo/commit/781cf30))\n\n\n### :bug: Bug Fixes\n\n* **plugin:** plugin search error ([ef07c15](https://github.com/Molunerfinn/PicGo/commit/ef07c15)), closes [#1383](https://github.com/Molunerfinn/PicGo/issues/1383)\n\n\n### :package: Chore\n\n* **notification:** add notification for windows sign ([#1387](https://github.com/Molunerfinn/PicGo/issues/1387)) ([592f985](https://github.com/Molunerfinn/PicGo/commit/592f985))\n* update link ([27464dc](https://github.com/Molunerfinn/PicGo/commit/27464dc))\n* update picgo version to support s.ee ([e2e05ae](https://github.com/Molunerfinn/PicGo/commit/e2e05ae)), closes [#1385](https://github.com/Molunerfinn/PicGo/issues/1385)\n\n\n\n# :tada: 2.5.0 (2026-01-27)\n\n\n### :sparkles: Features\n\n* add picgo cloud and config sync ([#1382](https://github.com/Molunerfinn/PicGo/issues/1382)) ([4344772](https://github.com/Molunerfinn/PicGo/commit/4344772)), closes [#1381](https://github.com/Molunerfinn/PicGo/issues/1381)\n\n\n### :bug: Bug Fixes\n\n* review changes ([5cabbbd](https://github.com/Molunerfinn/PicGo/commit/5cabbbd))\n\n\n### :package: Chore\n\n* **multi-arch:** add multi-arch build support ([#1379](https://github.com/Molunerfinn/PicGo/issues/1379)) ([678cb71](https://github.com/Molunerfinn/PicGo/commit/678cb71))\n\n\n### :pencil: Documentation\n\n* **faq:** update faq ([4fb3f2d](https://github.com/Molunerfinn/PicGo/commit/4fb3f2d))\n* **release:** update 2.4.3 docs ([5eb3755](https://github.com/Molunerfinn/PicGo/commit/5eb3755))\n\n\n\n## :tada: 2.4.3 (2026-01-12)\n\n\n### :sparkles: Features\n\n* add global url rewrite support ([#1377](https://github.com/Molunerfinn/PicGo/issues/1377)) ([2ed1dd5](https://github.com/Molunerfinn/PicGo/commit/2ed1dd5)), closes [#1255](https://github.com/Molunerfinn/PicGo/issues/1255) [#1281](https://github.com/Molunerfinn/PicGo/issues/1281)\n* **upload:** add batch url upload support ([#1376](https://github.com/Molunerfinn/PicGo/issues/1376)) ([c84d542](https://github.com/Molunerfinn/PicGo/commit/c84d542)), closes [#1302](https://github.com/Molunerfinn/PicGo/issues/1302)\n\n\n### :bug: Bug Fixes\n\n* **gallery:** fix picbed list visible status not sync with gallery ([#1373](https://github.com/Molunerfinn/PicGo/issues/1373)) ([dfe92d4](https://github.com/Molunerfinn/PicGo/commit/dfe92d4)), closes [#1372](https://github.com/Molunerfinn/PicGo/issues/1372)\n* **ui:** some ui bugs ([089884b](https://github.com/Molunerfinn/PicGo/commit/089884b))\n\n\n\n## :tada: 2.4.2 (2026-01-07)\n\n\n### :sparkles: Features\n\n* **notification:** refactor notification and add notificationSound settings ([#1370](https://github.com/Molunerfinn/PicGo/issues/1370)) ([2936b19](https://github.com/Molunerfinn/PicGo/commit/2936b19)), closes [#1229](https://github.com/Molunerfinn/PicGo/issues/1229)\n\n\n### :bug: Bug Fixes\n\n* **tray:** clamp image titles to two lines ([525492b](https://github.com/Molunerfinn/PicGo/commit/525492b))\n\n\n### :pencil: Documentation\n\n* **2.4.2:** update changelog ([2c9a188](https://github.com/Molunerfinn/PicGo/commit/2c9a188))\n\n\n\n## :tada: 2.4.2-beta.0 (2025-12-31)\n\n\n### :sparkles: Features\n\n* **config:** add copy config \\&\\& add double confirm before copy \\& delete config ([25f86a6](https://github.com/Molunerfinn/PicGo/commit/25f86a6))\n\n\n### :bug: Bug Fixes\n\n* **icon:** app icon too large in macOS 15.x ([840c33b](https://github.com/Molunerfinn/PicGo/commit/840c33b)), closes [#1367](https://github.com/Molunerfinn/PicGo/issues/1367)\n\n\n### :package: Chore\n\n* electron builder not publish with build command ([9269f96](https://github.com/Molunerfinn/PicGo/commit/9269f96))\n* fix workflow build ([a1d22e5](https://github.com/Molunerfinn/PicGo/commit/a1d22e5))\n* **signature:** add signature \\& notarization process ([ee6ca02](https://github.com/Molunerfinn/PicGo/commit/ee6ca02))\n* update FAQ \\&\\& plugin filter logic ([f7f5804](https://github.com/Molunerfinn/PicGo/commit/f7f5804))\n\n\n\n## :tada: 2.4.1 (2025-12-23)\n\n\n### :sparkles: Features\n\n* add showMenubarIcon setting ([#1366](https://github.com/Molunerfinn/PicGo/issues/1366)) ([d0eb3da](https://github.com/Molunerfinn/PicGo/commit/d0eb3da))\n\n\n### :bug: Bug Fixes\n\n* **custom:** build workflow error ([a0db473](https://github.com/Molunerfinn/PicGo/commit/a0db473))\n* **custom:** data report ([eef736f](https://github.com/Molunerfinn/PicGo/commit/eef736f))\n* **custom:** workflow env bug ([f84f5f1](https://github.com/Molunerfinn/PicGo/commit/f84f5f1))\n\n\n### :pencil: Documentation\n\n* **custom:** update readme ([a671ea4](https://github.com/Molunerfinn/PicGo/commit/a671ea4))\n* **custom:** update readme ([c658b9b](https://github.com/Molunerfinn/PicGo/commit/c658b9b))\n* **custom:** update README ([c8c9122](https://github.com/Molunerfinn/PicGo/commit/c8c9122))\n* update 2.4.1 changelog ([d071957](https://github.com/Molunerfinn/PicGo/commit/d071957))\n\n\n### :package: Chore\n\n* **custom:** rm yarn.lock ([8c310e7](https://github.com/Molunerfinn/PicGo/commit/8c310e7))\n\n\n\n## :tada: 2.4.1-beta.1 (2025-12-10)\n\n\n### :bug: Bug Fixes\n\n* **custom:** the issue that x64 macOS app can't be opened ([54d15a6](https://github.com/Molunerfinn/PicGo/commit/54d15a6)), closes [#1363](https://github.com/Molunerfinn/PicGo/issues/1363)\n\n\n### :package: Chore\n\n* update builder config && add legacy version file upload process ([2cc2983](https://github.com/Molunerfinn/PicGo/commit/2cc2983))\n\n\n\n## :tada: 2.4.1-beta.0 (2025-12-09)\n\n\n### :bug: Bug Fixes\n\n* fix docs link ([d6c0a85](https://github.com/Molunerfinn/PicGo/commit/d6c0a85))\n* pic-migrater post handler error ([b421c4b](https://github.com/Molunerfinn/PicGo/commit/b421c4b))\n* tray window clipboard image not show bug ([#1362](https://github.com/Molunerfinn/PicGo/issues/1362)) ([56f61d4](https://github.com/Molunerfinn/PicGo/commit/56f61d4))\n\n\n### :package: Chore\n\n* change release url ([9d4ea82](https://github.com/Molunerfinn/PicGo/commit/9d4ea82))\n* disabled build universal installer ([366ac11](https://github.com/Molunerfinn/PicGo/commit/366ac11))\n* fix some workflow bug ([db627a4](https://github.com/Molunerfinn/PicGo/commit/db627a4))\n* fix upload dist arch bug ([8e94b2a](https://github.com/Molunerfinn/PicGo/commit/8e94b2a))\n* upgrade electron version && migrate to electron-vite ([#1361](https://github.com/Molunerfinn/PicGo/issues/1361)) ([070ce2b](https://github.com/Molunerfinn/PicGo/commit/070ce2b))\n\n\n\n# :tada: 2.4.0 (2025-11-23)\n\n\n### :pencil: Documentation\n\n* add 2.4.0 changelog ([cccd295](https://github.com/Molunerfinn/PicGo/commit/cccd295))\n* add warp sponsor shoutout ([e0d45fa](https://github.com/Molunerfinn/PicGo/commit/e0d45fa))\n* add warp sponsor shoutout ([de441a8](https://github.com/Molunerfinn/PicGo/commit/de441a8))\n* update docs ([4a29bf2](https://github.com/Molunerfinn/PicGo/commit/4a29bf2))\n* update docs ([24e4a82](https://github.com/Molunerfinn/PicGo/commit/24e4a82))\n* update readme & warp sponsor link ([45b3227](https://github.com/Molunerfinn/PicGo/commit/45b3227))\n\n\n### :package: Chore\n\n* change funding yml ([2450a52](https://github.com/Molunerfinn/PicGo/commit/2450a52))\n* update picgo core to 1.5.11 to solve url encode bug ([cfb6146](https://github.com/Molunerfinn/PicGo/commit/cfb6146))\n\n\n\n# :tada: 2.4.0-beta.10 (2025-06-08)\n\n\n### :sparkles: Features\n\n* finish form uploader for picgo server ([9841418](https://github.com/Molunerfinn/PicGo/commit/9841418))\n* **server:** add support for form upload in PicGo Server ([#1327](https://github.com/Molunerfinn/PicGo/issues/1327)) ([a928b4c](https://github.com/Molunerfinn/PicGo/commit/a928b4c)), closes [#428](https://github.com/Molunerfinn/PicGo/issues/428)\n\n\n### :bug: Bug Fixes\n\n* auto-copy url can't be turned off ([#1300](https://github.com/Molunerfinn/PicGo/issues/1300)) ([dacb926](https://github.com/Molunerfinn/PicGo/commit/dacb926))\n* encoded url filename unreadable ([2bd5d06](https://github.com/Molunerfinn/PicGo/commit/2bd5d06))\n* unable to use clip in wayland ([#1301](https://github.com/Molunerfinn/PicGo/issues/1301)) ([7df37a2](https://github.com/Molunerfinn/PicGo/commit/7df37a2))\n\n\n\n# :tada: 2.4.0-beta.9 (2024-12-02)\n\n\n### :bug: Bug Fixes\n\n* clipboard filename missing second ([c9fe402](https://github.com/Molunerfinn/PicGo/commit/c9fe402)), closes [#1293](https://github.com/Molunerfinn/PicGo/issues/1293)\n* copy text bug ([c8ba547](https://github.com/Molunerfinn/PicGo/commit/c8ba547)), closes [#1210](https://github.com/Molunerfinn/PicGo/issues/1210) [#1280](https://github.com/Molunerfinn/PicGo/issues/1280)\n* plugin list search bug ([04140de](https://github.com/Molunerfinn/PicGo/commit/04140de)), closes [#1297](https://github.com/Molunerfinn/PicGo/issues/1297)\n\n\n### :package: Chore\n\n* update ci macos version ([316928e](https://github.com/Molunerfinn/PicGo/commit/316928e))\n\n\n\n# :tada: 2.4.0-beta.8 (2024-07-16)\n\n\n### :bug: Bug Fixes\n\n* tencent cos url encode bug ([ff7336b](https://github.com/Molunerfinn/PicGo/commit/ff7336b)), closes [#1265](https://github.com/Molunerfinn/PicGo/issues/1265)\n\n\n\n# :tada: 2.4.0-beta.7 (2024-04-22)\n\n\n### :sparkles: Features\n\n* add startup mode ([aaec99f](https://github.com/Molunerfinn/PicGo/commit/aaec99f)), closes [#915](https://github.com/Molunerfinn/PicGo/issues/915)\n\n\n### :bug: Bug Fixes\n\n* config page scroll bug ([8e91582](https://github.com/Molunerfinn/PicGo/commit/8e91582)), closes [#1237](https://github.com/Molunerfinn/PicGo/issues/1237)\n* tray menu open bug ([50e0a64](https://github.com/Molunerfinn/PicGo/commit/50e0a64)), closes [#1217](https://github.com/Molunerfinn/PicGo/issues/1217)\n\n\n\n# :tada: 2.4.0-beta.6 (2023-11-19)\n\n\n### :bug: Bug Fixes\n\n* app.asar directroy copy error ([#1180](https://github.com/Molunerfinn/PicGo/issues/1180)) ([cd07b33](https://github.com/Molunerfinn/PicGo/commit/cd07b33)), closes [#1179](https://github.com/Molunerfinn/PicGo/issues/1179)\n* can't add new config for picbed ([050a3dd](https://github.com/Molunerfinn/PicGo/commit/050a3dd)), closes [#1184](https://github.com/Molunerfinn/PicGo/issues/1184)\n\n\n\n# :tada: 2.4.0-beta.5 (2023-09-10)\n\n\n### :sparkles: Features\n\n* add gallery toolbox menu ([#1177](https://github.com/Molunerfinn/PicGo/issues/1177)) ([0f7b07d](https://github.com/Molunerfinn/PicGo/commit/0f7b07d))\n\n\n\n# :tada: 2.4.0-beta.4 (2023-08-26)\n\n\n### :sparkles: Features\n\n* add configName for upload page ([894d0a2](https://github.com/Molunerfinn/PicGo/commit/894d0a2))\n* support \"tips\" option for uploader ([1b92f20](https://github.com/Molunerfinn/PicGo/commit/1b92f20))\n* **tcyun:** add slim section ([#1165](https://github.com/Molunerfinn/PicGo/issues/1165)) ([a2320c3](https://github.com/Molunerfinn/PicGo/commit/a2320c3))\n\n\n### :bug: Bug Fixes\n\n* open config file bug ([2db0fea](https://github.com/Molunerfinn/PicGo/commit/2db0fea)), closes [#1163](https://github.com/Molunerfinn/PicGo/issues/1163)\n\n\n\n# :tada: 2.4.0-beta.3 (2023-07-09)\n\n\n### :bug: Bug Fixes\n\n* rename page bug ([bc2e928](https://github.com/Molunerfinn/PicGo/commit/bc2e928)), closes [#1130](https://github.com/Molunerfinn/PicGo/issues/1130)\n* tailwind css bug ([e3566b5](https://github.com/Molunerfinn/PicGo/commit/e3566b5))\n\n\n\n# :tada: 2.4.0-beta.2 (2023-07-09)\n\n\n### :bug: Bug Fixes\n\n* fileName encode bug ([8d9a400](https://github.com/Molunerfinn/PicGo/commit/8d9a400)), closes [#1121](https://github.com/Molunerfinn/PicGo/issues/1121)\n\n\n\n# :tada: 2.4.0-beta.1 (2023-05-03)\n\n\n### :sparkles: Features\n\n* add picgo toolbox for auto detect & fix problems ([dfbc96f](https://github.com/Molunerfinn/PicGo/commit/dfbc96f))\n* add settings.encodeOutputURL options ([f75514d](https://github.com/Molunerfinn/PicGo/commit/f75514d)), closes [#731](https://github.com/Molunerfinn/PicGo/issues/731)\n* add showDockIcon option ([46f54e1](https://github.com/Molunerfinn/PicGo/commit/46f54e1)), closes [#1045](https://github.com/Molunerfinn/PicGo/issues/1045)\n* add showDockIcon option ([32eb176](https://github.com/Molunerfinn/PicGo/commit/32eb176)), closes [#1045](https://github.com/Molunerfinn/PicGo/issues/1045)\n* support dragging any type of file to upload ([520d6d3](https://github.com/Molunerfinn/PicGo/commit/520d6d3)), closes [#1052](https://github.com/Molunerfinn/PicGo/issues/1052)\n\n\n### :bug: Bug Fixes\n\n* console.log EPIPE error ([7363be7](https://github.com/Molunerfinn/PicGo/commit/7363be7)), closes [#1101](https://github.com/Molunerfinn/PicGo/issues/1101)\n* custom url template encode bug ([063962d](https://github.com/Molunerfinn/PicGo/commit/063962d)), closes [#1112](https://github.com/Molunerfinn/PicGo/issues/1112)\n* fix copy link encoding bug ([34657ae](https://github.com/Molunerfinn/PicGo/commit/34657ae)), closes [#731](https://github.com/Molunerfinn/PicGo/issues/731)\n* i18n bug ([911e34e](https://github.com/Molunerfinn/PicGo/commit/911e34e))\n* isDarkMode() error when dragging file to tray icon ([b7d2edb](https://github.com/Molunerfinn/PicGo/commit/b7d2edb)), closes [#1107](https://github.com/Molunerfinn/PicGo/issues/1107)\n* typescript nightly build bug ([455cb49](https://github.com/Molunerfinn/PicGo/commit/455cb49)), closes [#1082](https://github.com/Molunerfinn/PicGo/issues/1082)\n\n\n### :package: Chore\n\n* change version files' upload dest & dist files' upload dest ([4f392f3](https://github.com/Molunerfinn/PicGo/commit/4f392f3))\n\n\n### :pencil: Documentation\n\n* update FAQ ([6801334](https://github.com/Molunerfinn/PicGo/commit/6801334)), closes [#1067](https://github.com/Molunerfinn/PicGo/issues/1067)\n\n\n\n# :tada: 2.4.0-beta.0 (2023-01-05)\n\n\n### :sparkles: Features\n\n* add file-name in gallery & add unknown-file-type placholder image ([f0787d3](https://github.com/Molunerfinn/PicGo/commit/f0787d3)), closes [#1050](https://github.com/Molunerfinn/PicGo/issues/1050)\n* add file-name in tray-page ([c011654](https://github.com/Molunerfinn/PicGo/commit/c011654)), closes [#1054](https://github.com/Molunerfinn/PicGo/issues/1054)\n* support multiple config ([#1016](https://github.com/Molunerfinn/PicGo/issues/1016)) ([9555649](https://github.com/Molunerfinn/PicGo/commit/9555649))\n\n\n### :bug: Bug Fixes\n\n* handleUploaderConfig should be placed in main/utils ([fc897ce](https://github.com/Molunerfinn/PicGo/commit/fc897ce))\n* nsis script ([44f5fbb](https://github.com/Molunerfinn/PicGo/commit/44f5fbb)), closes [#1019](https://github.com/Molunerfinn/PicGo/issues/1019)\n\n\n### :pencil: Documentation\n\n* update FAQ.md ([#1011](https://github.com/Molunerfinn/PicGo/issues/1011)) ([05998bb](https://github.com/Molunerfinn/PicGo/commit/05998bb))\n\n\n\n## :tada: 2.3.1 (2022-11-13)\n\n\n### :sparkles: Features\n\n* add $extName for paste template ([64e54d0](https://github.com/Molunerfinn/PicGo/commit/64e54d0)), closes [#1000](https://github.com/Molunerfinn/PicGo/issues/1000)\n\n\n### :bug: Bug Fixes\n\n* upyun options is not required ([167e424](https://github.com/Molunerfinn/PicGo/commit/167e424)), closes [#1002](https://github.com/Molunerfinn/PicGo/issues/1002)\n\n\n\n## :tada: 2.3.1-beta.8 (2022-10-30)\n\n\n### :sparkles: Features\n\n* add remoteNotice ([9317871](https://github.com/Molunerfinn/PicGo/commit/9317871))\n* macOS tray icon more clearer ([ecd462f](https://github.com/Molunerfinn/PicGo/commit/ecd462f))\n\n\n### :bug: Bug Fixes\n\n* url encode bug && copy-paste url encode bug ([4de7a1d](https://github.com/Molunerfinn/PicGo/commit/4de7a1d)), closes [#996](https://github.com/Molunerfinn/PicGo/issues/996)\n\n\n### :pencil: Documentation\n\n* add PicHoro ([a355bc0](https://github.com/Molunerfinn/PicGo/commit/a355bc0))\n\n\n\n## :tada: 2.3.1-beta.7 (2022-10-23)\n\n\n### :sparkles: Features\n\n* add zh-TW.yml ([#976](https://github.com/Molunerfinn/PicGo/issues/976)) ([72371de](https://github.com/Molunerfinn/PicGo/commit/72371de))\n\n\n### :bug: Bug Fixes\n\n* change decodeURI -> decodeURIComponent & encode also ([7677f1e](https://github.com/Molunerfinn/PicGo/commit/7677f1e))\n* macOS tray icon background color bug ([9791ff2](https://github.com/Molunerfinn/PicGo/commit/9791ff2)), closes [#970](https://github.com/Molunerfinn/PicGo/issues/970)\n* some bugs will case picgo-gui-local.log too large ([012a01d](https://github.com/Molunerfinn/PicGo/commit/012a01d)), closes [#995](https://github.com/Molunerfinn/PicGo/issues/995)\n* some case will cause picgo-gui-local.log too large ([3c01861](https://github.com/Molunerfinn/PicGo/commit/3c01861))\n* some texts in zh-TW ([#978](https://github.com/Molunerfinn/PicGo/issues/978)) ([531bc13](https://github.com/Molunerfinn/PicGo/commit/531bc13))\n\n\n### :pencil: Documentation\n\n* update readme & FAQ ([d438f82](https://github.com/Molunerfinn/PicGo/commit/d438f82))\n\n\n\n## :tada: 2.3.1-beta.6 (2022-09-04)\n\n\n### :sparkles: Features\n\n* cli support uploading image with url ([e848918](https://github.com/Molunerfinn/PicGo/commit/e848918))\n* finish i18n system ([428ffc7](https://github.com/Molunerfinn/PicGo/commit/428ffc7))\n\n\n### :bug: Bug Fixes\n\n* macos clipboard image can't show on tray page ([20e38f4](https://github.com/Molunerfinn/PicGo/commit/20e38f4)), closes [#961](https://github.com/Molunerfinn/PicGo/issues/961)\n* showFileExplorer result bug ([b6b2eea](https://github.com/Molunerfinn/PicGo/commit/b6b2eea))\n* windows upload clipboard file with builtin-clipboard not work ([7b50ba7](https://github.com/Molunerfinn/PicGo/commit/7b50ba7))\n\n\n### :package: Chore\n\n* up issue template ([5f1fb08](https://github.com/Molunerfinn/PicGo/commit/5f1fb08))\n* update cos upload url ([86012c0](https://github.com/Molunerfinn/PicGo/commit/86012c0))\n\n\n\n## :tada: 2.3.1-beta.5 (2022-08-14)\n\n\n### :sparkles: Features\n\n* add dist upload to cos & update checkupdate logic ([c926414](https://github.com/Molunerfinn/PicGo/commit/c926414))\n* add logFileSizeLimit for log file ([219b367](https://github.com/Molunerfinn/PicGo/commit/219b367)), closes [#935](https://github.com/Molunerfinn/PicGo/issues/935) [#945](https://github.com/Molunerfinn/PicGo/issues/945)\n* add server response headers for CORS ([#939](https://github.com/Molunerfinn/PicGo/issues/939)) ([fb69bad](https://github.com/Molunerfinn/PicGo/commit/fb69bad))\n* tray-window add open-setting-window button ([83ab3c6](https://github.com/Molunerfinn/PicGo/commit/83ab3c6))\n* update tray icon in macOS 11 or 12 ([4ebdc72](https://github.com/Molunerfinn/PicGo/commit/4ebdc72)), closes [#776](https://github.com/Molunerfinn/PicGo/issues/776)\n\n\n### :bug: Bug Fixes\n\n* build error ([1db84a2](https://github.com/Molunerfinn/PicGo/commit/1db84a2))\n* plugin config can't save ([09e4e82](https://github.com/Molunerfinn/PicGo/commit/09e4e82)), closes [#943](https://github.com/Molunerfinn/PicGo/issues/943)\n\n\n### :package: Chore\n\n* fix github action build scripts ([e6b9d88](https://github.com/Molunerfinn/PicGo/commit/e6b9d88))\n* fix version error ([0a9e169](https://github.com/Molunerfinn/PicGo/commit/0a9e169))\n* update macOS tray icon ([87161b3](https://github.com/Molunerfinn/PicGo/commit/87161b3))\n* update manually action ([351fbda](https://github.com/Molunerfinn/PicGo/commit/351fbda))\n\n\n### :pencil: Documentation\n\n* update cos links ([3102d7b](https://github.com/Molunerfinn/PicGo/commit/3102d7b))\n\n\n\n## :tada: 2.3.1-beta.4 (2022-06-12)\n\n\n### :bug: Bug Fixes\n\n* **db:** fix some db bugs ([d3bb5ca](https://github.com/Molunerfinn/PicGo/commit/d3bb5ca)), closes [#873](https://github.com/Molunerfinn/PicGo/issues/873) [#806](https://github.com/Molunerfinn/PicGo/issues/806)\n* **gallery:** can't copy gallery pics link ([8d861be](https://github.com/Molunerfinn/PicGo/commit/8d861be)), closes [#901](https://github.com/Molunerfinn/PicGo/issues/901)\n\n\n### :pencil: Documentation\n\n* fix electron_mirror link error ([5d06469](https://github.com/Molunerfinn/PicGo/commit/5d06469)), closes [#849](https://github.com/Molunerfinn/PicGo/issues/849)\n* update FAQ ([a79efbf](https://github.com/Molunerfinn/PicGo/commit/a79efbf))\n* update readme & FAQ ([746635e](https://github.com/Molunerfinn/PicGo/commit/746635e))\n\n\n### :package: Chore\n\n* add issue template ([db6c5b8](https://github.com/Molunerfinn/PicGo/commit/db6c5b8))\n\n\n\n## :tada: 2.3.1-beta.3 (2022-04-04)\n\n\n### :sparkles: Features\n\n* add i18n for en ([1936ccf](https://github.com/Molunerfinn/PicGo/commit/1936ccf))\n* add tencent-cos options for url ([af291e4](https://github.com/Molunerfinn/PicGo/commit/af291e4)), closes [#862](https://github.com/Molunerfinn/PicGo/issues/862) [#863](https://github.com/Molunerfinn/PicGo/issues/863) [#865](https://github.com/Molunerfinn/PicGo/issues/865) [#524](https://github.com/Molunerfinn/PicGo/issues/524) [#845](https://github.com/Molunerfinn/PicGo/issues/845) [#732](https://github.com/Molunerfinn/PicGo/issues/732)\n* add upload-clipboard-image from electron' clipboard ([27628da](https://github.com/Molunerfinn/PicGo/commit/27628da)), closes [#822](https://github.com/Molunerfinn/PicGo/issues/822)\n* add wayland support for linux ([f1c8507](https://github.com/Molunerfinn/PicGo/commit/f1c8507))\n* click cancel in rename-window will use origin filename now ([04701d4](https://github.com/Molunerfinn/PicGo/commit/04701d4)), closes [#791](https://github.com/Molunerfinn/PicGo/issues/791)\n\n\n### :bug: Bug Fixes\n\n* fix mini-page can't upload image from dragging browser image ([6bcd019](https://github.com/Molunerfinn/PicGo/commit/6bcd019)), closes [#822](https://github.com/Molunerfinn/PicGo/issues/822)\n* mini window not always on top after reopen ([c79a286](https://github.com/Molunerfinn/PicGo/commit/c79a286))\n* notification freeze the main-process after uploading with clipboard ([3a50315](https://github.com/Molunerfinn/PicGo/commit/3a50315)), closes [#824](https://github.com/Molunerfinn/PicGo/issues/824)\n* picgo.log path error ([6b6ae27](https://github.com/Molunerfinn/PicGo/commit/6b6ae27)), closes [#819](https://github.com/Molunerfinn/PicGo/issues/819)\n\n\n### :pencil: Documentation\n\n* update readme ([6bcda9b](https://github.com/Molunerfinn/PicGo/commit/6bcda9b)), closes [#849](https://github.com/Molunerfinn/PicGo/issues/849) [#850](https://github.com/Molunerfinn/PicGo/issues/850)\n\n\n### :package: Chore\n\n* types change ([43d2a8e](https://github.com/Molunerfinn/PicGo/commit/43d2a8e))\n* update fix-path ([bcaf255](https://github.com/Molunerfinn/PicGo/commit/bcaf255)), closes [#774](https://github.com/Molunerfinn/PicGo/issues/774)\n\n\n\n## :tada: 2.3.1-beta.2 (2022-01-06)\n\n\n### :bug: Bug Fixes\n\n* electron builder actions bug ([5dd6e72](https://github.com/Molunerfinn/PicGo/commit/5dd6e72))\n* linux github actions workflow bug ([5cb8151](https://github.com/Molunerfinn/PicGo/commit/5cb8151))\n\n\n\n## :tada: 2.3.1-beta.1 (2022-01-05)\n\n\n### :package: Chore\n\n* update ci build scripts ([56e814a](https://github.com/Molunerfinn/PicGo/commit/56e814a))\n\n\n\n## :tada: 2.3.1-beta.0 (2022-01-05)\n\n\n### :bug: Bug Fixes\n\n* mini window drag bug ([34b3656](https://github.com/Molunerfinn/PicGo/commit/34b3656))\n\n\n### :package: Chore\n\n* add mac-arm64 build support ([f2a4197](https://github.com/Molunerfinn/PicGo/commit/f2a4197))\n* update electron from v6 -> v16 ([ea20d3b](https://github.com/Molunerfinn/PicGo/commit/ea20d3b))\n\n\n\n# :tada: 2.3.0 (2021-09-11)\n\n\n### :sparkles: Features\n\n* add open devtool option ([75e3edc](https://github.com/Molunerfinn/PicGo/commit/75e3edc))\n\n\n### :bug: Bug Fixes\n\n* shift key function in gallery page ([5895889](https://github.com/Molunerfinn/PicGo/commit/5895889))\n* some bugs ([a676c08](https://github.com/Molunerfinn/PicGo/commit/a676c08)), closes [#722](https://github.com/Molunerfinn/PicGo/issues/722)\n* urlEncode bug when copy ([6c6f847](https://github.com/Molunerfinn/PicGo/commit/6c6f847)), closes [#731](https://github.com/Molunerfinn/PicGo/issues/731)\n\n\n### :pencil: Documentation\n\n* update FAQ ([58420c8](https://github.com/Molunerfinn/PicGo/commit/58420c8))\n\n\n\n# :tada: 2.3.0-beta.8 (2021-08-13)\n\n\n### :bug: Bug Fixes\n\n* settings bug ([20d3cf9](https://github.com/Molunerfinn/PicGo/commit/20d3cf9)), closes [#710](https://github.com/Molunerfinn/PicGo/issues/710)\n* upload clipboard images via http should return list ([ae69263](https://github.com/Molunerfinn/PicGo/commit/ae69263)), closes [#721](https://github.com/Molunerfinn/PicGo/issues/721)\n\n\n\n# :tada: 2.3.0-beta.7 (2021-08-01)\n\n\n### :sparkles: Features\n\n* add gallery db ([6ddd660](https://github.com/Molunerfinn/PicGo/commit/6ddd660))\n* add win32 support ([1657542](https://github.com/Molunerfinn/PicGo/commit/1657542)), closes [#632](https://github.com/Molunerfinn/PicGo/issues/632)\n* finish custom config path ([7030f7a](https://github.com/Molunerfinn/PicGo/commit/7030f7a)), closes [#255](https://github.com/Molunerfinn/PicGo/issues/255)\n\n\n### :bug: Bug Fixes\n\n* bug of gallery db for plugin ([96a63ea](https://github.com/Molunerfinn/PicGo/commit/96a63ea))\n* enable plugin should reload ([49e5f34](https://github.com/Molunerfinn/PicGo/commit/49e5f34)), closes [#659](https://github.com/Molunerfinn/PicGo/issues/659)\n* gallery db bug ([f1eb7f4](https://github.com/Molunerfinn/PicGo/commit/f1eb7f4))\n* multiple uploading in the same time will cause output conflict ([06b67e5](https://github.com/Molunerfinn/PicGo/commit/06b67e5)), closes [#666](https://github.com/Molunerfinn/PicGo/issues/666)\n* multiple uploading in the same time will cause rename failed ([12cecc2](https://github.com/Molunerfinn/PicGo/commit/12cecc2))\n* uploader error in linux ([ab762ef](https://github.com/Molunerfinn/PicGo/commit/ab762ef)), closes [#627](https://github.com/Molunerfinn/PicGo/issues/627)\n* use uploader first ([92022a6](https://github.com/Molunerfinn/PicGo/commit/92022a6))\n* windows ia32 && x64 build options ([bdf523a](https://github.com/Molunerfinn/PicGo/commit/bdf523a))\n\n\n\n# :tada: 2.3.0-beta.6 (2021-04-24)\n\n\n### :sparkles: Features\n\n* add baidu tongji for analytics ([f536391](https://github.com/Molunerfinn/PicGo/commit/f536391))\n* add logs for picgo-server ([2d9e9c0](https://github.com/Molunerfinn/PicGo/commit/2d9e9c0)), closes [#627](https://github.com/Molunerfinn/PicGo/issues/627)\n* add privacy policy ([992ff35](https://github.com/Molunerfinn/PicGo/commit/992ff35))\n\n\n### :bug: Bug Fixes\n\n* add plugin install failed notice ([b05139f](https://github.com/Molunerfinn/PicGo/commit/b05139f))\n* default picBed using picBed.uploader instead of picBed.current ([0a986c8](https://github.com/Molunerfinn/PicGo/commit/0a986c8))\n* dev error with install vue-devtools ([a657c51](https://github.com/Molunerfinn/PicGo/commit/a657c51)), closes [#653](https://github.com/Molunerfinn/PicGo/issues/653) [#658](https://github.com/Molunerfinn/PicGo/issues/658)\n* disable plugin need reload app ([a1b70b4](https://github.com/Molunerfinn/PicGo/commit/a1b70b4))\n* fix analytics value ([06d40ef](https://github.com/Molunerfinn/PicGo/commit/06d40ef))\n* windows cli uploading bug ([321e339](https://github.com/Molunerfinn/PicGo/commit/321e339)), closes [#657](https://github.com/Molunerfinn/PicGo/issues/657)\n\n\n### :package: Chore\n\n* add main process hot reload ([3fd6e4e](https://github.com/Molunerfinn/PicGo/commit/3fd6e4e))\n* fix eslint error notice in indent ([69e1dc8](https://github.com/Molunerfinn/PicGo/commit/69e1dc8))\n\n\n\n# :tada: 2.3.0-beta.5 (2021-04-04)\n\n\n### :sparkles: Features\n\n* add local plugin support && npm registry/proxy support ([f0e1fa1](https://github.com/Molunerfinn/PicGo/commit/f0e1fa1))\n* 为Linux系统适配桌面图标栏（Tray） ([#603](https://github.com/Molunerfinn/PicGo/issues/603)) ([0fe3ade](https://github.com/Molunerfinn/PicGo/commit/0fe3ade))\n\n\n### :bug: Bug Fixes\n\n* default github placeholder ([51d80a6](https://github.com/Molunerfinn/PicGo/commit/51d80a6))\n\n\n### :package: Chore\n\n* change travis-ci -> GitHub Actions ([064f37d](https://github.com/Molunerfinn/PicGo/commit/064f37d))\n\n\n\n# :tada: 2.3.0-beta.4 (2020-12-19)\n\n\n### :sparkles: Features\n\n* add global value for PicGo get GUI_VERSION & CORE_VERSION ([eab014d](https://github.com/Molunerfinn/PicGo/commit/eab014d))\n* **config:** auto configuration backup & fallback to avoid main process crash ([32b8b97](https://github.com/Molunerfinn/PicGo/commit/32b8b97)), closes [#568](https://github.com/Molunerfinn/PicGo/issues/568)\n\n\n### :bug: Bug Fixes\n\n* disabled plugin won't be shown in plugin page ([33fdb16](https://github.com/Molunerfinn/PicGo/commit/33fdb16))\n* shortKeyConfig maybe undefined ([7b5e5ef](https://github.com/Molunerfinn/PicGo/commit/7b5e5ef)), closes [#557](https://github.com/Molunerfinn/PicGo/issues/557)\n* url encode before uploading by url ([ce2b5cd](https://github.com/Molunerfinn/PicGo/commit/ce2b5cd)), closes [#581](https://github.com/Molunerfinn/PicGo/issues/581)\n\n\n### :pencil: Documentation\n\n* add flutter-picgo ([92ff282](https://github.com/Molunerfinn/PicGo/commit/92ff282))\n* add gitads ([2d42381](https://github.com/Molunerfinn/PicGo/commit/2d42381))\n* rm gitads ([bb90e17](https://github.com/Molunerfinn/PicGo/commit/bb90e17))\n* update discussions in doc ([e793599](https://github.com/Molunerfinn/PicGo/commit/e793599))\n* update install command by Homebrew ([#599](https://github.com/Molunerfinn/PicGo/issues/599)) ([bd2311b](https://github.com/Molunerfinn/PicGo/commit/bd2311b))\n* update README.md ([#555](https://github.com/Molunerfinn/PicGo/issues/555)) ([2151857](https://github.com/Molunerfinn/PicGo/commit/2151857))\n\n\n\n# :tada: 2.3.0-beta.3 (2020-07-12)\n\n\n### :bug: Bug Fixes\n\n* choose default picBed failure ([21d3942](https://github.com/Molunerfinn/PicGo/commit/21d3942)), closes [#537](https://github.com/Molunerfinn/PicGo/issues/537)\n* shortkey disabled failure ([4f0809e](https://github.com/Molunerfinn/PicGo/commit/4f0809e)), closes [#534](https://github.com/Molunerfinn/PicGo/issues/534)\n\n\n\n# :tada: 2.3.0-beta.2 (2020-07-12)\n\n\n### :sparkles: Features\n\n* add qrcode for picbeds' config ([7fabc47](https://github.com/Molunerfinn/PicGo/commit/7fabc47))\n\n\n### :bug: Bug Fixes\n\n* encoding the result of picgo-server ([db71139](https://github.com/Molunerfinn/PicGo/commit/db71139))\n* initialize db bugs ([5f87018](https://github.com/Molunerfinn/PicGo/commit/5f87018))\n\n\n### :pencil: Documentation\n\n* update readme ([1c5880a](https://github.com/Molunerfinn/PicGo/commit/1c5880a))\n\n\n\n# :tada: 2.3.0-beta.1 (2020-06-28)\n\n\n### :bug: Bug Fixes\n\n* auto-copy option && copy style ([b6e3adb](https://github.com/Molunerfinn/PicGo/commit/b6e3adb))\n* beta version update bug ([18ad542](https://github.com/Molunerfinn/PicGo/commit/18ad542))\n* paste url encoding bug ([59d3eba](https://github.com/Molunerfinn/PicGo/commit/59d3eba)), closes [#454](https://github.com/Molunerfinn/PicGo/issues/454)\n\n\n### :pencil: Documentation\n\n* update FAQ && ISSUE_TEMPLATE ([2c57a27](https://github.com/Molunerfinn/PicGo/commit/2c57a27))\n* update readme ([2adff1e](https://github.com/Molunerfinn/PicGo/commit/2adff1e))\n\n\n\n# :tada: 2.3.0-beta.0 (2020-04-30)\n\n\n### :sparkles: Features\n\n* add autoCopy option for users to use or not ([67e526f](https://github.com/Molunerfinn/PicGo/commit/67e526f))\n* add beta-version update support ([ad6acd8](https://github.com/Molunerfinn/PicGo/commit/ad6acd8))\n* add smms-v2 support ([3f3ea69](https://github.com/Molunerfinn/PicGo/commit/3f3ea69))\n* add uploading image from URL support ([a28c678](https://github.com/Molunerfinn/PicGo/commit/a28c678))\n* finish all-select && shift multi-select ([2aeca50](https://github.com/Molunerfinn/PicGo/commit/2aeca50)), closes [#342](https://github.com/Molunerfinn/PicGo/issues/342)\n\n\n### :bug: Bug Fixes\n\n* confused port number auto increasing when opening a new PicGo app ([cd70a1a](https://github.com/Molunerfinn/PicGo/commit/cd70a1a))\n* correct inputbox value && remove listener ([32334e9](https://github.com/Molunerfinn/PicGo/commit/32334e9))\n* give a hint when node.js is not installed ([7e86618](https://github.com/Molunerfinn/PicGo/commit/7e86618))\n* right-click menu upload fails with PicGo open ([96cdfea](https://github.com/Molunerfinn/PicGo/commit/96cdfea)), closes [#415](https://github.com/Molunerfinn/PicGo/issues/415)\n* some win10 upload clipboard image crash ([cc182b0](https://github.com/Molunerfinn/PicGo/commit/cc182b0))\n* travis-ci bug ([b357dfb](https://github.com/Molunerfinn/PicGo/commit/b357dfb))\n* url uploader bug ([96544f5](https://github.com/Molunerfinn/PicGo/commit/96544f5))\n\n\n### :pencil: Documentation\n\n* update choco install picgo ([f357557](https://github.com/Molunerfinn/PicGo/commit/f357557))\n* update docs ([fd7673e](https://github.com/Molunerfinn/PicGo/commit/fd7673e))\n* update readme ([fb014dd](https://github.com/Molunerfinn/PicGo/commit/fb014dd))\n\n\n### :package: Chore\n\n* update funding url ([024d9cf](https://github.com/Molunerfinn/PicGo/commit/024d9cf))\n\n\n\n## :tada: 2.2.2 (2020-01-20)\n\n\n### :bug: Bug Fixes\n\n* picgo-server upload clipboard file's result bug ([4ebd76f](https://github.com/Molunerfinn/PicGo/commit/4ebd76f))\n* releaseUrl may can't get latest version ([ee46ab1](https://github.com/Molunerfinn/PicGo/commit/ee46ab1))\n\n\n\n## :tada: 2.2.1 (2020-01-09)\n\n\n### :sparkles: Features\n\n* add alias for plugin config name ([5a06483](https://github.com/Molunerfinn/PicGo/commit/5a06483))\n* add aliyun oss options ([a33f1ad](https://github.com/Molunerfinn/PicGo/commit/a33f1ad)), closes [#347](https://github.com/Molunerfinn/PicGo/issues/347)\n* **server:** add http server for uploading images by a http request ([c56d4ef](https://github.com/Molunerfinn/PicGo/commit/c56d4ef))\n* add server config settings ([6b57cf7](https://github.com/Molunerfinn/PicGo/commit/6b57cf7))\n* only shows visible pic-beds ([9d4d605](https://github.com/Molunerfinn/PicGo/commit/9d4d605)), closes [#310](https://github.com/Molunerfinn/PicGo/issues/310)\n\n\n### :bug: Bug Fixes\n\n* decrease title-bar z-index when config-form dialog shows ([f2750e1](https://github.com/Molunerfinn/PicGo/commit/f2750e1))\n* **website:** website pictures error ([a5b6526](https://github.com/Molunerfinn/PicGo/commit/a5b6526))\n* add new tray icon for macOS dark-mode ([c5adf3b](https://github.com/Molunerfinn/PicGo/commit/c5adf3b)), closes [#267](https://github.com/Molunerfinn/PicGo/issues/267)\n* beforeOpen handler in windows ([cd30a6c](https://github.com/Molunerfinn/PicGo/commit/cd30a6c))\n* busApi event register first && emit later ([e1a0cbb](https://github.com/Molunerfinn/PicGo/commit/e1a0cbb))\n* enum type error ([4e3fa28](https://github.com/Molunerfinn/PicGo/commit/4e3fa28))\n* handle empty request-body ([81e6acb](https://github.com/Molunerfinn/PicGo/commit/81e6acb))\n* launch error in new structrue ([bc8e641](https://github.com/Molunerfinn/PicGo/commit/bc8e641))\n* miniWindow minimize bug ([5f2b7c7](https://github.com/Molunerfinn/PicGo/commit/5f2b7c7))\n* plugin config-form && default plugin logo ([514fc40](https://github.com/Molunerfinn/PicGo/commit/514fc40))\n* release script ([b4f10c6](https://github.com/Molunerfinn/PicGo/commit/b4f10c6))\n* removeById handler error ([c4f0a30](https://github.com/Molunerfinn/PicGo/commit/c4f0a30)), closes [#382](https://github.com/Molunerfinn/PicGo/issues/382)\n* rename page not work ([29a55ed](https://github.com/Molunerfinn/PicGo/commit/29a55ed))\n* save debug mode && PICGO_ENV into config file ([c6ead5b](https://github.com/Molunerfinn/PicGo/commit/c6ead5b))\n* server may never start ([73870a5](https://github.com/Molunerfinn/PicGo/commit/73870a5))\n* settingPage && miniPage style in windows ([3fd9572](https://github.com/Molunerfinn/PicGo/commit/3fd9572))\n\n\n### :pencil: Documentation\n\n* add note for windows electron mirror ([46a49ed](https://github.com/Molunerfinn/PicGo/commit/46a49ed))\n* remove weibo picbed ([e81b8f4](https://github.com/Molunerfinn/PicGo/commit/e81b8f4))\n* update installation by scoop ([91b397d](https://github.com/Molunerfinn/PicGo/commit/91b397d)), closes [#295](https://github.com/Molunerfinn/PicGo/issues/295)\n* update readme ([1b3522e](https://github.com/Molunerfinn/PicGo/commit/1b3522e))\n* update README ([f491209](https://github.com/Molunerfinn/PicGo/commit/f491209))\n* update site ([fe9e19a](https://github.com/Molunerfinn/PicGo/commit/fe9e19a))\n\n\n\n# :tada: 2.2.0 (2020-01-01)\n\n\n### :sparkles: Features\n\n* add alias for plugin config name ([5a06483](https://github.com/Molunerfinn/PicGo/commit/5a06483))\n* add aliyun oss options ([a33f1ad](https://github.com/Molunerfinn/PicGo/commit/a33f1ad)), closes [#347](https://github.com/Molunerfinn/PicGo/issues/347)\n* **server:** add http server for uploading images by a http request ([c56d4ef](https://github.com/Molunerfinn/PicGo/commit/c56d4ef))\n* add server config settings ([6b57cf7](https://github.com/Molunerfinn/PicGo/commit/6b57cf7))\n* only shows visible pic-beds ([9d4d605](https://github.com/Molunerfinn/PicGo/commit/9d4d605)), closes [#310](https://github.com/Molunerfinn/PicGo/issues/310)\n\n\n### :bug: Bug Fixes\n\n* beforeOpen handler in windows ([cd30a6c](https://github.com/Molunerfinn/PicGo/commit/cd30a6c))\n* **website:** website pictures error ([a5b6526](https://github.com/Molunerfinn/PicGo/commit/a5b6526))\n* add new tray icon for macOS dark-mode ([c5adf3b](https://github.com/Molunerfinn/PicGo/commit/c5adf3b)), closes [#267](https://github.com/Molunerfinn/PicGo/issues/267)\n* busApi event register first && emit later ([e1a0cbb](https://github.com/Molunerfinn/PicGo/commit/e1a0cbb))\n* decrease title-bar z-index when config-form dialog shows ([f2750e1](https://github.com/Molunerfinn/PicGo/commit/f2750e1))\n* enum type error ([4e3fa28](https://github.com/Molunerfinn/PicGo/commit/4e3fa28))\n* handle empty request-body ([81e6acb](https://github.com/Molunerfinn/PicGo/commit/81e6acb))\n* launch error in new structrue ([bc8e641](https://github.com/Molunerfinn/PicGo/commit/bc8e641))\n* plugin config-form && default plugin logo ([514fc40](https://github.com/Molunerfinn/PicGo/commit/514fc40))\n* release script ([b4f10c6](https://github.com/Molunerfinn/PicGo/commit/b4f10c6))\n* rename page not work ([29a55ed](https://github.com/Molunerfinn/PicGo/commit/29a55ed))\n* save debug mode && PICGO_ENV into config file ([c6ead5b](https://github.com/Molunerfinn/PicGo/commit/c6ead5b))\n* settingPage && miniPage style in windows ([3fd9572](https://github.com/Molunerfinn/PicGo/commit/3fd9572))\n\n\n### :pencil: Documentation\n\n* add note for windows electron mirror ([46a49ed](https://github.com/Molunerfinn/PicGo/commit/46a49ed))\n* remove weibo picbed ([e81b8f4](https://github.com/Molunerfinn/PicGo/commit/e81b8f4))\n* update installation by scoop ([91b397d](https://github.com/Molunerfinn/PicGo/commit/91b397d)), closes [#295](https://github.com/Molunerfinn/PicGo/issues/295)\n* update readme ([1b3522e](https://github.com/Molunerfinn/PicGo/commit/1b3522e))\n* update README ([f491209](https://github.com/Molunerfinn/PicGo/commit/f491209))\n* update site ([fe9e19a](https://github.com/Molunerfinn/PicGo/commit/fe9e19a))\n\n\n\n## :tada: 2.1.2 (2019-04-19)\n\n\n### :sparkles: Features\n\n* add file-name for customurl ([c59e2bc](https://github.com/Molunerfinn/PicGo/commit/c59e2bc)), closes [#173](https://github.com/Molunerfinn/PicGo/issues/173)\n\n\n### :bug: Bug Fixes\n\n* log-level filter bug ([4e02244](https://github.com/Molunerfinn/PicGo/commit/4e02244)), closes [#237](https://github.com/Molunerfinn/PicGo/issues/237)\n* log-level's reset value from 'all' -> ['all'] ([3c6b329](https://github.com/Molunerfinn/PicGo/commit/3c6b329)), closes [#240](https://github.com/Molunerfinn/PicGo/issues/240) [#237](https://github.com/Molunerfinn/PicGo/issues/237)\n* mini window hidden bug in linux ([466dbec](https://github.com/Molunerfinn/PicGo/commit/466dbec)), closes [#239](https://github.com/Molunerfinn/PicGo/issues/239)\n\n\n\n## :tada: 2.1.1 (2019-04-16)\n\n\n### :bug: Bug Fixes\n\n* upload-area can't upload images ([4000cea](https://github.com/Molunerfinn/PicGo/commit/4000cea))\n\n\n\n# :tada: 2.1.0 (2019-04-15)\n\n\n### :sparkles: Features\n\n* add commandline argvs support for picgo app ([6db86ec](https://github.com/Molunerfinn/PicGo/commit/6db86ec))\n* add gui-api for remove event ([407b821](https://github.com/Molunerfinn/PicGo/commit/407b821)), closes [#201](https://github.com/Molunerfinn/PicGo/issues/201)\n* add windows context menu ([e5fbe75](https://github.com/Molunerfinn/PicGo/commit/e5fbe75))\n* add workflow for mac ([7f17697](https://github.com/Molunerfinn/PicGo/commit/7f17697))\n* support commandline -> upload images in clipboard ([74c7016](https://github.com/Molunerfinn/PicGo/commit/74c7016))\n\n\n### :bug: Bug Fixes\n\n* qiniu area option from select group -> input ([c64959a](https://github.com/Molunerfinn/PicGo/commit/c64959a)), closes [#230](https://github.com/Molunerfinn/PicGo/issues/230)\n\n\n### :pencil: Documentation\n\n* add brew cask source ([6122e17](https://github.com/Molunerfinn/PicGo/commit/6122e17))\n\n\n### :package: Chore\n\n* add picgo bump-version ([37f1d34](https://github.com/Molunerfinn/PicGo/commit/37f1d34))\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## 贡献指南\n\n### 安装与启动\n\n1. 使用 [yarn](https://yarnpkg.com/) 安装依赖\n\n```bash\nyarn install\n```\n\n然后通过\n\n```bash\nyarn dev\n```\n\n启动项目。\n\n2. 只跟 Electron 主进程相关的代码请在 `src/main` 目录下添加。只跟渲染进程相关的代码请在 `src/renderer` 目录下添加。两个进程都能使用的代码请在 `src/universal` 目录下添加。 **注意**：渲染进程没有 `Node.js` 能力，所有渲染进程需要用到 `Node.js` 模块相关的代码请在 `src/main/events/picgoCoreIPC.ts` 下添加事件进行处理。\n\n3. 所有的跨进程事件名请在 `src/universal/events/constants.ts` 里添加。\n\n4. 所有的全局类型定义请在 `src/universal/types/` 里添加，如果是 `enum`，请在 `src/universal/types/enum.ts` 里添加。\n\n\n### i18n\n\n1. 在 `public/i18n/` 下面创建一种语言的 `yml` 文件，例如 `zh-Hans.yml`。然后参考 `zh-CN.yml` 或者 `en.yml` 编写语言文件。并注意，PicGo 会通过语言文件中的 `LANG_DISPLAY_LABEL` 向用户展示该语言的名称。\n\n2. 在 `src/universal/i18n/index.ts` 里添加一种默认语言。其中 `label` 就是语言文件中 `LANG_DISPLAY_LABEL` 的值，`value` 是语言文件名。\n\n3. 如果是对已有语言文件进行更新，请在更新完，务必运行一遍 `yarn gen-i18n`，确保能生成正确的语言定义文件。\n\n### 提交代码\n\n1. 请检查代码没有多余的注释、`console.log` 等调试代码。\n2. 提交代码前，请执行命令 `git add . && yarn cz`，唤起 PicGo 的[代码提交规范工具](https://github.com/PicGo/bump-version)。通过该工具提交代码。\n"
  },
  {
    "path": "CONTRIBUTING_EN.md",
    "content": "## Contribution Guidelines\n\n### Installation and startup\n\n1. Use [yarn](https://yarnpkg.com/) to install dependencies\n\n```bash\nyarn install\n```\n\nthen pass\n\n```bash\nyarn dev\n```\n\nStartup project.\n\n2. Please add code only related to the main process of Electron in the `src/main` directory. Code only related to the rendering process should be added in the `src/renderer` directory. Add code that can be used by both processes in the `src/universal` directory. **Note**: The rendering process does not have the `Node.js` capability. All rendering processes need to use `Node.js modules` related code, please add events under `src/main/events/picgoCoreIPC.ts` for processing.\n\n3. Please add all cross-process event names in `src/universal/events/constants.ts`.\n\n4. Please add all global type definitions in `src/universal/types/`, if it is `enum`, please add it in `src/universal/types/enum.ts`.\n\n\n### i18n\n\n1. Create a language `yml` file under `public/i18n/`, for example `zh-Hans.yml`. Then refer to `zh-CN.yml` or `en.yml` to write language files. Also note that PicGo will display the name of the language to the user via `LANG_DISPLAY_LABEL` in the language file.\n\n2. Add a default language to `src/universal/i18n/index.ts`. where `label` is the value of `LANG_DISPLAY_LABEL` in the language file, and `value` is the name of the language file.\n\n3. If you are updating an existing language file, be sure to run `yarn gen-i18n` after the update to ensure that the correct language definition file can be generated.\n\n### Submit code\n\n1. Please check that the code has no extra comments, `console.log` and other debugging code.\n2. Before submitting the code, please execute the command `git add . && yarn cz` to invoke PicGo's [Code Submission Specification Tool](https://github.com/PicGo/bump-version). Submit code through this tool."
  },
  {
    "path": "FAQ.md",
    "content": "## Frequently Asked Questions / 常见问题\n\n> While using PicGo you may run into various issues. Many of them have already been asked and resolved, so please check the [documentation](https://docs.picgo.app/gui/guide/getting-started), this FAQ, and closed [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed) first — you will likely find the answer there.\n>\n> 在使用 PicGo 期间你会遇到很多问题，不过很多问题其实之前就有人提问过，也被解决，所以你可以先看看 [使用文档](https://docs.picgo.app/gui/guide/getting-started)，这份 FAQ，以及那些被关闭的 [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed)，应该能找到答案。\n\n## 1. Qiniu image host: upload succeeds but images don’t show in Album, or the URL has no `http://` prefix / 七牛图床上传图片成功后，相册里无法显示或图片无`http://`前缀\n\nThis is usually because the `Set URL` (access URL) in your Qiniu image host configuration does not include the `http://` or `https://` scheme.\n\nReference: [issue#79](https://github.com/Molunerfinn/PicGo/issues/79)\n\n通常是你的七牛图床配置里的`设定访问网址`没有加上`http://`或者`https//`头。\n\n参考：[issue#79](https://github.com/Molunerfinn/PicGo/issues/79)\n\n## 2. Can PicGo delete images on the remote image host after upload? / 能否支持图床远端同步删除\n\nNot at the moment. Some image hosts (e.g. Weibo image host, SM.MS, Imgur, etc.) don’t provide a backend management API, so PicGo does not support remote deletion for the sake of a consistent architecture.\n\n暂时不支持。有些图床（比如微博图床、SM.MS、Imgur 等）不支持后台管理，为了架构统一不支持远端删除。\n\n## 3. Can PicGo upload video files? / 能否支持上传视频文件\n\nSome image hosts support uploading video files, but not all. Please follow the capabilities of the image host (and/or the plugin) you are actually using.\n\n目前部分图床支持上传视频文件，但并非所有图床都支持，请以实际使用的图床以及插件为准。\n\n## 4. Weibo image host: uploaded images don’t preview / 微博图床上传之后无法显示预览图\n\nThis is usually caused by having a global proxy enabled.\n\nReference: [issue36](https://github.com/Molunerfinn/PicGo/issues/36)\n\n通常是挂了全局代理导致的。\n\n参考：[issue36](https://github.com/Molunerfinn/PicGo/issues/36)\n\n## 5. Can you add support for an image host? / 能否支持某某某图床\n\nAs of v1.6, PicGo supports the following built-in image hosts:\n\n- `Weibo image host` v1.0\n- `Qiniu image host` v1.0\n- `Tencent Cloud COS v4/v5` v1.1 & v1.5.0\n- `Upyun` v1.2.0\n- `GitHub` v1.5.0\n- `SM.MS` v1.5.1\n- `Alibaba Cloud OSS` v1.6.0\n- `Imgur` v1.6.0\n\nPicGo itself will not add support for additional third-party image hosts as built-in features. If you need other image hosts, please refer to existing third-party [plugins](https://github.com/PicGo/Awesome-PicGo). If the one you need doesn’t exist yet, you’re welcome to develop a plugin and share it with the community.\n\n截止 v1.6，PicGo 支持了如下图床：\n\n- `微博图床` v1.0\n- `七牛图床` v1.0\n- `腾讯云 COS v4\\v5 版本` v1.1 & v1.5.0\n- `又拍云` v1.2.0\n- `GitHub` v1.5.0\n- `SM.MS` v1.5.1\n- `阿里云 OSS` v1.6.0\n- `Imgur` v1.6.0\n\n所以本体内将不会再支持其他第三方图床。需要其他图床支持可以参考目前已有的三方 [插件](https://github.com/PicGo/Awesome-PicGo)，如果还是没有你所需要的图床欢迎开发一个插件供大家使用。\n\n## 6. GitHub image host uploads sometimes succeed and sometimes fail / GitHub 图床有时能上传，有时上传失败\n\n1. The GitHub image host does not allow uploading files with the same name. If you upload a duplicate filename, you will get an error. Enable `Timestamp Rename` to avoid name collisions.\n2. Due to GitHub network conditions (and the Great Firewall in mainland China), uploads may sometimes succeed and sometimes fail — there is no universal fix. For stability, consider using a paid cloud storage service such as Alibaba Cloud or Tencent Cloud; they are usually inexpensive.\n\n1. GitHub 图床不支持上传同名文件，如果有同名文件上传，会报错。建议开启 `时间戳重命名` 避免同名文件。\n2. GitHub 服务器和国内 GFW 的问题会导致有时上传成功，有时上传失败，无解。想要稳定请使用付费云存储，如阿里云、腾讯云等，价格也不会贵。\n\n## 7. Can’t open PicGo’s main window on macOS / Mac 上无法打开 PicGo 的主窗口界面\n\nOn macOS, PicGo is a menu bar app, so it won’t show an icon in the Dock by default. To open the main window, right-click (or two-finger click) the PicGo menu bar icon and choose “Open Main Window”.\n\nStarting from v2.4.1, PicGo lets you hide the Dock icon (`showDockIcon`) and the menu bar icon (`showMenubarIcon`) separately. If you turn both off (set both to `false`), you won’t be able to find the UI via either the Dock or the menu bar.\n\nHow to recover manually:\n\n1. Locate and edit PicGo’s config file `data.json`.\n    - If you can still open the settings page: PicGo Settings -> “Open Config File”.\n    - If you can’t find the UI: the default location is usually `~/Library/Application Support/PicGo/data.json` (if you configured a custom path, follow `configPath`).\n2. Set either field below to `true` (it’s recommended to keep at least one of them `true`). Do not modify other fields.\n3. Save and restart PicGo.\n\nPicGo 在 Mac 上是一个顶部栏应用，在 dock 栏是不会有图标的。要打开主窗口，请右键或者双指点按顶部栏 PicGo 图标，选择「打开详细窗口」即可打开主窗口。\n\n从 v2.4.1 开始，PicGo 支持在 macOS 下分别隐藏 Dock 栏图标（`showDockIcon`）和顶部栏图标（`showMenubarIcon`）。如果你把这两个配置都关闭（都设为 `false`），将会导致你无法通过 Dock 或顶部栏找到 PicGo 主界面。\n\n手动恢复方法：\n\n1. 找到并编辑 PicGo 的配置文件 `data.json`。\n    - 如果还能打开设置页：PicGo 设置 -> 「打开配置文件」。\n    - 如果已经找不到界面：默认配置文件通常在 `~/Library/Application Support/PicGo/data.json`（如果你曾配置过自定义路径，则以配置里的 `configPath` 为准）。\n2. 把以下任意一个字段改为 `true`（建议至少保留一个为 `true`），同时不要删改其他字段：\n\n```json\n{\n   \"settings\": {\n      // other settings ...\n      \"showDockIcon\": true,\n      \"showMenubarIcon\": true\n   }\n}\n```\n\n3. 保存后重启 PicGo。\n\n## 8. Upload failed, or server returned an error / 上传失败，或者是服务器出错\n\n1. PicGo’s built-in image hosts are tested; upload errors are usually not caused by PicGo itself. If you are using the GitHub image host, see FAQ #6.\n2. Check PicGo logs (PicGo Settings -> Log File -> Open) and look for key information in `[PicGo Error]`.\n   1. Search the error message first — you can often find the root cause via search engines without opening an issue.\n   2. If you see `401`, `403`, or other `40X` status codes, it almost certainly means your configuration is wrong. Double-check for typos, trailing spaces, etc.\n   3. If you see `HttpError`, `RequestError`, `socket hang up`, etc., that indicates a network issue. Please check your network, proxy, and DNS settings.\n3. Upload failures caused by network issues are often due to incorrect proxy settings. If you enabled a system proxy, it’s recommended to also configure the corresponding HTTP proxy in PicGo. See [#912](https://github.com/Molunerfinn/PicGo/issues/912)\n\n1. PicGo 自带的图床都经过测试，上传出错一般都不是 PicGo 自身的原因。如果你用的是 GitHub 图床请参考上面的第 6 点。\n2. 检查 PicGo 的日志（报错日志可以在 PicGo 设置 -> 设置日志文件 -> 点击打开 后找到），看看 `[PicGo Error]` 的报错信息里有什么关键信息\n   1. 先自行搜索 error 里的报错信息，往往你能百度或者谷歌出问题原因，不必开 issue。\n   2. 如果有带有 `401` 、`403` 等 `40X` 状态码字样的，不用怀疑，就是你配置写错了，仔细检查配置，看看是否多了空格之类的。\n   3. 如果带有 `HttpError`、`RequestError` 、 `socket hang up` 等字样的说明这是网络问题，我无法帮你解决网络问题，请检查你自己的网络，是否有代理，DNS 设置是否正常等。\n3. 通常网络问题引起的上传失败都是因为代理设置不当导致的。如果开启了系统代理，建议同时也在 PicGo 的代理设置中设置对应的HTTP代理。参考 [#912](https://github.com/Molunerfinn/PicGo/issues/912)\n\n## 9. Installed on macOS but there is no main UI window / macOS版本安装完之后没有主界面\n\nFind the PicGo icon in the macOS menu bar, then right-click (two-finger click on trackpad) to open the menu and choose “Open Main Window”.\n\n请找到PicGo在顶部栏的图标，然后右键（触摸板双指点按，或者鼠标右键），即可找到「打开详细窗口」的菜单。\n\n## 10. Album suddenly can’t show images, or doesn’t refresh after upload, or Typora + PicGo upload succeeds but doesn’t write back / 相册突然无法显示图片 或者 上传后相册不更新 或者 使用Typora+PicGo上传图片成功但是没有写回Typora\n\nThis may be caused by a corrupted album database. Locate `picgo.db` under your PicGo config directory, delete it (backup first if needed), then restart PicGo.\n\nAlso check the log file for errors and open an issue if necessary. Versions >= 2.3.0 have addressed issues caused by a corrupted `picgo.db`, so upgrading is recommended.\n\n这个原因可能是相册存储文件损坏导致的。可以找到 PicGo 配置文件所在路径下的 `picgo.db` ，将其删掉（删掉前建议备份一遍），再重启 PicGo 试试。\n注意同时看看日志文件里有没有什么error，必要时可以提issue。2.3.0以上的版本已经解决因为 `picgo.db` 损坏导致的上述问题，建议更新版本。\n\n## 11. Gitee-related issues / Gitee相关问题\n\nIf you run into upload issues with the Gitee image host, PicGo cannot help because PicGo does not provide an official Gitee uploader. Please open an issue in the repository of the Gitee plugin you are using.\n\n如果在使用 Gitee 图床的时候遇到上传的问题，由于 PicGo 并没有官方提供 Gitee 上传服务，无法帮你解决，请去你所使用的 Gitee 插件仓库发相关的issue。\n\n## 12. On macOS, PicGo shows “App is damaged”, or it doesn’t respond after installation / macOS系统安装完PicGo显示「文件已损坏」或者安装完打开没有反应\n\nPlease try the version >= v2.4.2, which has fixed this issue.\n\n请尝试使用 v2.4.2 及以上版本，已经修复该问题。\n\n\n## 13. Are third-party plugins claiming to be “PicGo Official image host” trustworthy? / 所谓「PicGo 官方图床」的第三方插件是否可信\n\nNo. Any third-party plugin that claims to be a “PicGo Official image host” (including, but not limited to, www.picgo.net) is not an official PicGo image host or service. Please do not trust such claims.\n\nAn official PicGo image host (if any) would be built into PicGo out of the box — it would not require you to download and install a “third-party plugin”, and it would not direct you to an unknown website to purchase or configure a so-called “official image host”. If you choose to use third-party image hosts, please prefer community plugins from reputable sources and assess their safety yourself.\n\n不可信。所有打着「PicGo 官方图床」旗号的第三方插件（包括不限于 www.picgo.net 等）都不是 PicGo 官方提供的图床或服务，请勿轻信。\n\nPicGo 不会以“第三方插件”的形式要求你另外下载安装所谓的 PicGo 官方图床。如果 PicGo 真的做了官方图床，一定是开箱即用的内置在本体里的。如果你需要使用第三方图床，请优先参考 PicGo 官方维护的插件集合与社区仓库，并自行甄别来源与安全性。\n\n## 14. SM.MS migrated to S.EE: how should I update my config? / SM.MS 迁移到 S.EE 后，配置应该怎么改？\n\nSM.MS uploader has migrated to **S.EE** and changed from the original free plan to a paid service.\n\nTo continue uploading normally:\n\n1. Get your API token from [https://s.ee/user/dashboard/](https://s.ee/user/dashboard/).\n2. Check your `picBed.smms.backupDomain`:\n   - if it is an old domain such as `sm.ms` or `smms.app`, remove this field, or\n   - change it to `s.ee`.\n\nSM.MS 上传器已迁移到 **S.EE**，并且服务已从原本免费改为收费。\n\n如需继续正常上传：\n\n1. 到 [https://s.ee/user/dashboard/](https://s.ee/user/dashboard/) 获取 API Token。\n2. 检查你的 `picBed.smms.backupDomain`：\n   - 如果是旧域名（如 `sm.ms`、`smms.app`），请删除该字段，或\n   - 改为 `s.ee`。\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017-present, Molunerfinn\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\" markdown=\"1\">\n  <sup>Special thanks to:</sup>\n  <br>\n  <a href=\"https://go.warp.dev/picgo\">\n    <img alt=\"Warp sponsorship\" width=\"400\" src=\"https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-03.png\">\n  </a>\n\n### [Warp, the intelligent terminal for developers](https://go.warp.dev/picgo)\n[Available for macOS, Linux, & Windows](https://go.warp.dev/picgo)<br>\n\n</div>\n\n<div align=\"center\" markdown=\"1\">\n  <sup>Sponsored by:</sup>\n  <br>\n  <a href=\"https://console.neon.tech/app/?promo=PicGo\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://neon.com/brand/neon-logo-dark-color.svg\">\n      <source media=\"(prefers-color-scheme: light)\" srcset=\"https://neon.com/brand/neon-logo-light-color.svg\">\n      <img alt=\"Neon sponsorship\" width=\"400\" src=\"https://neon.com/brand/neon-logo-dark-color.svg\">\n    </picture>\n  </a>\n\n### [Fast Postgres Databases for Teams and Agents](https://console.neon.tech/app/?promo=PicGo)\n\n</div>\n\n\n---\n\n[中文](./README_zh-CN.md) | **English**\n\n<div align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/New%20LOGO-150.png\" alt=\"PicGo Logo\">\n  <h1>PicGo</h1>\n  <h3>The Ultimate Image Uploader for Efficient Creators</h3>\n  \n  <p align=\"center\">\n    <a href=\"https://github.com/Molunerfinn/PicGo/actions\">\n      <img src=\"https://img.shields.io/badge/code%20style-standard-green.svg?style=flat-square\" alt=\"\">\n    </a>\n    <a href=\"https://github.com/Molunerfinn/PicGo/actions\">\n      <img src=\"https://github.com/Molunerfinn/PicGo/actions/workflows/main.yml/badge.svg\" alt=\"\">\n    </a>\n    <a href=\"https://github.com/Molunerfinn/PicGo/releases\">\n      <img src=\"https://img.shields.io/github/downloads/Molunerfinn/PicGo/total.svg?style=flat-square\" alt=\"\">\n    </a>\n    <a href=\"https://github.com/Molunerfinn/PicGo/releases/latest\">\n      <img src=\"https://img.shields.io/github/release/Molunerfinn/PicGo.svg?style=flat-square\" alt=\"\">\n    </a>\n    <a href=\"https://github.com/PicGo/bump-version\">\n      <img src=\"https://img.shields.io/badge/picgo-convention-blue.svg?style=flat-square\" alt=\"\">\n    </a>\n  </p>\n</div>\n\n## 📖 Overview\n\n**PicGo aims to make image uploading a seamless part of your creative workflow.**\n\nWhether you’re writing a blog post, taking notes, or authoring developer docs, PicGo helps you upload images in one step and automatically copies the resulting link—so you can stay focused on creating, not uploading.\n\n### Supported Image hosts\n\nPicGo supports mainstream Image hosts out of the box, and can be extended indefinitely through its plugin system:\n\n- **China cloud vendors**: Qiniu, Tencent Cloud COS, UPYUN, Alibaba Cloud OSS\n- **International / open platforms**: GitHub, SM.MS(S.EE), Imgur\n- **More options via plugins**: AWS S3, Cloudflare R2, MinIO, and more\n\n> **Note**: PicGo itself will no longer add new third-party Image hosts by default. You can build Image host plugins yourself—see [PicGo-Core](https://docs.picgo.app/core/).\n\n## ✨ Key Features\n\nPicGo is built around a fast, low-friction image upload experience:\n\n### ⚡ Smooth writing flow\n- **Auto-copy links**: once an upload finishes, the link is copied to your clipboard automatically.\n- **Flexible formats**: Markdown, HTML, URL, custom templates—paste directly into any editor.\n- **Zero-Context Switching**: Don't switch windows. Just paste images directly into your favorite editor, and let PicGo handle the upload in the background.\n  - _Enable this workflow via native support or community plugins:_ [Obsidian](https://obsidian.md) \\ [VS Code](https://code.visualstudio.com/) \\ [Typora](https://typora.io/) \\ [Neovim](https://neovim.io/) \\ [MarkText](https://marktext.me/) \\ [SiYuan](https://b3log.org/siyuan/en/) \\ And more...\n\n### 🚀 Fast uploads\n- **Multiple ways to upload**: drag & drop, paste from clipboard, hotkeys, and even right-click context menu upload on macOS/Windows.\n- **Global hotkey**: press `Command+Shift+U` (macOS) / `Ctrl+Shift+U` (Windows/Linux) to open the upload window without leaving your current app. The global key can be customized.\n\n### 🧩 Powerful plugin ecosystem\n- **Highly extensible**: plugins already exist for AWS S3, Cloudflare R2, MinIO, and many other Image hosts.\n- **Even more possibilities**: image compression, watermarking, renaming, Markdown image migration, and more.\n  - Explore plugins: [Awesome-PicGo](https://github.com/PicGo/Awesome-PicGo)\n\n### 🛠 Developer-friendly\n- **HTTP API**: upload via HTTP requests (v2.2.0+), making it easy to integrate with other tools.\n- **Open source**: fully open-source and transparent.\n- **Great documentation**: detailed docs help you get started quickly. For plugin development, see the [PicGo-Core docs](https://docs.picgo.app/core/).\n\n> There’s more to discover—development progress is tracked in [Projects](https://github.com/Molunerfinn/PicGo/projects).\n\nIf you’re new to PicGo, start with the [User Guide](https://docs.picgo.app/gui/guide/getting-started). If you run into issues, check the [FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md) and closed [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed).\n\n## Download & Install\n\n| Source                                                    | Link / Installation                                         | Platform   | Notes                                   |\n| --------------------------------------------------------- | ----------------------------------------------------------- | ---------- | --------------------------------------- |\n| GitHub Releases                                           | https://github.com/Molunerfinn/PicGo/releases               | All        | Downloads may be slow in mainland China |\n| [Shandong University mirror](https://mirrors.sdu.edu.cn/) | https://mirrors.sdu.edu.cn/github-release/Molunerfinn_PicGo | All        | Thanks to the mirror for hosting        |\n| [Scoop](https://scoop.sh/)                                | `scoop bucket add extras` & `scoop install picgo`           | Windows    | Thanks to @huangnauh and @Gladtbam      |\n| [Chocolatey](https://chocolatey.org/)                     | `choco install picgo`                                       | Windows    | Thanks to @iYato                        |\n| [Homebrew](https://brew.sh/)                              | `brew install picgo --cask`                                 | macOS      | Thanks to @womeimingzi11                |\n| [AUR](https://aur.archlinux.org/packages/yay)             | `yay -S picgo-appimage`                                     | Arch Linux | Thanks to @houbaron                     |\n| [Nix](https://search.nixos.org/packages?channel=unstable&query=picgo&show=picgo) | `nix-shell -p picgo`                 | Nix/NixOS  | Thanks to @qrzbing                      |\n\n## Screenshots\n\n![](https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/picgo-2.0.gif)\n\n![picgo-menubar](https://user-images.githubusercontent.com/12621342/34242310-b5056510-e655-11e7-8568-60ffd4f71910.gif)\n\n## Development\n\n> Currently tested on macOS and Windows only. Linux has not been fully tested.\n\nIf you want to learn, contribute, modify, or build PicGo yourself:\n\n> For an Electron-vue learning series, see: [Electron-vue development](https://molunerfinn.com/tags/Electron-vue/)\n\n1. Install Node.js and Git, and make sure you’re familiar with npm basics.\n2. Clone the repo: `git clone https://github.com/Molunerfinn/PicGo.git` and enter the directory.\n3. Install dependencies with `pnpm`. If you don’t have it yet, install it from the [pnpm website](https://pnpm.io/installation) first.\n4. On macOS you’ll need Xcode; on Windows you’ll need Visual Studio.\n5. For contributing, see [CONTRIBUTING.md](./CONTRIBUTING.md).\n\n### Development mode\n\nRun `pnpm run dev` to start the dev workflow with hot reload. Note: dev mode can be unstable and the process may crash—if that happens:\n\n```bash\nctrl+c # stop dev mode\npnpm run dev # restart\n```\n\n> On Windows, after dev mode starts, PicGo’s tray icon will appear in the bottom-right system tray area.\n\n### Production build\n\nTo build release artifacts locally, run `pnpm run build`. After a successful build, the installer files will be generated under `dist`.\n\n**Note**: If your network is unstable, `electron-builder` may fail to download Electron binaries. You can set an alternative mirror before building:\n\n```bash\nexport ELECTRON_MIRROR=\"https://npmmirror.com/mirrors/electron/\"\n# On Windows: set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ (no quotes)\npnpm run build\n```\n\nElectron binaries are stored under `~/.electron/`. If you need to refresh them, delete that directory and rebuild.\n\n## Related Projects\n\n- [vs-picgo](https://github.com/PicGo/vs-picgo): PicGo for VS Code.\n- [flutter-picgo](https://github.com/PicGo/flutter-picgo): mobile app (Android & iOS).\n- [PicHoro](https://github.com/Kuingsmile/PicHoro): another mobile app compatible with PicGo config (Android only for now).\n\n## Sponsorship\n\nIf you like PicGo and it helps your workflow, feel free to buy me a coffee.\n\nAlipay:\n\n![](https://user-images.githubusercontent.com/12621342/34188165-e7cdf372-e56f-11e7-8732-1338c88b9bb7.jpg)\n\nWeChat Pay:\n\n![](https://user-images.githubusercontent.com/12621342/34188201-212cda84-e570-11e7-9b7a-abb298699d85.jpg)\n\nGitHub Sponsors:\n\n[![Sponsor PicGo on GitHub](https://img.shields.io/badge/Sponsor-PicGo-blue.svg?style=flat-square)](https://github.com/sponsors/Molunerfinn)\n\n## License\n\n[MIT](http://opensource.org/licenses/MIT)\n\nCopyright (c) 2017 - Now Molunerfinn\n"
  },
  {
    "path": "README_zh-CN.md",
    "content": "<div align=\"center\" markdown=\"1\">\n  <sup>Special thanks to:</sup>\n  <br>\n  <a href=\"https://go.warp.dev/picgo\">\n    <img alt=\"Warp sponsorship\" width=\"400\" src=\"https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-03.png\">\n  </a>\n\n### [Warp, the intelligent terminal for developers](https://go.warp.dev/picgo)\n[Available for macOS, Linux, & Windows](https://go.warp.dev/picgo)<br>\n\n</div>\n\n<div align=\"center\" markdown=\"1\">\n  <sup>Sponsored by:</sup>\n  <br>\n  <a href=\"https://console.neon.tech/app/?promo=PicGo\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://neon.com/brand/neon-logo-dark-color.svg\">\n      <source media=\"(prefers-color-scheme: light)\" srcset=\"https://neon.com/brand/neon-logo-light-color.svg\">\n      <img alt=\"Neon sponsorship\" width=\"400\" src=\"https://neon.com/brand/neon-logo-dark-color.svg\">\n    </picture>\n  </a>\n\n### [Fast Postgres Databases for Teams and Agents](https://console.neon.tech/app/?promo=PicGo)\n\n</div>\n\n---\n\n**中文** | [English](./README.md)\n\n<div align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/New%20LOGO-150.png\" alt=\"PicGo Logo\">\n  <h1>PicGo</h1>\n  <h3>高效创作者的最佳图片上传工具</h3>\n  <p>The Ultimate Image Uploader for Efficient Creators</p>\n  \n  <p align=\"center\">\n    <a href=\"https://github.com/Molunerfinn/PicGo/actions\">\n      <img src=\"https://img.shields.io/badge/code%20style-standard-green.svg?style=flat-square\" alt=\"\">\n    </a>\n    <a href=\"https://github.com/Molunerfinn/PicGo/actions\">\n      <img src=\"https://github.com/Molunerfinn/PicGo/actions/workflows/main.yml/badge.svg\" alt=\"\">\n    </a>\n    <a href=\"https://github.com/Molunerfinn/PicGo/releases\">\n      <img src=\"https://img.shields.io/github/downloads/Molunerfinn/PicGo/total.svg?style=flat-square\" alt=\"\">\n    </a>\n    <a href=\"https://github.com/Molunerfinn/PicGo/releases/latest\">\n      <img src=\"https://img.shields.io/github/release/Molunerfinn/PicGo.svg?style=flat-square\" alt=\"\">\n    </a>\n    <a href=\"https://github.com/PicGo/bump-version\">\n      <img src=\"https://img.shields.io/badge/picgo-convention-blue.svg?style=flat-square\" alt=\"\">\n    </a>\n  </p>\n</div>\n\n## 📖 应用概述\n\n**PicGo 致力于将图片上传无缝集成到你的创作工作流中。**\n\n无论你是写博客、做笔记还是编写开发文档，PicGo 都能帮你一键上传图片并自动复制链接，让你专注于内容创作本身，而不是繁琐的上传步骤。\n\n### 核心支持\n\nPicGo 原生支持主流图床平台，并可通过插件系统无限扩展：\n\n- **国内云厂商**：七牛云、腾讯云 COS、又拍云、阿里云 OSS\n- **国际/开源平台**：GitHub、SM.MS(S.EE)、Imgur\n- **更多支持**：通过插件支持 AWS S3、Cloudflare R2、MinIO 等第三方图床\n\n> **注意**：PicGo 本体不再增加默认的第三方图床支持。你可以自行开发第三方图床插件。详见 [PicGo-Core](https://docs.picgo.app/core/)。\n\n## ✨ 特色功能\n\nPicGo 打造了全方位的上传体验，让“传图”这件事变得前所未有的简单：\n\n### ⚡️ 无缝写作流\n- **自动复制链接**：上传成功后，链接会自动复制到你的剪贴板。\n- **格式随心定义**：支持 Markdown、HTML、URL、自定义等多种格式，粘贴即用，完美适配你的编辑器。\n- **零上下文切换**：无需切换窗口。在你常用的编辑器里直接粘贴图片，让 PicGo 在后台完成上传。\n  - _通过原生支持或社区插件开启该工作流：_ [Obsidian](https://obsidian.md) \\ [VS Code](https://code.visualstudio.com/) \\ [Typora](https://typora.io/) \\ [Neovim](https://neovim.io/) \\ [MarkText](https://marktext.me/) \\ [SiYuan](https://b3log.org/siyuan/en/) \\ 等等……\n\n### 🚀 极速上传体验\n- **多维上传方式**：支持拖拽图片、剪贴板粘贴、快捷键上传，甚至在 macOS/Windows 上支持右键菜单直接上传。\n- **全局快捷键**：默认 `Command+Shift+U` (macOS) / `Ctrl+Shift+U` (Windows/Linux) 即可唤起上传，无需离开当前窗口。 快捷键可自定义。\n\n### 🧩 强大的插件生态\n- **高度可扩展**：PicGo 拥有丰富的插件系统，已有插件支持 AWS S3、Cloudflare R2、MinIO 等第三方图床。\n- **更多可能**：支持图片压缩、水印、重命名、Markdown 图片迁移等功能插件。\n  - 探索更多插件：[Awesome-PicGo](https://github.com/PicGo/Awesome-PicGo)\n\n### 🛠 开发者友好\n- **HTTP API**：支持通过 HTTP 请求调用 PicGo 上传 (v2.2.0+)，方便与其他工具集成。\n- **开源透明**：代码完全开源，安全可靠。\n- **丰富的文档**：详尽的开发文档助你快速上手。插件开发请参考 [PicGo-Core 文档](https://docs.picgo.app/core/)。\n\n> 更多功能等你自己去发现，开发进度可以查看 [Projects](https://github.com/Molunerfinn/PicGo/projects)。\n\n**如果第一次使用，请参考应用 [使用文档](https://docs.picgo.app/gui/guide/getting-started)。遇到问题了还可以看看 [FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md) 以及被关闭的 [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed)。**\n\n## 下载安装\n\n| 下载源                                        | 地址/安装方式                                               | 平台       | 备注                                                              |\n| --------------------------------------------- | ----------------------------------------------------------- | ---------- | ----------------------------------------------------------------- |\n| GitHub Release                                | https://github.com/Molunerfinn/PicGo/releases               | All        | 国内下载速度可能会慢                                              |\n| [山东大学镜像站](https://mirrors.sdu.edu.cn/) | https://mirrors.sdu.edu.cn/github-release/Molunerfinn_PicGo | All        | 感谢 [山东大学镜像站](https://mirrors.sdu.edu.cn/) 提供的镜像支持 |\n| [Scoop](https://scoop.sh/)                    | `scoop bucket add extras` & `scoop install picgo`           | Windows    | 感谢 @huangnauh 和 @Gladtbam 的贡献                               |\n| [Chocolatey](https://chocolatey.org/)         | `choco install picgo`                                       | Windows    | 感谢 @iYato 的贡献                                                |\n| [Homebrew](https://brew.sh/)                  | `brew install picgo --cask`                                 | macOS      | 感谢 @womeimingzi11 的贡献                                        |\n| [AUR](https://aur.archlinux.org/packages/yay) | `yay -S picgo-appimage`                                     | Arch-Linux | 感谢 @houbaron 的贡献                                             |\n| [Nix](https://search.nixos.org/packages?channel=unstable&query=picgo&show=picgo) | `nix-shell -p picgo`     | Nix/NixOS  | 感谢 @qrzbing 的贡献                                              |\n\n## 应用截图\n\n![](https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/picgo-2.0.gif)\n\n![picgo-menubar](https://user-images.githubusercontent.com/12621342/34242310-b5056510-e655-11e7-8568-60ffd4f71910.gif)\n\n## 开发说明\n\n> 目前仅针对 Mac、Windows。Linux 平台并未测试。\n\n如果你想要学习、开发、修改或自行构建 PicGo，可以依照下面的指示：\n\n> 如果想学习 Electron-vue 的开发，可以查看我写的系列教程——[Electron-vue 开发实战](https://molunerfinn.com/tags/Electron-vue/)\n\n1. 你需要有 Node、Git 环境，了解 npm 的相关知识。\n2. `git clone https://github.com/Molunerfinn/PicGo.git` 并进入项目。\n3. `pnpm` 下载依赖。注意如果你没有 `pnpm`，请去 [官网](https://pnpm.io/installation) 下载安装后再使用。 **用 `pnpm install` 将导致未知错误！**\n4. Mac 需要有 Xcode 环境，Windows 需要有 VS 环境。\n5. 如果需要贡献代码，可以参考[贡献指南](./CONTRIBUTING.md)。\n\n### 开发模式\n\n输入 `pnpm run dev` 进入开发模式，开发模式具有热重载特性。不过需要注意的是，开发模式不稳定，会有进程崩溃的情况。此时需要：\n\n```bash\nctrl+c # 退出开发模式\npnpm run dev # 重新进入开发模式\n```\n\n**注：Windows 开发模式运行之后会在底部任务栏的右下角应用区出现 PicGo 的应用图标。**\n\n### 生产模式\n\n如果你需要自行构建，可以 `pnpm run build` 开始进行构建。构建成功后，会在 `dist` 目录里出现构建成功的相应安装文件。\n\n**注意**：如果你的网络环境不太好，可能会出现 `electron-builder` 下载 `electron` 二进制文件失败的情况。这个时候需要在 build 之前指定一下 `electron` 的源为国内源：\n\n```bash\nexport ELECTRON_MIRROR=\"https://npmmirror.com/mirrors/electron/\"\n# 在 Windows 上，则可以使用 set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ （无需引号）\npnpm run build\n```\n\n只需第一次构建的时候指定一下国内源即可。后续构建不需要特地指定。二进制文件下载在 `~/.electron/` 目录下。如果想要更新 `electron` 构建版本，可以删除 `~/.electron/` 目录，然后重新运行上一步，让 `electron-builder `去下载最新的 `electron` 二进制文件。\n\n## 其他相关\n\n- [vs-picgo](https://github.com/PicGo/vs-picgo)：PicGo 的 VS Code 版。\n- [flutter-picgo](https://github.com/PicGo/flutter-picgo)：PicGo 的手机版 App（支持 Android 和 iOS ）。\n- [PicHoro](https://github.com/Kuingsmile/PicHoro)：另一款支持 PicGo 配置的手机版 App（暂时只支持 Android）。\n\n## 赞助\n\n如果你喜欢 PicGo 并且它对你确实有帮助，欢迎给我打赏一杯咖啡哈~\n\n支付宝：\n\n![](https://user-images.githubusercontent.com/12621342/34188165-e7cdf372-e56f-11e7-8732-1338c88b9bb7.jpg)\n\n微信：\n\n![](https://user-images.githubusercontent.com/12621342/34188201-212cda84-e570-11e7-9b7a-abb298699d85.jpg)\n\nGitHub Sponsors：\n\n[![Sponsor PicGo on GitHub](https://img.shields.io/badge/Sponsor-PicGo-blue.svg?style=flat-square)](https://github.com/sponsors/Molunerfinn)\n\n## License\n\n[MIT](http://opensource.org/licenses/MIT)\n\nCopyright (c) 2017 - Now Molunerfinn\n"
  },
  {
    "path": "babel.config.js",
    "content": "module.exports = {\n  presets: [\n    '@vue/cli-plugin-babel/preset'\n  ],\n  plugins: ['@babel/plugin-proposal-optional-chaining']\n}\n"
  },
  {
    "path": "build/entitlements.mac.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <!-- 允许 JIT (Electron 必须) -->\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    \n    <!-- 允许加载未签名的动态库 (插件、原生模块必须) -->\n    <key>com.apple.security.cs.disable-library-validation</key>\n    <true/>\n    \n    <!-- 允许执行内存中可写的页 (部分 Electron 版本需要) -->\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n  </dict>\n</plist>\n"
  },
  {
    "path": "build/installer.nsh",
    "content": "!macro customInstall\n   SetRegView 64\n   WriteRegStr HKCR \"*\\shell\\PicGo\" \"\" \"Upload pictures w&ith PicGo\"\n   WriteRegStr HKCR \"*\\shell\\PicGo\" \"Icon\" \"$INSTDIR\\PicGo.exe\"\n   WriteRegStr HKCR \"*\\shell\\PicGo\\command\" \"\" '\"$INSTDIR\\PicGo.exe\" \"upload\" \"%1\"'\n   SetRegView 32\n   WriteRegStr HKCR \"*\\shell\\PicGo\" \"\" \"Upload pictures w&ith PicGo\"\n   WriteRegStr HKCR \"*\\shell\\PicGo\" \"Icon\" \"$INSTDIR\\PicGo.exe\"\n   WriteRegStr HKCR \"*\\shell\\PicGo\\command\" \"\" '\"$INSTDIR\\PicGo.exe\" \"upload\" \"%1\"'\n!macroend\n!macro customUninstall\n   DeleteRegKey HKCR \"*\\shell\\PicGo\"\n!macroend\n"
  },
  {
    "path": "changelog/2.4.0.md",
    "content": "# PicGo 2.4.0 Changelog\n\n## Features\n- Add filename display in the gallery (#1050)\n  - Add default placeholders when images/URLs cannot be shown in the gallery or tray window (#1050)\n  ![image](https://user-images.githubusercontent.com/12621342/210806238-e77bc3b1-5e32-4c9f-af5c-278c5706450d.png)\n- Add filename display in the macOS tray window (#1054)\n  - Allow multiple configs per uploader type and choose one for upload (thanks @STDSuperman, #1016)\n  ![](https://user-images.githubusercontent.com/44311619/203093104-9537e08a-2ef0-450d-a59d-c470dbcdd6c8.png)\n- Allow the uploader selection menu to pick a specific config entry for that uploader type\n  ![image](https://user-images.githubusercontent.com/12621342/210804413-4f78804f-a451-4ca5-93a3-63d461261b18.png)\n- Add Dock icon visibility option (#1045)\n  <img width=\"787\" alt=\"image\" src=\"https://github.com/Molunerfinn/PicGo/assets/12621342/bb9492f1-6522-45ce-ae5d-c614901d8b06\" />\n- Add PicGo Repair Toolbox for self-diagnosis\n  <img width=\"324\" alt=\"image\" src=\"https://github.com/Molunerfinn/PicGo/assets/12621342/98cf6e64-7313-4ebe-83d0-aa86e4514109\" />\n- Add option to encode/escape output URLs (#731)\n  <img width=\"775\" alt=\"image\" src=\"https://github.com/Molunerfinn/PicGo/assets/12621342/6e0c13dd-3404-4b07-9c99-412321835b27\" />\n- Support drag-and-drop uploads for arbitrary file formats (#1052)\n- Tencent COS: add Endpoint and Intelligent Compression settings (thanks @palmcivet @yc910920)\n  ![image](https://github.com/Molunerfinn/PicGo/assets/12621342/6e623751-d330-47f8-89bc-986031452914)\n- Add Markdown-rendered tips for uploader config (see tcyun uploader config)\n  ![image](https://github.com/Molunerfinn/PicGo/assets/12621342/2f9b8734-717a-4f49-b397-1ddb9437bc3c)\n- Show the active uploader config name in the upload screen\n  ![image](https://github.com/Molunerfinn/PicGo/assets/12621342/52c58fff-4810-4b91-92d7-5ade512eaca8)\n- Gallery toolbar: bulk replace image URL host (#875)\n  Note: select images first; you can filter by uploader; e.g., replace `https://www.a.com/...` with `https://www.b.com/...`\n  ![image](https://github.com/Molunerfinn/PicGo/assets/12621342/ee314dfc-7699-4ceb-8638-cafe7948bd5a)\n- Add “Startup Mode”: Silent (default) / Main Window; Windows/Linux also support Mini Window (#915)\n  <img width=\"577\" alt=\"image\" src=\"https://github.com/Molunerfinn/PicGo/assets/12621342/7e63bb5c-44b0-480e-824f-3c2edbed4fbb\" />\n- PicGo Server: support multipart form uploads (field `files`, #428, thanks @happy-game)\n  <img width=\"951\" alt=\"image\" src=\"https://github.com/user-attachments/assets/14244f1d-60f5-487f-bde6-6d0e009645fb\" />\n\n## Bug Fixes\n- Fix Windows context-menu config script generation (#1019)\n- Fix custom link URL encoding (#1112)\n- Fix potential logging dead loop (#1101)\n- Fix drag-to-tray upload error (#1107)\n- Fix filename encoding (#1121)\n- Prevent errors on GitHub uploads with duplicate filenames\n- Fix beta.2 style issues\n- Fix slow filename display in the rename window (#1130)\n- Fix “open config file” actually opening the log file (#1163)\n- Fix plugin config dialog not reading/saving correctly\n- Fix inability to add new uploader configs (created as edit) (#1198, #1196)\n- Fix macOS context menu disappearing (thanks @muwoo, #1179)\n- Fix macOS tray window flashing after right-click (thanks @QThans, #1217)\n- Fix uploader config page not scrollable when many fields (#1237)\n- Fix Tencent COS URL encoding (#1265)\n- Fix plugin list search not working (#1297)\n- Fix clipboard filename losing “seconds” (#1293)\n- Fix macOS tray copy-link failing on click (#1280, #1210)\n- Fix auto-copy URL toggle not turning off (#1294, thanks @happy-game)\n- Fix Wayland clipboard image upload issue (#1261, thanks @happy-game)\n- Fix URL uploads with Chinese characters being encoded in filenames (#1339)\n\n## Other\n- COS distribution paused due to malicious traffic charges\n- Upgrade to Vue3\n- Refactor parts of the code\n- Refactor parts of the code (again)\n- Add more detailed hotkey logs (#1031)\n- Known issue: some button styles were incorrect (fixed in later betas)\n- Update README scoop installation section (thanks @wuhang2003)\n\n----------\n\n## Features\n- 新增 相册页新增文件名展示，参考 #1050\n- 新增 相册中和顶部栏窗口中无法展示的图片或者 url 将会展示默认图片，参考#1050\n  ![image](https://user-images.githubusercontent.com/12621342/210806238-e77bc3b1-5e32-4c9f-af5c-278c5706450d.png)\n- 新增 macOS 顶部栏窗口新增文件名展示，参考 #1054\n- 新增 同种类型图床支持多份配置，上传时可以指定某一份配置进行上传。感谢 @STDSuperman ，参考 #1016\n  ![](https://user-images.githubusercontent.com/44311619/203093104-9537e08a-2ef0-450d-a59d-c470dbcdd6c8.png)\n- 新增 选择图床的菜单可以支持选择该图床类型某一项具体配置\n  ![image](https://user-images.githubusercontent.com/12621342/210804413-4f78804f-a451-4ca5-93a3-63d461261b18.png)\n- 新增 显示 dock 栏图标选项，参考 #1045\n  <img width=\"787\" alt=\"image\" src=\"https://github.com/Molunerfinn/PicGo/assets/12621342/bb9492f1-6522-45ce-ae5d-c614901d8b06\" />\n- 新增 PicGo 修复工具箱，可以自行排查一些问题\n  <img width=\"324\" alt=\"image\" src=\"https://github.com/Molunerfinn/PicGo/assets/12621342/98cf6e64-7313-4ebe-83d0-aa86e4514109\" />\n- 新增 对输出的 URL 进行 encode 转义的选项，参考 #731\n  <img width=\"775\" alt=\"image\" src=\"https://github.com/Molunerfinn/PicGo/assets/12621342/6e0c13dd-3404-4b07-9c99-412321835b27\" />\n- 新增 支持拖拽任意格式文件上传，参考 #1052\n- 新增 腾讯云 COS 支持 `Endpoint` 配置 和 `极智压缩` 配置。 感谢 @palmcivet @yc910920 的贡献！\n  ![image](https://github.com/Molunerfinn/PicGo/assets/12621342/6e623751-d330-47f8-89bc-986031452914)\n- 新增 `tips` 配置渲染的支持，可以动态渲染 Uploader config 的 tips。支持 markdown 格式。参考 [tcyun uploader config](https://github.com/PicGo/PicGo-Core/blob/dev/src/plugins/uploader/tcyun.ts#L280)\n  ![image](https://github.com/Molunerfinn/PicGo/assets/12621342/2f9b8734-717a-4f49-b397-1ddb9437bc3c)\n- 新增 上传界面展示当前图床使用的配置名\n  ![image](https://github.com/Molunerfinn/PicGo/assets/12621342/52c58fff-4810-4b91-92d7-5ade512eaca8)\n- 新增 相册页工具栏，目前内置 `批量修改图片 URL HOST 的功能` 。参考 #875\n  注意：需要先选中指定的图片，然后会根据已选中的图片进行修改，你可以通过图床筛选功能只筛选出需要修改的图片\n  例如，你有一批图片都是 `https://www.a.com/...` 打头的 URL，你想把 `www.a.com` 批量修改成 `www.b.com` ，就可以用这个功能\n  ![image](https://github.com/Molunerfinn/PicGo/assets/12621342/ee314dfc-7699-4ceb-8638-cafe7948bd5a)\n- 新增 `启动模式`，可以设置启动的时候是否要打开窗口。全平台支持 `静默启动`（默认值） & `打开主窗口`，Windows 和 Linux 额外支持 `打开 Mini 窗口`。参考 #915\n  <img width=\"577\" alt=\"image\" src=\"https://github.com/Molunerfinn/PicGo/assets/12621342/7e63bb5c-44b0-480e-824f-3c2edbed4fbb\" />\n- 新增 PicGo Server 支持表单形式上传图片。参考 #428 ，感谢 @happy-game\n  表单字段为 files。参考截图：\n  <img width=\"951\" alt=\"image\" src=\"https://github.com/user-attachments/assets/14244f1d-60f5-487f-bde6-6d0e009645fb\" />\n\n## Bug Fixes\n- 修复 windows 右键菜单配置生成脚本，参考 #1019\n- 修复 自定义链接 URL encode 问题，参考 #1112\n- 修复 日志写入可能存在死循环问题，参考 #1101\n- 修复 拖拽文件到顶部栏图标报错问题，参考 #1107\n- 修复 文件名 encode 问题。参考 #1121\n- 修复 GitHub 重名文件上传不再报错\n- 修复 beta.2 版本部分样式问题\n- 修复 重命名窗口某些情况下显示文件名过慢的问题，参考 #1130\n- 修复 打开配置文件打开的是日志文件的 bug，参考 #1163\n- 修复 插件配置弹窗打开后无法正确读取和保存配置的问题\n- 修复 无法新增图床配置的问题（新增变成了编辑）。参考 #1198，#1196\n- 修复 macOS 右键菜单消失问题。感谢 @muwoo 。参考 #1179\n- 修复 macOS 顶部栏窗口右键之后一闪而过的问题。 感谢 @QThans。 参考 #1217\n- 修复 图床配置页面配置项过多时无法滚动到底部的问题。参考 #1237\n- 修复 腾讯云 COS URL encode 的问题。参考 #1265\n- 修复 插件列表无法搜索的问题。 参考 #1297\n- 修复 剪贴板文件名丢失 `秒` 的问题。 参考 #1293\n- 修复 macOS 顶部栏点击图片复制链接失效的问题。 参考 #1280 , #1210\n- 修复 自动复制 URL 这个开关无法被关闭的问题。参考 #1294 ，感谢 @happy-game\n- 修复 wayland 里无法使用剪贴板图片的问题。参考 #1261 ，感谢 @happy-game\n- 修复 直接通过 URL 上传图片的时候，带有汉字的 URL 上传后，文件名被 encode 的问题。 参考 #1339\n\n## Other\n- 由于 PicGo 存储的 COS 空间被恶意刷大量流量导致欠费，暂时停止 COS 渠道的 PicGo 分发\n- 更新至 Vue3\n- 重构部分代码\n- 重构部分代码\n- 补充更详细的快捷键相关日志，参考 #1031\n- 已知问题：部分按钮样式有点问题，下个版本会修复\n- 更新 README中的 scoop 下载安装的部分，感谢 @wuhang2003\n"
  },
  {
    "path": "changelog/2.4.1.md",
    "content": "# PicGo 2.4.1 Changelog\n\n## Features\n\n- Add `showMenubarIcon` setting to control the visibility of the macOS menu bar icon. See #1222 for details\n\n## Bug Fixes\n- Fix: An issue where the macOS menu bar window could not read images from the clipboard. See https://github.com/Molunerfinn/PicGo/issues/1310 for details\n- Fix: An issue where the macOS Intel build could not be opened. Please take a look at #1363 and #1310 for details\n\n## Other\n- Upgraded Electron to v38 and migrated the underlying build framework to Electron-vite\n- Unified the main window UI on Windows/Linux to match the macOS style\n  <img width=\"821\" height=\"472\" alt=\"image\" src=\"https://github.com/user-attachments/assets/dbafadeb-7a9f-40e5-bd87-d4012c9a3902\" />\n- Updated ARM64 builds for Windows & Linux, and added Linux deb packages\n\n----------\n\n## Features\n\n- 新增 `showMenubarIcon` 配置项，用于控制 macOS 顶部栏图标的显示与隐藏。参考 #1222 \n\n## Bug Fixes\n- 修复 macOS 顶部栏窗口无法获取剪贴板图片的问题。参考 https://github.com/Molunerfinn/PicGo/issues/1310\n- 修复 macOS intel 架构 app 无法打开的问题。参考 #1363, #1310\n\n## Other\n- 更新 Electron 版本到 38 以及切换底层开发框架到 Electron-vite\n- 更新 Windows\\Linux 系统现在主窗口的样式跟 macOS 一致了\n  <img width=\"821\" height=\"472\" alt=\"image\" src=\"https://github.com/user-attachments/assets/dbafadeb-7a9f-40e5-bd87-d4012c9a3902\" />\n- 更新 Windows & Linux 平台的 ARM64 架构构建产物；新增 Linux `deb` 构建产物\n"
  },
  {
    "path": "changelog/2.4.2.md",
    "content": "# PicGo 2.4.2 Changelog\n\n## Features\n- New: macOS version with signature && notarization. No more manual handling\n  <img width=\"260\" height=\"260\" alt=\"image\" src=\"https://github.com/user-attachments/assets/dc1b7114-1f10-40e3-b45e-e3b330712399\" />\n- Add duplicate button for image host config item. And add a double confirm dialog for `duplicate` and `delete` config item action\n  <img width=\"800\" height=\"450\" alt=\"image\" src=\"https://github.com/user-attachments/assets/238881e3-e993-4cb7-9699-fd2f7441ce02\" />\n- Add: Refactor notifications and add `notificationSound` setting (#1370)\n\n## Bug Fixes\n- Fix: An issue that the app icon will be too big under macOS 26. See #1367 for more details\n- Fix: Clamp tray image titles to two lines or the style will be broken\n\n## Other\n- Refactor: Remove config store (#1371)\n\n----------\n\n## Features\n- 新增：macOS 版本签名+公证，现在不再需要手动放行，可以直接下载安装使用\n  <img width=\"260\" height=\"260\" alt=\"image\" src=\"https://github.com/user-attachments/assets/dc1b7114-1f10-40e3-b45e-e3b330712399\" />\n- 新增：图床配置界面新增复制按钮，可以一键复制已有配置，方便进行多样化操作。同时复制和删除均增加二次确认弹窗\n  <img width=\"800\" height=\"450\" alt=\"image\" src=\"https://github.com/user-attachments/assets/238881e3-e993-4cb7-9699-fd2f7441ce02\" />\n- 新增：重构通知并新增 `notificationSound` 设置（#1370）\n\n## Bug Fixes\n- 修复：在 macOS 26 版本下 app 图标过大的问题。参考 #1367\n- 修复：托盘图片标题最多显示两行否则样式会被破坏的问题\n\n## Other\n- 重构：移除 config store（#1371）\n"
  },
  {
    "path": "changelog/2.4.3.md",
    "content": "# PicGo 2.4.3 Changelog\n\n## Features\n- Add: Batch URL upload support (#1376). See #1302 for details\n- Add: [Global URL rewrite](https://docs.picgo.app/core/guide/config.html#settings) support (#1377). See #1281 for details\n- Refactor: Gallery image URL can not only rewrite the host but also the whole link. See #1255 for details\n\n----------\n\n## Features\n- 新增：支持批量 URL 上传（#1376），参考 #1302\n- 新增：支持[全局 URL 重写](https://docs.picgo.app/zh/core/guide/config.html#settings)（#1377），参考 #1281\n- 重构：相册页的图片 URL 可以批量修改，不仅仅只是修改 HOST，参考 #1255\n\n## Bug Fixes\n- 修复：相册页图床列表显示状态与相册不同步的问题（#1373），参考 #1372\n- 修复：一些 UI 问题\n"
  },
  {
    "path": "changelog/2.5.1.md",
    "content": "# PicGo 2.5.1 Changelog\n\n## Features\n- Update: Bump `picgo` dependency to `^2.0.1` to support legacy `sm.ms` migrate to `s.ee` (#1385)\n\n## Bug Fixes\n- Fix: Plugin search no longer throws when an npm package description is empty (#1383)\n\n## Other\n- Update: Refresh the documentation link in the GitHub bug report template\n\n----------\n\n## Features\n- 更新：将 `picgo` 依赖升级到 `^2.0.1` 以支持 `s.ee`。参考 #1385\n\n## Bug Fixes\n- 修复：插件搜索在 npm 包描述为空时会报错的问题。参考 #1383\n\n## Other\n- 更新：更新 GitHub 问题模板中的文档链接\n"
  },
  {
    "path": "changelog/2.5.2.md",
    "content": "# PicGo 2.5.2 Changelog\n\n## Bug Fixes\n- Fix: Resolve `s.ee` compatibility issues that could report upload failure even when the upload actually succeeded (#1385)\n\n----------\n\n## Bug Fixes\n- 修复：解决 `s.ee` 兼容性问题，上传实际成功时仍可能提示上传失败（#1385）\n"
  },
  {
    "path": "changelog/2.5.3.md",
    "content": "# PicGo 2.5.3 Changelog\n\n## Bug Fixes\n- Fix: Plugin configuration dialogs now refresh correctly when switching between plugins, avoiding stale form data from the previous plugin (#1394)\n- Fix: Latest version checking now compares stable and beta releases correctly when beta updates are enabled, avoiding incorrect update prompts (#1396)\n\n## Other\n- Update: README now includes a Nix/NixOS setup note, thanks @qrzbing for the contribution! (#1390)\n\n----------\n\n## Bug Fixes\n- 修复：在不同插件之间切换时，插件配置弹窗会正确刷新，避免沿用上一个插件的旧表单数据（#1394）\n- 修复：开启 beta 更新通道后，最新版本检查会正确比较正式版与 beta 版，避免更新提示不准确（#1396）\n\n## Other\n- 更新：README 补充了 Nix/NixOS 相关说明，感谢 @qrzbing 的贡献！（#1390）\n"
  },
  {
    "path": "changelog/gen_changelog.md",
    "content": "# gen_changelog.md\n\nThis guide describes how to generate a consolidated changelog for any PicGo release series (e.g., 2.4.0, 2.5.0) from the GitHub release notes of its betas and finals\n\n## Source of truth\n- Use GitHub release pages only (e.g., `https://github.com/Molunerfinn/PicGo/releases/tag/vX.Y.Z-beta.N` and `vX.Y.Z` if present). You can fetch these via MCP GitHub APIs (e.g., `get_release_by_tag`, `list_releases`) instead of manual browsing\n- Do **not** use `CHANGELOG.md` in the repo\n- Preserve all text and images exactly as written in the releases\n  - Exception: Remove holiday greetings (e.g., \"Happy New Year!\", \"新年快乐！\") from the generated changelog. These can stay in GitHub release notes but should not appear in `changelog/X.Y.Z.md`\n\n## Structure\n- Target file: `changelog/X.Y.Z.md` (replace with the series version)\n- Use these top-level sections when they contain items: `## Features`, `## Bug Fixes`, `## Other`\n- Omit any empty section title entirely (do not output a heading with no bullets)\n- Do **not** nest content under individual beta version headers; merge all items into these sections\n- Keep items in chronological order (early → late) within each section, mirroring the beta sequence\n- Keep inline issue references, thanks, and notes as-is\n- Remove download-link sections entirely\n- After the English sections, insert a line with `----------`, then add a full Chinese translation with the same structure/content (including images)\n\n## Formatting rules\n- Markdown, plain ASCII\n- Bullet lists only; no numbered lists required\n- Indent images under their bullet with two spaces to keep association clear\n- Keep inline HTML image tags from the releases (e.g., `<img width=\"...\">`) untouched\n- Keep line breaks and multi-line notes intact\n- Do not reword or summarize; copy content verbatim except for removing beta headers and download sections\n- If you need to add items from git commit logs (for the same release series), do not paste raw commit messages. Normalize them to the existing changelog style (e.g., use `Add:`, `Fix:`, `Refactor:` prefixes without emoji)\n- Chinese translation must mirror the English bullets in order and content (keep images alongside the translated bullets)\n\n## Regeneration steps\n1) Collect all release bodies for the target series (e.g., `vX.Y.Z-beta.0…N` and, if present, `vX.Y.Z`) from GitHub releases\n2) For each release, copy bullets and images into the appropriate section:\n   - Features ↔ “Feature(s)” or “Features” blocks\n   - Bug Fixes ↔ “Bug Fixes” blocks\n   - Other ↔ “Other”, “Notice”, or misc notes that are not features/bugs\n   - If one section has no items after merging, skip that section heading in both EN and ZH halves\n3) Omit “国内可下载链接” (or any download links) entirely\n4) Drop beta subheadings; keep only section-level bullets in chronological order\n5) Ensure images remain adjacent to their bullets with indentation\n6) After the English sections are complete, insert `----------` on its own line\n7) Append the Chinese translation, preserving bullet order and images, reusing only the section headings that appeared in English (same heading set/order; no “(Chinese)” suffix)\n8) Save the result to `changelog/X.Y.Z.md`\n\n## Quick checklist\n- [ ] All non-empty Features items present with images kept\n- [ ] All non-empty Bug Fixes items present\n- [ ] All non-empty Other notes present\n- [ ] No beta headers\n- [ ] No download links\n- [ ] No empty section headings\n- [ ] Chronological ordering preserved\n- [ ] Chinese translation present with matching bullets/images after `----------`\n"
  },
  {
    "path": "electron-builder.config.ts",
    "content": "import type { Configuration } from 'electron-builder'\nimport dotenv from 'dotenv'\n \ndotenv.config()\n\nconst shouldNotarize = process.env.SKIP_NOTARIZE !== 'true'\n\nconst config: Configuration = {\n  appId: 'com.molunerfinn.picgo',\n  productName: 'PicGo',\n  afterSign: shouldNotarize ? 'scripts/notarize.js' : undefined,\n  // publish: [\n  //   {\n  //     provider: 'github',\n  //     owner: 'Molunerfinn',\n  //     repo: 'PicGo',\n  //     releaseType: 'draft'\n  //   }\n  // ],\n  // temporarily disable auto update feature\n  publish: [],\n  files: [\n    'dist_electron/**/*',\n    'public/**/*',\n    'package.json',\n    '!node_modules/@babel/**/*',\n    \"!**/node_modules/typescript{,/**}\"\n  ],\n  extraResources: [\n    {\n      from: 'public',\n      to: 'public'\n    }\n  ],\n  dmg: {\n    contents: [\n      {\n        x: 410,\n        y: 150,\n        type: 'link',\n        path: '/Applications'\n      },\n      {\n        x: 130,\n        y: 150,\n        type: 'file'\n      }\n    ]\n  },\n  mac: {\n    icon: 'build/icons/icon.icns',\n    extendInfo: {\n      LSUIElement: 0\n    },\n    target: [\n      {\n        target: 'dmg',\n        arch: ['arm64', 'x64']\n      }\n    ],\n    artifactName: 'PicGo-${version}-${arch}.${ext}',\n    hardenedRuntime: true,\n    entitlements: 'build/entitlements.mac.plist',\n    entitlementsInherit: 'build/entitlements.mac.plist',\n    notarize: false\n  },\n  win: {\n    icon: 'build/icons/icon.ico',\n    artifactName: 'PicGo-${version}-${arch}.exe',\n    target: [\n      {\n        target: 'nsis',\n        arch: ['x64']\n      },\n      {\n        target: 'nsis',\n        arch: ['ia32']\n      },\n      {\n        target: 'nsis',\n        arch: ['arm64']\n      }\n    ]\n  },\n  nsis: {\n    shortcutName: 'PicGo',\n    oneClick: false,\n    allowToChangeInstallationDirectory: true,\n    buildUniversalInstaller: false,\n    include: 'build/installer.nsh'\n  },\n  linux: {\n    executableName: 'PicGo',\n    icon: 'build/icons/512x512.png',\n    artifactName: 'PicGo-${version}-${arch}.${ext}',\n    target: [\n      {\n        target: 'AppImage',\n        arch: ['x64', 'arm64']\n      },\n      {\n        target: 'deb',\n        arch: ['x64', 'arm64']\n      },\n      {\n        target: 'snap',\n        arch: ['x64'],\n      }\n    ],\n    maintainer: 'Molunerfinn',\n    category: 'Utility',\n    publish: []\n  },\n  snap: {\n    publish: []\n  }\n}\n\nexport default config\n"
  },
  {
    "path": "electron.vite.config.ts",
    "content": "import { defineConfig, externalizeDepsPlugin } from 'electron-vite'\nimport vue from '@vitejs/plugin-vue'\n// temp for webUtils\nimport electronRenderer from '@molunerfinn/vite-plugin-electron-renderer'\nimport { resolve } from 'path'\n\nconst alias = {\n  '@': resolve(__dirname, 'src/renderer'),\n  '~': resolve(__dirname, 'src'),\n  '#': resolve(__dirname, 'src/universal'),\n  root: resolve(__dirname, '.'),\n  apis: resolve(__dirname, 'src/main/apis'),\n  '@core': resolve(__dirname, 'src/main/apis/core')\n}\n\nexport default defineConfig({\n  main: {\n    plugins: [externalizeDepsPlugin()],\n    resolve: { alias },\n    build: {\n      outDir: 'dist_electron/main',\n      rollupOptions: {\n        input: {\n          index: resolve(__dirname, 'src/background.ts')\n        }\n      }\n    }\n  },\n  preload: {\n    plugins: [externalizeDepsPlugin()],\n    resolve: { alias },\n    build: {\n      outDir: 'dist_electron/preload',\n      rollupOptions: {\n        input: {\n          index: resolve(__dirname, 'src/preload/index.ts')\n        }\n      }\n    }\n  },\n  renderer: {\n    root: 'src/renderer',\n    publicDir: resolve(__dirname, 'src/renderer/public'),\n    resolve: { alias },\n    plugins: [vue(), electronRenderer()],\n    build: {\n      outDir: 'dist_electron/renderer',\n      rollupOptions: {\n        input: {\n          index: resolve(__dirname, 'src/renderer/index.html')\n        }\n      }\n    },\n    server: {\n      port: 5173\n    }\n  }\n})\n"
  },
  {
    "path": "eslint.config.js",
    "content": "const globals = require('globals')\nconst path = require('node:path')\nconst eslintJs = require('@eslint/js')\nconst tsPlugin = require('@typescript-eslint/eslint-plugin')\nconst tsParser = require('@typescript-eslint/parser')\nconst importPlugin = require('eslint-plugin-import')\nconst promisePlugin = require('eslint-plugin-promise')\nconst vuePlugin = require('eslint-plugin-vue')\nconst stylistic = require('@stylistic/eslint-plugin')\n\nconst isProduction = process.env.NODE_ENV === 'production'\nconst vueConfigs = vuePlugin.configs['flat/recommended'].map(config => ({\n  ...config,\n  languageOptions: {\n    ...(config.languageOptions || {}),\n    parserOptions: {\n      ...(config.languageOptions?.parserOptions || {}),\n      parser: tsParser,\n      ecmaVersion: 'latest',\n      sourceType: 'module',\n      extraFileExtensions: ['.vue']\n    }\n  }\n}))\n\nconst tsConfigs = tsPlugin.configs['flat/recommended'].map(config => ({\n  ...config,\n  files: config.files || ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],\n  languageOptions: {\n    ...(config.languageOptions || {}),\n    parser: tsParser,\n    parserOptions: {\n      ...(config.languageOptions?.parserOptions || {}),\n      ecmaVersion: 'latest',\n      sourceType: 'module',\n      extraFileExtensions: ['.vue'],\n      project: path.join(__dirname, 'tsconfig.json'),\n      tsconfigRootDir: __dirname\n    }\n  },\n  rules: {\n    ...(config.rules || {}),\n    'no-unused-vars': 'off',\n    '@typescript-eslint/no-unused-vars': ['error', {\n      argsIgnorePattern: '^_',\n      varsIgnorePattern: '^_',\n      caughtErrors: 'none'\n    }],\n    '@typescript-eslint/no-explicit-any': 'off',\n    '@typescript-eslint/ban-ts-comment': 'off',\n    '@typescript-eslint/no-require-imports': 'off',\n    '@typescript-eslint/no-unsafe-function-type': 'off',\n    '@typescript-eslint/no-empty-object-type': 'off'\n  }\n}))\n\nmodule.exports = [\n  {\n    ignores: [\n      'dist/**',\n      'dist_electron/**',\n      'build/**',\n      'test/unit/coverage/**',\n      'test/unit/*.js',\n      'test/e2e/*.js',\n      'node_modules/**',\n      'vitest.config.ts',\n    ]\n  },\n  {\n    name: 'eslint/base',\n    languageOptions: {\n      ecmaVersion: 'latest',\n      sourceType: 'module',\n      globals: {\n        __static: 'readonly'\n      }\n    },\n    plugins: {\n      import: importPlugin,\n      promise: promisePlugin,\n      '@stylistic': stylistic\n    },\n    settings: {\n      'import/resolver': {\n        node: {\n          extensions: ['.js', '.jsx', '.ts', '.tsx', '.d.ts', '.vue']\n        }\n      }\n    },\n    rules: {\n      ...eslintJs.configs.recommended.rules,\n      ...importPlugin.configs.recommended.rules,\n      ...promisePlugin.configs.recommended.rules,\n      'import/named': 'off',\n      'import/no-named-as-default-member': 'off',\n      'import/no-unresolved': 'off',\n      'promise/catch-or-return': 'off',\n      'promise/always-return': 'off',\n      'no-console': 'off',\n      'no-debugger': isProduction ? 'error' : 'off',\n      'no-async-promise-executor': 'off',\n      'no-empty': ['error', { allowEmptyCatch: true }],\n      'no-unused-vars': 'off',\n      '@stylistic/indent': ['error', 2],\n      '@stylistic/semi': ['error', 'never'],\n      'no-unexpected-multiline': 'error' \n    }\n  },\n  ...vueConfigs,\n  {\n    files: ['*.vue', '**/*.vue'],\n    rules: {\n      'vue/no-v-html': 'off',\n      'vue/attribute-hyphenation': 'off'\n    }\n  },\n  ...tsConfigs,\n  {\n    files: ['**/*.{ts,tsx,vue}'],\n    rules: {\n      'no-undef': 'off'\n    }\n  },\n  {\n    files: ['**/*.d.ts'],\n    rules: {\n      'no-var': 'off',\n      '@typescript-eslint/no-empty-object-type': 'off',\n      '@typescript-eslint/no-explicit-any': 'off'\n    }\n  },\n  {\n    // 1. 针对所有 JS 文件（或者特定目录）启用 Node 全局变量\n    files: [\"**/*.js\", \"scripts/*.js\"], \n    languageOptions: {\n      globals: {\n        ...globals.node, // 注入 process, require, module, __dirname 等\n        ...globals.browser // 如果你的项目是前端项目，可能还需要 browser\n      },\n      sourceType: \"commonjs\" // 如果你的项目代码主要是 CJS，加上这个；如果是 ESM 则设为 \"module\"\n    }\n  },\n]\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"picgo\",\n  \"version\": \"2.5.3\",\n  \"private\": true,\n  \"main\": \"dist_electron/main/index.js\",\n  \"description\": \"A powerful & simple image uploader for creators.\",\n  \"author\": {\n    \"name\": \"Molunerfinn\",\n    \"email\": \"marksz@teamsz.xyz\"\n  },\n  \"scripts\": {\n    \"build\": \"electron-vite build\",\n    \"build:win\": \"npm run build && electron-builder --config electron-builder.config.ts --win --publish never\",\n    \"build:mac\": \"npm run build && electron-builder --config electron-builder.config.ts --mac --publish never\",\n    \"build:linux\": \"npm run build && electron-builder --config electron-builder.config.ts --linux --publish never\",\n    \"build:local\": \"dotenv -e .env -- electron-vite build && electron-builder --config electron-builder.config.ts --publish never\",\n    \"lint\": \"pnpm lint:dpdm && eslint --ext .js,.jsx,.ts,.tsx,.vue src/\",\n    \"tsc\": \"tsc --noEmit\",\n    \"vue-tsc\": \"vue-tsc --noEmit\",\n    \"bump\": \"bump-version\",\n    \"cz\": \"git-cz\",\n    \"dev\": \"electron-vite dev\",\n    \"preview\": \"electron-vite preview\",\n    \"gen-i18n\": \"node ./scripts/gen-i18n-types.js\",\n    \"lint:fix\": \"eslint --fix --ext .js,.jsx,.ts,.tsx,.vue src/\",\n    \"postinstall\": \"electron-builder install-app-deps\",\n    \"postuninstall\": \"electron-builder install-app-deps\",\n    \"upload-dist\": \"node ./scripts/upload-dist.js\",\n    \"lint:dpdm\": \"dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/background.ts\",\n    \"check\": \"pnpm run tsc && pnpm run vue-tsc && pnpm run lint\",\n    \"test\": \"vitest run\",\n    \"prepare\": \"husky\",\n    \"commitlint\": \"commitlint --edit\"\n  },\n  \"dependencies\": {\n    \"@element-plus/icons-vue\": \"^2.3.2\",\n    \"@picgo/i18n\": \"^1.0.0\",\n    \"@picgo/store\": \"^2.1.0\",\n    \"@picgo/video-duration\": \"^1.0.1\",\n    \"axios\": \"^0.19.0\",\n    \"clip-filepaths\": \"^0.3.0\",\n    \"comment-json\": \"^4.5.1\",\n    \"compare-versions\": \"^4.1.3\",\n    \"core-js\": \"^3.27.1\",\n    \"dayjs\": \"^1.11.19\",\n    \"element-plus\": \"^2.3.7\",\n    \"epipebomb\": \"^1.0.0\",\n    \"fs-extra\": \"^10.0.0\",\n    \"js-yaml\": \"^4.1.1\",\n    \"keycode\": \"^2.2.0\",\n    \"lodash\": \"^4.17.21\",\n    \"lodash-id\": \"^0.14.0\",\n    \"lowdb\": \"^1.0.0\",\n    \"marked\": \"^7.0.4\",\n    \"mime-types\": \"^3.0.2\",\n    \"mitt\": \"^3.0.1\",\n    \"multer\": \"^1.4.5-lts.1\",\n    \"picgo\": \"^2.0.3\",\n    \"qrcode.vue\": \"^3.3.3\",\n    \"semver\": \"^7.7.3\",\n    \"shell-path\": \"2.1.0\",\n    \"systeminformation\": \"^5.27.14\",\n    \"tunnel\": \"^0.0.6\",\n    \"uuid\": \"^9.0.0\",\n    \"vue\": \"^3.3.4\",\n    \"vue-router\": \"^4.2.2\",\n    \"vue3-lazyload\": \"^0.3.6\",\n    \"vue3-photo-preview\": \"^0.3.0\",\n    \"write-file-atomic\": \"^7.0.0\"\n  },\n  \"devDependencies\": {\n    \"@aws-sdk/client-s3\": \"^3.276.0\",\n    \"@aws-sdk/lib-storage\": \"^3.276.0\",\n    \"@eslint/js\": \"^9.39.1\",\n    \"@molunerfinn/vite-plugin-electron-renderer\": \"^0.14.7\",\n    \"@picgo/bump-version\": \"^2.0.0\",\n    \"@stylistic/eslint-plugin\": \"^5.6.1\",\n    \"@types/electron-devtools-installer\": \"^2.2.0\",\n    \"@types/fs-extra\": \"^9.0.13\",\n    \"@types/inquirer\": \"^6.5.0\",\n    \"@types/js-yaml\": \"^4.0.5\",\n    \"@types/lodash\": \"^4.17.21\",\n    \"@types/lowdb\": \"^1.0.9\",\n    \"@types/multer\": \"^1.4.12\",\n    \"@types/node\": \"^20\",\n    \"@types/request-promise-native\": \"^1.0.17\",\n    \"@types/semver\": \"^7.3.8\",\n    \"@types/tunnel\": \"^0.0.3\",\n    \"@types/uuid\": \"^9.0.2\",\n    \"@types/write-file-atomic\": \"^4.0.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.49.0\",\n    \"@typescript-eslint/parser\": \"^8.49.0\",\n    \"@vitejs/plugin-vue\": \"^6.0.2\",\n    \"autoprefixer\": \"^10.4.14\",\n    \"commitizen\": \"^4.3.1\",\n    \"conventional-changelog\": \"^3.1.18\",\n    \"cz-customizable\": \"^7.5.1\",\n    \"dotenv\": \"^16.0.1\",\n    \"dotenv-cli\": \"^11.0.0\",\n    \"dpdm\": \"^3.13.1\",\n    \"electron\": \"^38\",\n    \"electron-builder\": \"26.1.0\",\n    \"electron-devtools-installer\": \"^3.2.0\",\n    \"electron-vite\": \"^4.0.1\",\n    \"eslint\": \"^9.39.1\",\n    \"eslint-plugin-import\": \"^2.32.0\",\n    \"eslint-plugin-promise\": \"^7.2.1\",\n    \"eslint-plugin-vue\": \"^10.6.2\",\n    \"globals\": \"^16.5.0\",\n    \"husky\": \"^9.1.7\",\n    \"postcss\": \"^8.4.23\",\n    \"stylus\": \"^0.54.7\",\n    \"stylus-loader\": \"^3.0.2\",\n    \"tailwindcss\": \"^3.3.2\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^7.2.6\",\n    \"vitest\": \"^4.0.16\",\n    \"vue-tsc\": \"^3.2.3\"\n  },\n  \"commitlint\": {\n    \"extends\": [\n      \"./node_modules/@picgo/bump-version/commitlint-picgo\"\n    ]\n  },\n  \"config\": {\n    \"commitizen\": {\n      \"path\": \"./node_modules/cz-customizable\"\n    },\n    \"cz-customizable\": {\n      \"config\": \"./node_modules/@picgo/bump-version/.cz-config.js\"\n    }\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"commit-msg\": \"npm run lint:dpdm && commitlint -E HUSKY_GIT_PARAMS\"\n    }\n  }\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "onlyBuiltDependencies:\n  - core-js\n  - ejs\n  - electron\n  - electron-winstaller\n  - esbuild\n  - husky\n  - vue-demi\n\n# for multi-arch builds, include both x64 and arm64 versions of electron\n# will be deprecated in future(use different arch machine to build different arch binaries)\nsupportedArchitectures:\n  os:\n    - current\n  cpu:\n    - x64\n    - arm64\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    autoprefixer: {},\n    tailwindcss: {}\n  }\n}\n"
  },
  {
    "path": "public/Upload pictures with PicGo.workflow/Contents/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>NSServices</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>NSBackgroundColorName</key>\n\t\t\t<string>background</string>\n\t\t\t<key>NSBackgroundSystemColorName</key>\n\t\t\t<string>systemBlueColor</string>\n\t\t\t<key>NSIconName</key>\n\t\t\t<string>NSTouchBarShare</string>\n\t\t\t<key>NSMenuItem</key>\n\t\t\t<dict>\n\t\t\t\t<key>default</key>\n\t\t\t\t<string>Upload pictures with PicGo</string>\n\t\t\t</dict>\n\t\t\t<key>NSMessage</key>\n\t\t\t<string>runWorkflowAsService</string>\n\t\t\t<key>NSRequiredContext</key>\n\t\t\t<dict>\n\t\t\t\t<key>NSApplicationIdentifier</key>\n\t\t\t\t<string>com.apple.finder</string>\n\t\t\t</dict>\n\t\t\t<key>NSSendFileTypes</key>\n\t\t\t<array>\n\t\t\t\t<string>public.image</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "public/Upload pictures with PicGo.workflow/Contents/document.wflow",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>AMApplicationBuild</key>\n\t<string>444.42</string>\n\t<key>AMApplicationVersion</key>\n\t<string>2.9</string>\n\t<key>AMDocumentVersion</key>\n\t<string>2</string>\n\t<key>actions</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>action</key>\n\t\t\t<dict>\n\t\t\t\t<key>AMAccepts</key>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>Container</key>\n\t\t\t\t\t<string>List</string>\n\t\t\t\t\t<key>Optional</key>\n\t\t\t\t\t<true/>\n\t\t\t\t\t<key>Types</key>\n\t\t\t\t\t<array>\n\t\t\t\t\t\t<string>com.apple.cocoa.string</string>\n\t\t\t\t\t</array>\n\t\t\t\t</dict>\n\t\t\t\t<key>AMActionVersion</key>\n\t\t\t\t<string>2.0.3</string>\n\t\t\t\t<key>AMApplication</key>\n\t\t\t\t<array>\n\t\t\t\t\t<string>自动操作</string>\n\t\t\t\t</array>\n\t\t\t\t<key>AMParameterProperties</key>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>COMMAND_STRING</key>\n\t\t\t\t\t<dict/>\n\t\t\t\t\t<key>CheckedForUserDefaultShell</key>\n\t\t\t\t\t<dict/>\n\t\t\t\t\t<key>inputMethod</key>\n\t\t\t\t\t<dict/>\n\t\t\t\t\t<key>shell</key>\n\t\t\t\t\t<dict/>\n\t\t\t\t\t<key>source</key>\n\t\t\t\t\t<dict/>\n\t\t\t\t</dict>\n\t\t\t\t<key>AMProvides</key>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>Container</key>\n\t\t\t\t\t<string>List</string>\n\t\t\t\t\t<key>Types</key>\n\t\t\t\t\t<array>\n\t\t\t\t\t\t<string>com.apple.cocoa.string</string>\n\t\t\t\t\t</array>\n\t\t\t\t</dict>\n\t\t\t\t<key>ActionBundlePath</key>\n\t\t\t\t<string>/System/Library/Automator/Run Shell Script.action</string>\n\t\t\t\t<key>ActionName</key>\n\t\t\t\t<string>运行 Shell 脚本</string>\n\t\t\t\t<key>ActionParameters</key>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>COMMAND_STRING</key>\n\t\t\t\t\t<string>/Applications/PicGo.app/Contents/MacOS/PicGo upload \"$@\" &gt; /dev/null 2&gt;&amp;1 &amp;</string>\n\t\t\t\t\t<key>CheckedForUserDefaultShell</key>\n\t\t\t\t\t<true/>\n\t\t\t\t\t<key>inputMethod</key>\n\t\t\t\t\t<integer>1</integer>\n\t\t\t\t\t<key>shell</key>\n\t\t\t\t\t<string>/bin/bash</string>\n\t\t\t\t\t<key>source</key>\n\t\t\t\t\t<string></string>\n\t\t\t\t</dict>\n\t\t\t\t<key>BundleIdentifier</key>\n\t\t\t\t<string>com.apple.RunShellScript</string>\n\t\t\t\t<key>CFBundleVersion</key>\n\t\t\t\t<string>2.0.3</string>\n\t\t\t\t<key>CanShowSelectedItemsWhenRun</key>\n\t\t\t\t<false/>\n\t\t\t\t<key>CanShowWhenRun</key>\n\t\t\t\t<true/>\n\t\t\t\t<key>Category</key>\n\t\t\t\t<array>\n\t\t\t\t\t<string>AMCategoryUtilities</string>\n\t\t\t\t</array>\n\t\t\t\t<key>Class Name</key>\n\t\t\t\t<string>RunShellScriptAction</string>\n\t\t\t\t<key>InputUUID</key>\n\t\t\t\t<string>79609224-28DD-4ADE-AA8F-5A6C68C18C18</string>\n\t\t\t\t<key>Keywords</key>\n\t\t\t\t<array>\n\t\t\t\t\t<string>Shell</string>\n\t\t\t\t\t<string>脚本</string>\n\t\t\t\t\t<string>命令</string>\n\t\t\t\t\t<string>运行</string>\n\t\t\t\t\t<string>Unix</string>\n\t\t\t\t</array>\n\t\t\t\t<key>OutputUUID</key>\n\t\t\t\t<string>35CD6B4C-A616-4F89-8D76-DCD3249C5B4E</string>\n\t\t\t\t<key>UUID</key>\n\t\t\t\t<string>4350A83B-E7E6-4D2B-9768-B1D676CF58F3</string>\n\t\t\t\t<key>UnlocalizedApplications</key>\n\t\t\t\t<array>\n\t\t\t\t\t<string>Automator</string>\n\t\t\t\t</array>\n\t\t\t\t<key>arguments</key>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>0</key>\n\t\t\t\t\t<dict>\n\t\t\t\t\t\t<key>default value</key>\n\t\t\t\t\t\t<integer>0</integer>\n\t\t\t\t\t\t<key>name</key>\n\t\t\t\t\t\t<string>inputMethod</string>\n\t\t\t\t\t\t<key>required</key>\n\t\t\t\t\t\t<string>0</string>\n\t\t\t\t\t\t<key>type</key>\n\t\t\t\t\t\t<string>0</string>\n\t\t\t\t\t\t<key>uuid</key>\n\t\t\t\t\t\t<string>0</string>\n\t\t\t\t\t</dict>\n\t\t\t\t\t<key>1</key>\n\t\t\t\t\t<dict>\n\t\t\t\t\t\t<key>default value</key>\n\t\t\t\t\t\t<string></string>\n\t\t\t\t\t\t<key>name</key>\n\t\t\t\t\t\t<string>source</string>\n\t\t\t\t\t\t<key>required</key>\n\t\t\t\t\t\t<string>0</string>\n\t\t\t\t\t\t<key>type</key>\n\t\t\t\t\t\t<string>0</string>\n\t\t\t\t\t\t<key>uuid</key>\n\t\t\t\t\t\t<string>1</string>\n\t\t\t\t\t</dict>\n\t\t\t\t\t<key>2</key>\n\t\t\t\t\t<dict>\n\t\t\t\t\t\t<key>default value</key>\n\t\t\t\t\t\t<false/>\n\t\t\t\t\t\t<key>name</key>\n\t\t\t\t\t\t<string>CheckedForUserDefaultShell</string>\n\t\t\t\t\t\t<key>required</key>\n\t\t\t\t\t\t<string>0</string>\n\t\t\t\t\t\t<key>type</key>\n\t\t\t\t\t\t<string>0</string>\n\t\t\t\t\t\t<key>uuid</key>\n\t\t\t\t\t\t<string>2</string>\n\t\t\t\t\t</dict>\n\t\t\t\t\t<key>3</key>\n\t\t\t\t\t<dict>\n\t\t\t\t\t\t<key>default value</key>\n\t\t\t\t\t\t<string></string>\n\t\t\t\t\t\t<key>name</key>\n\t\t\t\t\t\t<string>COMMAND_STRING</string>\n\t\t\t\t\t\t<key>required</key>\n\t\t\t\t\t\t<string>0</string>\n\t\t\t\t\t\t<key>type</key>\n\t\t\t\t\t\t<string>0</string>\n\t\t\t\t\t\t<key>uuid</key>\n\t\t\t\t\t\t<string>3</string>\n\t\t\t\t\t</dict>\n\t\t\t\t\t<key>4</key>\n\t\t\t\t\t<dict>\n\t\t\t\t\t\t<key>default value</key>\n\t\t\t\t\t\t<string>/bin/sh</string>\n\t\t\t\t\t\t<key>name</key>\n\t\t\t\t\t\t<string>shell</string>\n\t\t\t\t\t\t<key>required</key>\n\t\t\t\t\t\t<string>0</string>\n\t\t\t\t\t\t<key>type</key>\n\t\t\t\t\t\t<string>0</string>\n\t\t\t\t\t\t<key>uuid</key>\n\t\t\t\t\t\t<string>4</string>\n\t\t\t\t\t</dict>\n\t\t\t\t</dict>\n\t\t\t\t<key>isViewVisible</key>\n\t\t\t\t<true/>\n\t\t\t\t<key>location</key>\n\t\t\t\t<string>449.000000:305.000000</string>\n\t\t\t\t<key>nibPath</key>\n\t\t\t\t<string>/System/Library/Automator/Run Shell Script.action/Contents/Resources/Base.lproj/main.nib</string>\n\t\t\t</dict>\n\t\t\t<key>isViewVisible</key>\n\t\t\t<true/>\n\t\t</dict>\n\t</array>\n\t<key>connectors</key>\n\t<dict/>\n\t<key>workflowMetaData</key>\n\t<dict>\n\t\t<key>applicationBundleID</key>\n\t\t<string>com.apple.finder</string>\n\t\t<key>applicationBundleIDsByPath</key>\n\t\t<dict>\n\t\t\t<key>/System/Library/CoreServices/Finder.app</key>\n\t\t\t<string>com.apple.finder</string>\n\t\t</dict>\n\t\t<key>applicationPath</key>\n\t\t<string>/System/Library/CoreServices/Finder.app</string>\n\t\t<key>applicationPaths</key>\n\t\t<array>\n\t\t\t<string>/System/Library/CoreServices/Finder.app</string>\n\t\t</array>\n\t\t<key>backgroundColor</key>\n\t\t<data>\n\t\tYnBsaXN0MDDUAQIDBAUGNjdYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVy\n\t\tVCR0b3ASAAGGoKoHCBMUFR4kKC8zVSRudWxs1QkKCwwNDg8QERJWJGNsYXNz\n\t\tW05TQ29sb3JOYW1lXE5TQ29sb3JTcGFjZV1OU0NhdGFsb2dOYW1lV05TQ29s\n\t\tb3KACYADEAaAAoAEVlN5c3RlbV8QD3N5c3RlbUJsdWVDb2xvctUWFwsYCRka\n\t\tGxwOXE5TQ29tcG9uZW50c1VOU1JHQl8QEk5TQ3VzdG9tQ29sb3JTcGFjZUcw\n\t\tIDAgMSAxTxARMCAwIDAuOTk4MTg4OTcyNQAQAYAFgAnTHyAJISIjVE5TSURV\n\t\tTlNJQ0MQB4AGgAjSJQkmJ1dOUy5kYXRhTxEMSAAADEhMaW5vAhAAAG1udHJS\n\t\tR0IgWFlaIAfOAAIACQAGADEAAGFjc3BNU0ZUAAAAAElFQyBzUkdCAAAAAAAA\n\t\tAAAAAAAAAAD21gABAAAAANMtSFAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\t\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEWNwcnQAAAFQAAAAM2Rlc2MAAAGE\n\t\tAAAAbHd0cHQAAAHwAAAAFGJrcHQAAAIEAAAAFHJYWVoAAAIYAAAAFGdYWVoA\n\t\tAAIsAAAAFGJYWVoAAAJAAAAAFGRtbmQAAAJUAAAAcGRtZGQAAALEAAAAiHZ1\n\t\tZWQAAANMAAAAhnZpZXcAAAPUAAAAJGx1bWkAAAP4AAAAFG1lYXMAAAQMAAAA\n\t\tJHRlY2gAAAQwAAAADHJUUkMAAAQ8AAAIDGdUUkMAAAQ8AAAIDGJUUkMAAAQ8\n\t\tAAAIDHRleHQAAAAAQ29weXJpZ2h0IChjKSAxOTk4IEhld2xldHQtUGFja2Fy\n\t\tZCBDb21wYW55AABkZXNjAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAA\n\t\tAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\t\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAA81EAAQAA\n\t\tAAEWzFhZWiAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAG+iAAA49QAAA5BY\n\t\tWVogAAAAAAAAYpkAALeFAAAY2lhZWiAAAAAAAAAkoAAAD4QAALbPZGVzYwAA\n\t\tAAAAAAAWSUVDIGh0dHA6Ly93d3cuaWVjLmNoAAAAAAAAAAAAAAAWSUVDIGh0\n\t\tdHA6Ly93d3cuaWVjLmNoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\t\tAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAALklFQyA2MTk2Ni0yLjEgRGVm\n\t\tYXVsdCBSR0IgY29sb3VyIHNwYWNlIC0gc1JHQgAAAAAAAAAAAAAALklFQyA2\n\t\tMTk2Ni0yLjEgRGVmYXVsdCBSR0IgY29sb3VyIHNwYWNlIC0gc1JHQgAAAAAA\n\t\tAAAAAAAAAAAAAAAAAAAAAABkZXNjAAAAAAAAACxSZWZlcmVuY2UgVmlld2lu\n\t\tZyBDb25kaXRpb24gaW4gSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAsUmVmZXJl\n\t\tbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAA\n\t\tAAAAAAAAAAAAAAAAAAAAAAAAdmlldwAAAAAAE6T+ABRfLgAQzxQAA+3MAAQT\n\t\tCwADXJ4AAAABWFlaIAAAAAAATAlWAFAAAABXH+dtZWFzAAAAAAAAAAEAAAAA\n\t\tAAAAAAAAAAAAAAAAAAACjwAAAAJzaWcgAAAAAENSVCBjdXJ2AAAAAAAABAAA\n\t\tAAAFAAoADwAUABkAHgAjACgALQAyADcAOwBAAEUASgBPAFQAWQBeAGMAaABt\n\t\tAHIAdwB8AIEAhgCLAJAAlQCaAJ8ApACpAK4AsgC3ALwAwQDGAMsA0ADVANsA\n\t\t4ADlAOsA8AD2APsBAQEHAQ0BEwEZAR8BJQErATIBOAE+AUUBTAFSAVkBYAFn\n\t\tAW4BdQF8AYMBiwGSAZoBoQGpAbEBuQHBAckB0QHZAeEB6QHyAfoCAwIMAhQC\n\t\tHQImAi8COAJBAksCVAJdAmcCcQJ6AoQCjgKYAqICrAK2AsECywLVAuAC6wL1\n\t\tAwADCwMWAyEDLQM4A0MDTwNaA2YDcgN+A4oDlgOiA64DugPHA9MD4APsA/kE\n\t\tBgQTBCAELQQ7BEgEVQRjBHEEfgSMBJoEqAS2BMQE0wThBPAE/gUNBRwFKwU6\n\t\tBUkFWAVnBXcFhgWWBaYFtQXFBdUF5QX2BgYGFgYnBjcGSAZZBmoGewaMBp0G\n\t\trwbABtEG4wb1BwcHGQcrBz0HTwdhB3QHhgeZB6wHvwfSB+UH+AgLCB8IMghG\n\t\tCFoIbgiCCJYIqgi+CNII5wj7CRAJJQk6CU8JZAl5CY8JpAm6Cc8J5Qn7ChEK\n\t\tJwo9ClQKagqBCpgKrgrFCtwK8wsLCyILOQtRC2kLgAuYC7ALyAvhC/kMEgwq\n\t\tDEMMXAx1DI4MpwzADNkM8w0NDSYNQA1aDXQNjg2pDcMN3g34DhMOLg5JDmQO\n\t\tfw6bDrYO0g7uDwkPJQ9BD14Peg+WD7MPzw/sEAkQJhBDEGEQfhCbELkQ1xD1\n\t\tERMRMRFPEW0RjBGqEckR6BIHEiYSRRJkEoQSoxLDEuMTAxMjE0MTYxODE6QT\n\t\txRPlFAYUJxRJFGoUixStFM4U8BUSFTQVVhV4FZsVvRXgFgMWJhZJFmwWjxay\n\t\tFtYW+hcdF0EXZReJF64X0hf3GBsYQBhlGIoYrxjVGPoZIBlFGWsZkRm3Gd0a\n\t\tBBoqGlEadxqeGsUa7BsUGzsbYxuKG7Ib2hwCHCocUhx7HKMczBz1HR4dRx1w\n\t\tHZkdwx3sHhYeQB5qHpQevh7pHxMfPh9pH5Qfvx/qIBUgQSBsIJggxCDwIRwh\n\t\tSCF1IaEhziH7IiciVSKCIq8i3SMKIzgjZiOUI8Ij8CQfJE0kfCSrJNolCSU4\n\t\tJWgllyXHJfcmJyZXJocmtyboJxgnSSd6J6sn3CgNKD8ocSiiKNQpBik4KWsp\n\t\tnSnQKgIqNSpoKpsqzysCKzYraSudK9EsBSw5LG4soizXLQwtQS12Last4S4W\n\t\tLkwugi63Lu4vJC9aL5Evxy/+MDUwbDCkMNsxEjFKMYIxujHyMioyYzKbMtQz\n\t\tDTNGM38zuDPxNCs0ZTSeNNg1EzVNNYc1wjX9Njc2cjauNuk3JDdgN5w31zgU\n\t\tOFA4jDjIOQU5Qjl/Obw5+To2OnQ6sjrvOy07azuqO+g8JzxlPKQ84z0iPWE9\n\t\toT3gPiA+YD6gPuA/IT9hP6I/4kAjQGRApkDnQSlBakGsQe5CMEJyQrVC90M6\n\t\tQ31DwEQDREdEikTORRJFVUWaRd5GIkZnRqtG8Ec1R3tHwEgFSEtIkUjXSR1J\n\t\tY0mpSfBKN0p9SsRLDEtTS5pL4kwqTHJMuk0CTUpNk03cTiVObk63TwBPSU+T\n\t\tT91QJ1BxULtRBlFQUZtR5lIxUnxSx1MTU19TqlP2VEJUj1TbVShVdVXCVg9W\n\t\tXFapVvdXRFeSV+BYL1h9WMtZGllpWbhaB1pWWqZa9VtFW5Vb5Vw1XIZc1l0n\n\t\tXXhdyV4aXmxevV8PX2Ffs2AFYFdgqmD8YU9homH1YklinGLwY0Njl2PrZEBk\n\t\tlGTpZT1lkmXnZj1mkmboZz1nk2fpaD9olmjsaUNpmmnxakhqn2r3a09rp2v/\n\t\tbFdsr20IbWBtuW4SbmtuxG8eb3hv0XArcIZw4HE6cZVx8HJLcqZzAXNdc7h0\n\t\tFHRwdMx1KHWFdeF2Pnabdvh3VnezeBF4bnjMeSp5iXnnekZ6pXsEe2N7wnwh\n\t\tfIF84X1BfaF+AX5ifsJ/I3+Ef+WAR4CogQqBa4HNgjCCkoL0g1eDuoQdhICE\n\t\t44VHhauGDoZyhteHO4efiASIaYjOiTOJmYn+imSKyoswi5aL/IxjjMqNMY2Y\n\t\tjf+OZo7OjzaPnpAGkG6Q1pE/kaiSEZJ6kuOTTZO2lCCUipT0lV+VyZY0lp+X\n\t\tCpd1l+CYTJi4mSSZkJn8mmia1ZtCm6+cHJyJnPedZJ3SnkCerp8dn4uf+qBp\n\t\toNihR6G2oiailqMGo3aj5qRWpMelOKWpphqmi6b9p26n4KhSqMSpN6mpqhyq\n\t\tj6sCq3Wr6axcrNCtRK24ri2uoa8Wr4uwALB1sOqxYLHWskuywrM4s660JbSc\n\t\ttRO1irYBtnm28Ldot+C4WbjRuUq5wro7urW7LrunvCG8m70VvY++Cr6Evv+/\n\t\ter/1wHDA7MFnwePCX8Lbw1jD1MRRxM7FS8XIxkbGw8dBx7/IPci8yTrJuco4\n\t\tyrfLNsu2zDXMtc01zbXONs62zzfPuNA50LrRPNG+0j/SwdNE08bUSdTL1U7V\n\t\t0dZV1tjXXNfg2GTY6Nls2fHadtr724DcBdyK3RDdlt4c3qLfKd+v4DbgveFE\n\t\t4cziU+Lb42Pj6+Rz5PzlhOYN5pbnH+ep6DLovOlG6dDqW+rl63Dr++yG7RHt\n\t\tnO4o7rTvQO/M8Fjw5fFy8f/yjPMZ86f0NPTC9VD13vZt9vv3ivgZ+Kj5OPnH\n\t\t+lf65/t3/Af8mP0p/br+S/7c/23//4AH0ikqKyxaJGNsYXNzbmFtZVgkY2xh\n\t\tc3Nlc11OU011dGFibGVEYXRhoystLlZOU0RhdGFYTlNPYmplY3TSKSowMVxO\n\t\tU0NvbG9yU3BhY2WiMi5cTlNDb2xvclNwYWNl0ikqNDVXTlNDb2xvcqI0Ll8Q\n\t\tD05TS2V5ZWRBcmNoaXZlctE4OVRyb290gAEACAARABoAIwAtADIANwBCAEgA\n\t\tUwBaAGYAcwCBAIkAiwCNAI8AkQCTAJoArAC3AMQAygDfAOcA+wD9AP8BAQEI\n\t\tAQ0BEwEVARcBGQEeASYNcg10DXkNhA2NDZsNnw2mDa8NtA3BDcQN0Q3WDd4N\n\t\t4Q3zDfYN+wAAAAAAAAIBAAAAAAAAADoAAAAAAAAAAAAAAAAAAA39\n\t\t</data>\n\t\t<key>backgroundColorName</key>\n\t\t<string>systemBlueColor</string>\n\t\t<key>inputTypeIdentifier</key>\n\t\t<string>com.apple.Automator.fileSystemObject.image</string>\n\t\t<key>outputTypeIdentifier</key>\n\t\t<string>com.apple.Automator.nothing</string>\n\t\t<key>presentationMode</key>\n\t\t<integer>15</integer>\n\t\t<key>processesInput</key>\n\t\t<integer>0</integer>\n\t\t<key>serviceApplicationBundleID</key>\n\t\t<string>com.apple.finder</string>\n\t\t<key>serviceApplicationPath</key>\n\t\t<string>/System/Library/CoreServices/Finder.app</string>\n\t\t<key>serviceInputTypeIdentifier</key>\n\t\t<string>com.apple.Automator.fileSystemObject.image</string>\n\t\t<key>serviceOutputTypeIdentifier</key>\n\t\t<string>com.apple.Automator.nothing</string>\n\t\t<key>serviceProcessesInput</key>\n\t\t<integer>0</integer>\n\t\t<key>systemImageName</key>\n\t\t<string>NSTouchBarShare</string>\n\t\t<key>useAutomaticInputType</key>\n\t\t<integer>0</integer>\n\t\t<key>workflowTypeIdentifier</key>\n\t\t<string>com.apple.Automator.servicesMenu</string>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "public/i18n/en.yml",
    "content": "LANG_DISPLAY_LABEL: \"English\"\nABOUT: About\nOPEN_MAIN_WINDOW: Open Main Window\nCHOOSE_DEFAULT_PICBED: Choose Default Picbed\nOPEN_UPDATE_HELPER: Open Update Helper\nPRIVACY_TERMS_AGREEMENT: Privacy & Terms Agreement\nRELOAD_APP: Reload App\nUPLOAD_FAILED: Upload Failed\nUPLOAD_SUCCEED: Upload Succeed\nUPLOAD_PROGRESS: Upload Progress\nOPERATION_FAILED: Operation Failed\nOPERATION_SUCCEED: Operation Succeed\nUPLOADING: Uploading\nQUICK_UPLOAD: Quick Upload\nUPLOAD_BY_CLIPBOARD: Upload by Clipboard\nHIDE_WINDOW: Hide Window\nSPONSOR_PICGO: Sponsor PicGo\nSHOW_PICBED_QRCODE: Show Picbed Qrcode\nPICBED_QRCODE: Picbed Qrcode\nENABLE: Enable\nDISABLE: Disable\nCONFIG_THING: Config ${c}\nFIND_NEW_VERSION: Find New Version\nNO_MORE_NOTICE: No More Notice\nSHOW_DEVTOOLS: Show Devtools\nCURRENT_PICBED: Current Picbed\nOPEN_TOOLBOX: Open Toolbox\n\n# ---renderer i18n begin---\n\nCHOOSE_YOUR_DEFAULT_PICBED: \"Choose ${d} as your default picbed:\"\nUPLOAD_AREA: Upload Area\nGALLERY: Gallery\nPICBEDS_SETTINGS: Picbeds Settings\nPICGO_SETTINGS: PicGo Settings\nPLUGIN_SETTINGS: Plugins Settings\nPICGO_CLOUD_TITLE: PicGo Cloud\nPICGO_CLOUD_ERROR_TITLE: PicGo Cloud Error\nPICGO_CLOUD_NOT_LOGGED_IN: Not logged in to PicGo Cloud.\nPICGO_CLOUD_LOGIN: Log In\nPICGO_CLOUD_LOGOUT: Log Out\nPICGO_CLOUD_CANCEL_LOGIN: Cancel Login\nPICGO_CLOUD_RETRY: Retry\nPICGO_CLOUD_CONFIG_SYNC: Config Sync\nPICGO_CLOUD_LOGIN_IN_PROGRESS: Login in progress. Please finish login in your browser.\nPICGO_CLOUD_LOGGED_IN_AS: \"Logged in as: ${user}\"\nPICGO_CLOUD_OPEN: Open PicGo Cloud\nPICGO_CLOUD_LOGIN_TIMEOUT: Login timed out. Please try again.\nPICGO_CLOUD_LOGIN_FAILED: Login failed. Please try again.\nPICGO_CLOUD_AGREE_PREFIX: \"I have read and agree to PicGo's \"\nPICGO_CLOUD_TERMS_OF_SERVICE: Terms of Service\nPICGO_CLOUD_AGREE_AND: \" and \"\nPICGO_CLOUD_PRIVACY_POLICY: Privacy Policy\nPICGO_CLOUD_LOGIN_EXPIRED: Login expired. Please log in again.\nPICGO_CLOUD_ENCRYPTION_MODE_LABEL: Encryption mode\nPICGO_CLOUD_ENCRYPTION_MODE_AUTO: Auto\nPICGO_CLOUD_ENCRYPTION_MODE_SERVER: Server-side encryption\nPICGO_CLOUD_ENCRYPTION_MODE_E2E: End-to-end encryption\nPICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO: \"Auto: follow the cloud's last used encryption method (defaults to server-side).\"\nPICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER: \"Server-side encryption: your config is encrypted on the server before being stored. No PIN required.\"\nPICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E: \"End-to-end encryption: use your PIN to encrypt/decrypt data. PicGo does not store your PIN; losing it means data cannot be recovered.\"\nPICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC: Read full docs\nPICGO_CLOUD_E2E_CHECKBOX_LABEL: Enable E2E encryption\nPICGO_CLOUD_E2E_ENABLE_WARNING_TITLE: Enable E2E Encryption\nPICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE: \"E2E encryption requires you to keep your PIN safe. If you lose your PIN, the encrypted data cannot be recovered. PicGo does not store your PIN or any recovery data.\"\nPICGO_CLOUD_REMOTE_E2E_AUTO_ENABLED: Remote config is E2E-encrypted. E2E has been enabled locally.\nPICGO_CLOUD_E2E_PIN_SETUP_TITLE: Set up E2E PIN\nPICGO_CLOUD_E2E_PIN_DECRYPT_TITLE: Enter E2E PIN\nPICGO_CLOUD_E2E_PIN_RETRY_TITLE: \"Incorrect PIN. Please try again (attempt ${retryCount}).\"\nPICGO_CLOUD_E2E_PIN_PLACEHOLDER: PIN\nPICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER: Confirm PIN\nPICGO_CLOUD_CONFIG_SYNC_SUCCESS: Config sync succeeded.\nPICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED: Conflict detected. Please resolve the conflicts.\nPICGO_CLOUD_CONFIG_SYNC_FAILED: Config sync failed.\nPICGO_CLOUD_CONFIG_SYNC_ABORTED: Config sync cancelled.\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE: Confirm switch encryption method?\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY: \"You are switching from \\\"${from}\\\" to \\\"${to}\\\".\\n\\nNote: Switching encryption modes will clear all your cloud history versions.\\nThis is because older history versions cannot be decrypted or verified under the new mode.\\nAfter switching, the system will immediately create a new backup as the starting point.\"\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM: Confirm switch and clear history\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL: Cancel\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED: Encryption switch cancelled by user\nPICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON: \"Config sync failed: ${reason}\"\nPICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY: Too many incorrect PIN attempts. Please try again.\nPICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID: Local config is invalid. Please check your config file.\nPICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS: Config sync is in progress.\nPICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING: There is a pending conflict session. Please resolve conflicts first.\nPICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION: No conflict session found.\nPICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE: Please choose Local or Cloud for all conflict items.\nPICGO_CLOUD_CONFIG_SYNC_STARTING: Config sync started...\nPICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE: \"Conflict Detected (${count})\"\nPICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL: Choose All Local\nPICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD: Choose All Cloud\nPICGO_CLOUD_CONFIG_SYNC_RESET_ALL: Reset All\nPICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION: Local Version\nPICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION: Cloud Version\nPICGO_CLOUD_CONFIG_SYNC_ABORT: Abort\nPICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC: Confirm & Sync\nPICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED: (empty)\nPICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED: Conflicts resolved.\nPICGO_CLOUD_LAST_SYNC_TIME: \"Last local sync time: ${time}\"\nPICGO_CLOUD_LAST_SYNC_TIME_NONE: None\nPICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE: Restart Required?\nPICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE: Some settings may require a restart to take effect. Restart now?\nPICGO_CLOUD_CONFIG_SYNC_RESTART_NOW: Restart Now\nPICGO_CLOUD_CONFIG_SYNC_RESTART_LATER: Later\nINPUT_BOX_CONFIRM_MISMATCH: The two inputs do not match.\nPICGO_SPONSOR_TEXT: PicGo is a free software, if you like it, please don't forget to buy me a cup of coffee.\nALIPAY: Alipay\nWECHATPAY: Wechat Pay\nCHOOSE_PICBED: Choose Picbed\nCOPY_PICBED_CONFIG: Copy Picbed Config\nCOPY_PICBED_CONFIG_SUCCEED: Copy Picbed Config Succeed\nINPUT: Input\nCANCEL: Cancel\nCONFIRM: Confirm\nCHOOSE_SHOWED_PICBED: Choose Showed Picbed\nCHOOSE_PASTE_FORMAT: Choose Paste Format\nSEARCH: Search\nCOPY: Copy\nDELETE: Delete\nSELECT_ALL: Select All\nCHANGE_IMAGE_URL: Change Image URL\nCHANGE_IMAGE_URL_SUCCEED: Change Image URL Succeed\nCOPY_LINK_SUCCEED: Copy Link Succeed\nBATCH_COPY_LINK_SUCCEED: Batch Copy Link Succeed\nFILE_RENAME: File Rename\nCOPY_FILE_PATH: Copy file path\nOPEN_FILE_PATH: Open file path\nSUCCESS: Success\nFAILED: Failed\n\n# settings\n\nSETTINGS: Settings\nSETTINGS_OPEN_CONFIG_FILE: Open Config File\nSETTINGS_CLICK_TO_OPEN: Click to Open\nSETTINGS_SET_LOG_FILE: Set Log File\nSETTINGS_CLICK_TO_SET: Click to Set\nSETTINGS_CLICK_TO_CHECK: Click to Check\nSETTINGS_SET_SHORTCUT: Set Shortcut\nSETTINGS_URL_REWRITE: URL Rewrite\nSETTINGS_CUSTOM_LINK_FORMAT: Custom Link Format\nSETTINGS_SET_PROXY_AND_MIRROR: Set Proxy and Mirror\nSETTINGS_SET_SERVER: Set Server\nSETTINGS_CHECK_UPDATE: Check Update\nSETTINGS_OPEN_UPDATE_HELPER: Open Update Helper\nSETTINGS_OPEN: Open\nSETTINGS_CLOSE: Close\nSETTINGS_ACCEPT_BETA_UPDATE: Accept Beta Update\nSETTINGS_LAUNCH_ON_BOOT: Launch On Boot\nSETTINGS_RENAME_BEFORE_UPLOAD: Rename Before Upload\nSETTINGS_TIMESTAMP_RENAME: Timestamp Rename\nSETTINGS_OPEN_UPLOAD_TIPS: Open Upload Tips\nSETTINGS_NOTIFICATION_SOUND: Play Notification Sound\nSETTINGS_MINI_WINDOW_ON_TOP: Mini Window On Top\nSETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: Auto Copy URL After Upload\nSETTINGS_TIPS_PLACEHOLDER_URL: Use $url to represent url position\nSETTINGS_TIPS_PLACEHOLDER_FILENAME: Use $fileName to represent file name position\nSETTINGS_TIPS_PLACEHOLDER_EXTNAME: Use $extName to represent file's ext position\nSETTINGS_TIPS_SUCH_AS: \"Such as: $url/$fileName\"\nSETTINGS_UPLOAD_PROXY: Upload Proxy\nSETTINGS_PLUGIN_INSTALL_PROXY: Proxy for Plugin Install\nSETTINGS_PLUGIN_INSTALL_MIRROR: Mirror for Plugin Install\nSETTINGS_CURRENT_VERSION: Current Version\nSETTINGS_NEWEST_VERSION: Newest Version\nSETTINGS_GETING: Getting...\nSETTINGS_TIPS_HAS_NEW_VERSION: PicGo has a new version, please click confirm to open download page\nSETTINGS_LOG_FILE: Log File\nSETTINGS_LOG_LEVEL: Log Level\nSETTINGS_LOG_FILE_SIZE: Log File Size\nSETTINGS_SET_PICGO_SERVER: Set PicGo Server\nSETTINGS_TIPS_SERVER_NOTICE: If you don't know what is the server's function, please read the document, or don't modify the configuration.\nSETTINGS_ENABLE_SERVER: Enable Server\nSETTINGS_SET_SERVER_HOST: Set Server Host\nSETTINGS_SET_SERVER_PORT: Set Server Port\nSETTINGS_TIP_PLACEHOLDER_HOST: Default:127.0.0.1\nSETTINGS_TIP_PLACEHOLDER_PORT: Default:36677\nSETTINGS_LOG_LEVEL_ALL: All\nSETTINGS_LOG_LEVEL_SUCCESS: Success\nSETTINGS_LOG_LEVEL_ERROR: Error\nSETTINGS_LOG_LEVEL_INFO: Info\nSETTINGS_LOG_LEVEL_WARN: Warn\nSETTINGS_LOG_LEVEL_NONE: None\nSETTINGS_RESULT: Result\nSETTINGS_DEFAULT_PICBED: Default Picbed\nSETTINGS_SET_DEFAULT_PICBED: Set Default Picbed\nSETTINGS_NOT_CONFIG_OPTIONS: Not Config Options\nSETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: Use Builtin Clipboard to Upload\nSETTINGS_CHOOSE_LANGUAGE: Choose Language\nUPLOADER_CONFIG_NAME: Configuration Name\nBUILTIN_CLIPBOARD_TIPS: Use builtin clipboard function to upload instead of using scripts\n\n# url rewrite\n\nURL_REWRITE_HELP: Rewrites uploaded image URLs. Rules are evaluated in order; the first matched rule wins.\nURL_REWRITE_ADD_RULE: Add Rule\nURL_REWRITE_EDIT_RULE: Edit Rule\nURL_REWRITE_EMPTY: No rules\nURL_REWRITE_ORDER: Order\nURL_REWRITE_MATCH: Match\nURL_REWRITE_REPLACE: Replace\nURL_REWRITE_FLAGS: Flags\nURL_REWRITE_ENABLED: Enabled\nURL_REWRITE_ACTIONS: Actions\nURL_REWRITE_MOVE_UP: Up\nURL_REWRITE_MOVE_DOWN: Down\nURL_REWRITE_EDIT: Edit\nURL_REWRITE_DELETE: Delete\nURL_REWRITE_DELETE_CONFIRM: Delete this rule?\nURL_REWRITE_MATCH_TIPS: Supports regex (JavaScript RegExp)\nURL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path\nURL_REWRITE_REPLACE_TIPS: Replacement string (supports $1, $2, ...)\nURL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath\nURL_REWRITE_OPTIONS: Options\nURL_REWRITE_RULE_ENABLED: Enable this rule\nURL_REWRITE_FLAG_GLOBAL_LABEL: Global (g)\nURL_REWRITE_FLAG_GLOBAL_DESC: Replace all occurrences, not just the first one\nURL_REWRITE_FLAG_IGNORE_CASE_LABEL: Ignore case (i)\nURL_REWRITE_FLAG_IGNORE_CASE_DESC: Case-insensitive matching (e.g. JPG equals jpg)\nURL_REWRITE_MATCH_REQUIRED: Match is required\nURL_REWRITE_REPLACE_REQUIRED: Replace is required\nURL_REWRITE_INVALID_REGEX: Invalid regex\nURL_REWRITE_PREVIEW_TITLE: Preview\nURL_REWRITE_PREVIEW_TIPS: Enter a URL to see how the current rules rewrite it (matched in order; only the first match is applied)\nURL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png\nURL_REWRITE_PREVIEW_RUN: Preview\nURL_REWRITE_PREVIEW_OUTPUT: Output URL\nURL_REWRITE_PREVIEW_INPUT_REQUIRED: Please enter a URL to preview\nURL_REWRITE_PREVIEW_RULE_INVALID: Invalid rule\nURL_REWRITE_PREVIEW_MATCHED_RULE: Matched rule\nURL_REWRITE_PREVIEW_NO_MATCH: No rules matched\nUPLOADER_CONFIG_PLACEHOLDER: Please Enter Configuration Name\nSELECTED_SETTING_HINT: Selected\nSETTINGS_ENCODE_OUTPUT_URL: Encode Output(or Copyed) URL\nSETTINGS_SHOW_DOCK_ICON: Show Dock icon\nSETTINGS_SHOW_MENUBAR_ICON: Show Menubar icon\nSETTINGS_SHOW_MENUBAR_ICON_TIPS: If both \"Show Dock icon\" and \"Show Menubar icon\" are turned off, you won't be able to find PicGo's main window. Edit the config file and set showDockIcon or showMenubarIcon to true to recover.\nSETTINGS_STARTUP_MODE: Startup Mode\nSETTINGS_STARTUP_MODE_MAIN_WINDOW: Open Main Window\nSETTINGS_STARTUP_MODE_MINI_WINDOW: Open Mini Window\nSETTINGS_STARTUP_MODE_HIDE: Silent Startup\n\n# shortcut-page\n\nSHORTCUT_NAME: Shortcut Name\nSHORTCUT_BIND: Shortcut Binding\nSHORTCUT_STATUS: Status\nSHORTCUT_ENABLED: Enabled\nSHORTCUT_DISABLED: Disabled\nSHORTCUT_SOURCE: Source\nSHORTCUT_HANDLE: Handle\nSHORTCUT_ENABLE: Enable\nSHORTCUT_DISABLE: Disable\nSHORTCUT_EDIT: Edit\nSHORTCUT_CHANGE_UPLOAD: Change Upload Shortcut\n\n# gallery-page\nGALLERY_URL_REWRITE_TITLE: Rewrite Selected Image URLs\nGALLERY_URL_REWRITE_RESULT_TITLE: Rewrite Image URL Result\nGALLERY_URL_REWRITE_WARN_NO_SELECTION: You must select at least one picture first\n\nGALLERY_URL_REWRITE_APPLY_GLOBAL_RULES: Apply global URL rewrite rules\nGALLERY_URL_REWRITE_GLOBAL_RULES_COUNT: Global rules\nGALLERY_URL_REWRITE_TEMP_RULE_TIPS: Optional. Leave empty to skip the temporary rule. Temporary rule has higher priority than global rules.\nGALLERY_URL_REWRITE_TEMP_RULE_REQUIRED: Match and Replace are required for the temporary rule\nGALLERY_URL_REWRITE_NO_RULES_TO_APPLY: No global rules enabled and no temporary rule provided\nGALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: Save the temporary rule to global URL rewrite rules?\nGALLERY_URL_REWRITE_APPLY_AND_SAVE: Apply and Save\nGALLERY_URL_REWRITE_APPLY_ONLY: Apply Only\nGALLERY_URL_REWRITE_NO_CHANGES: No URLs were changed\nGALLERY_URL_REWRITE_EMPTY_RESULT_WARN: The rewrite result is empty; skipped\n\n# tray-page\n\nWAIT_TO_UPLOAD: Wait to Upload\nALREADY_UPLOAD: Already Upload\n\n# upload-page\n\nPICTURE_UPLOAD: Picture Upload\nDRAG_FILE_TO_HERE: Drag file to here, or\nCLICK_TO_UPLOAD: click to upload\nLINK_FORMAT: Link Format\nCUSTOM: Custom\nCLIPBOARD_PICTURE: Clipboard\nTIPS_DRAG_VALID_PICTURE_OR_URL: Drag valid picture or url to here\nTIPS_INPUT_URL: Input URL\nTIPS_HTTP_PREFIX: Starts with http:// or https://. Multiple URLs supported (one per line)\nTIPS_INPUT_VALID_URL: Input valid URL\n\n# plugins\n\nPLUGIN_SEARCH_PLACEHOLDER: Search picgo plugins on npm, or click the button to view the awesome plugins list\nPLUGIN_INSTALL: Install\nPLUGIN_INSTALLING: Installing...\nPLUGIN_INSTALLED: Installed\nPLUGIN_DOING_SOMETHING: Doing...\nPLUGIN_LIST: Plugin List\nPLUGIN_IMPORT_LOCAL: Import Local Plugins\n\n# tips\n\nTIPS_REMOVE_LINK: This operation will remove the picture from the album, continue?\nTIPS_WILL_REMOVE_CHOOSED_IMAGES: This operation will remove the picture from the album, continue?\nTIPS_MUST_CONTAINS_URL: Must contains $url or $fileName or $extName\nTIPS_NETWORK_ERROR: Network Error\nTIPS_NEED_RELOAD: Need Reload App\nTIPS_PLEASE_CHOOSE_LOG_LEVEL: Please choose log level\nTIPS_SET_SUCCEED: Set successfully\nTIPS_PLUGIN_NOT_GUI_IMPLEMENT: This plugin is not optimized for the GUI, continue?\nTIPS_CLICK_NOTIFICATION_TO_RELOAD: Click notification to reload app\nTIPS_GET_PLUGIN_LIST_FAILED: Get plugin list failed\n\n# ---renderer i18n end---\n\n# plugins\nPLUGIN_INSTALL_SUCCEED: Plugin install succeed\nPLUGIN_INSTALL_FAILED: Plugin install failed\nPLUGIN_UNINSTALL_SUCCEED: Plugin uninstall succeed\nPLUGIN_UNINSTALL_FAILED: Plugin uninstall failed\nPLUGIN_UPDATE_SUCCEED: Plugin update succeed\nPLUGIN_UPDATE_FAILED: Plugin update failed\nPLUGIN_IMPORT_SUCCEED: Plugin import succeed\nPLUGIN_IMPORT_FAILED: Plugin import failed\nENABLE_PLUGIN: Enable Plugin\nDISABLE_PLUGIN: Disable Plugin\nUNINSTALL_PLUGIN: Uninstall Plugin\nUPDATE_PLUGIN: Update Plugin\n\n# toolbox\nTOOLBOX: Toolbox\nTOOLBOX_TITLE: Troubleshoot PicGo runtime issues\nTOOLBOX_SUB_TITLE: Scan the following items immediately to fix usage issues\nTOOLBOX_CHECK_CONFIG_FILE_BROKEN: Check if the configuration file is damaged\nTOOLBOX_CHECK_GALLERY_FILE_BROKEN: Check if the album file is damaged\nTOOLBOX_CHECK_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: Check if there is a problem with clipboard picture upload\nTOOLBOX_CHECK_PROBLEM_WITH_PROXY: Check if the proxy settings are normal\nTOOLBOX_FIX_DONE_NEED_RELOAD: Repair completed, need to restart to take effect, restart or not\nTOOLBOX_CANT_AUTO_FIX: Unable to automatically repair, please repair the following problems yourself\nTOOLBOX_START_SCAN: Start scanning\nTOOLBOX_RE_SCAN: Re scanning\nTOOLBOX_START_FIX: Start fixing\nTOOLBOX_SUCCESS_TIPS: Congratulations, no problems were found\nTOOLBOX_CHECK_CONFIG_FILE_PATH_TIPS: \"The configuration file path is: ${path}\"\nTOOLBOX_CHECK_CONFIG_FILE_BROKEN_TIPS: The configuration file is damaged\nTOOLBOX_CHECK_GALLERY_FILE_PATH_TIPS: \"The album file path is: ${path}\"\nTOOLBOX_CHECK_GALLERY_FILE_BROKEN_TIPS: The album file is damaged\nTOOLBOX_CHECK_PROXY_SUCCESS_TIPS: Proxy settings normal\nTOOLBOX_CHECK_PROXY_NO_PROXY_TIPS: No proxy settings\nTOOLBOX_CHECK_PROXY_PROXY_IS_NOT_CORRECT: Proxy settings incorrect\nTOOLBOX_CHECK_PROXY_PROXY_IS_NOT_WORKING: Proxy settings unavailable\nTOOLBOX_CHECK_CLIPBOARD_FILE_PATH_TIPS: \"The temporary folder path for clipboard pictures is: ${path}\"\nTOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS: \"The temporary folder for clipboard pictures does not exist: ${path}\"\nTOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS: \"Please create the folder yourself: ${path}\"\n\n# tips\nTIPS_NOTICE: Tips\nTIPS_WARNING: Warning\nTIPS_ERROR: Error\nTIPS_SKIPPED_INVALID_URLS: Skipped ${n} invalid URL(s), see logs for details\nTIPS_TOO_MANY_URLS_CONFIRM: You are about to upload ${n} URLs at once. This may cause lag. It's recommended to upload in batches. Continue?\nTIPS_NO_VALID_URLS: No valid URL found\nTIPS_INSTALL_NODE_AND_RELOAD_PICGO: Please install Node.js and restart PicGo to continue\nTIPS_PLUGIN_REMOVE_GALLERY_ITEM: Plugin is trying to remove some images from the album gallery, continue?\nTIPS_PLUGIN_OVERWRITE_GALLERY: Plugin is trying to overwrite the album gallery, continue?\nTIPS_UPLOAD_NOT_PICTURES: The latest clipboard item is not a picture\nTIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicGo config file broken, has been restored to default\nTIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicGo config file broken, has been restored to backup\nTIPS_PICGO_BACKUP_FILE_VERSION: \"Backup file version: ${v}\"\nTIPS_CUSTOM_CONFIG_FILE_PATH_ERROR: Custom config file parse error, please check the path content\nTIPS_SHORTCUT_MODIFIED_SUCCEED: Shortcut modified successfully\nTIPS_SHORTCUT_MODIFIED_CONFLICT: Shortcut conflict, please reset\nTIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED: Custom link style modified successfully\nTIPS_FIND_NEW_VERSION: Find new version ${v}，update many new features, do you want to download the latest version?\nTIPS_DELETE_UPLOADER_CONFIG: Are you sure you want to delete this config?\nTIPS_COPY_UPLOADER_CONFIG: Are you sure you want to copy this config?\nTIPS_UPLOADER_CONFIG_NAME_EMPTY: Config name cannot be empty\nTIPS_UPLOADER_CONFIG_NOT_FOUND: Config not found\nTIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST: Cannot delete the last config\n\n# privacy\nPRIVACY: \"Please read and agree to the Privacy Policy ${privacyUrl} and Terms of Service ${termsUrl} before using.\"\nPRIVACY_TIPS: Please agree the privacy policy to upload\nQUIT: Quit\n"
  },
  {
    "path": "public/i18n/zh-CN.yml",
    "content": "LANG_DISPLAY_LABEL: 中文\nABOUT: 关于\nOPEN_MAIN_WINDOW: 打开主窗口\nCHOOSE_DEFAULT_PICBED: 选择默认图床\nOPEN_UPDATE_HELPER: 打开更新助手\nPRIVACY_TERMS_AGREEMENT: 隐私与条款协议\nRELOAD_APP: 重启应用\nUPLOAD_SUCCEED: 上传成功\nUPLOAD_FAILED: 上传失败\nUPLOAD_PROGRESS: 上传进度\nOPERATION_SUCCEED: 操作成功\nOPERATION_FAILED: 操作失败\nUPLOADING: 正在上传\nQUICK_UPLOAD: 快捷上传\nUPLOAD_BY_CLIPBOARD: 剪贴板图片上传\nHIDE_WINDOW: 隐藏窗口\nSPONSOR_PICGO: 赞助 PicGo\nSHOW_PICBED_QRCODE: 生成图床配置二维码\nPICBED_QRCODE: 图床配置二维码\nENABLE: 启用\nDISABLE: 禁用\nCONFIG_THING: 配置${c}\nFIND_NEW_VERSION: 发现新版本\nNO_MORE_NOTICE: 以后不再提醒\nSHOW_DEVTOOLS: 打开开发者工具\nCURRENT_PICBED: 当前图床\nOPEN_TOOLBOX: 打开修复工具箱\n\n# ---renderer i18n begin---\n\nCHOOSE_YOUR_DEFAULT_PICBED: 选择 ${d} 作为你默认图床：\nUPLOAD_AREA: 上传区\nGALLERY: 相册\nPICBEDS_SETTINGS: 图床设置\nPICGO_SETTINGS: PicGo设置\nPLUGIN_SETTINGS: 插件设置\nPICGO_CLOUD_TITLE: PicGo Cloud\nPICGO_CLOUD_ERROR_TITLE: PicGo Cloud 错误\nPICGO_CLOUD_NOT_LOGGED_IN: 尚未登录 PicGo Cloud\nPICGO_CLOUD_LOGIN: 登录\nPICGO_CLOUD_LOGOUT: 退出登录\nPICGO_CLOUD_CANCEL_LOGIN: 取消登录\nPICGO_CLOUD_RETRY: 重试\nPICGO_CLOUD_CONFIG_SYNC: 配置同步\nPICGO_CLOUD_LOGIN_IN_PROGRESS: 登录进行中，请在浏览器完成登录。\nPICGO_CLOUD_LOGGED_IN_AS: \"已登录：${user}\"\nPICGO_CLOUD_OPEN: 打开 PicGo Cloud\nPICGO_CLOUD_LOGIN_TIMEOUT: 登录超时，请重试。\nPICGO_CLOUD_LOGIN_FAILED: 登录失败，请重试。\nPICGO_CLOUD_AGREE_PREFIX: 我已阅读并同意 PicGo 的\nPICGO_CLOUD_TERMS_OF_SERVICE: 服务条款\nPICGO_CLOUD_AGREE_AND: 以及\nPICGO_CLOUD_PRIVACY_POLICY: 隐私政策\nPICGO_CLOUD_LOGIN_EXPIRED: 登录失效，请重新登录。\nPICGO_CLOUD_ENCRYPTION_MODE_LABEL: 加密模式\nPICGO_CLOUD_ENCRYPTION_MODE_AUTO: 自动\nPICGO_CLOUD_ENCRYPTION_MODE_SERVER: 服务端加密\nPICGO_CLOUD_ENCRYPTION_MODE_E2E: 端到端加密\nPICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO: 自动：跟随云端上一次配置的加密方式（默认服务端加密）。\nPICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER: 服务端加密：我们会在服务端对你的配置加密后再存储，不需要 PIN。\nPICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E: 端到端加密：需要你输入 PIN 来加密/解密数据。PicGo 不存储 PIN，丢失将无法找回数据。\nPICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC: 查看完整文档\nPICGO_CLOUD_E2E_CHECKBOX_LABEL: 开启 E2E 加密\nPICGO_CLOUD_E2E_ENABLE_WARNING_TITLE: 开启 E2E 加密\nPICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE: E2E 加密需要你自行妥善保管 PIN。PIN 丢失将无法恢复加密数据。PicGo 不会存储 PIN 或任何恢复信息。\nPICGO_CLOUD_REMOTE_E2E_AUTO_ENABLED: 检测到远端配置已开启 E2E 加密，已自动在本地开启 E2E。\nPICGO_CLOUD_E2E_PIN_SETUP_TITLE: 设置 E2E PIN\nPICGO_CLOUD_E2E_PIN_DECRYPT_TITLE: 输入 E2E PIN\nPICGO_CLOUD_E2E_PIN_RETRY_TITLE: \"PIN 错误，请重试（第 ${retryCount} 次）。\"\nPICGO_CLOUD_E2E_PIN_PLACEHOLDER: PIN\nPICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER: 确认 PIN\nPICGO_CLOUD_CONFIG_SYNC_SUCCESS: 配置同步成功。\nPICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED: 检测到配置冲突，请选择解决方案。\nPICGO_CLOUD_CONFIG_SYNC_FAILED: 配置同步失败。\nPICGO_CLOUD_CONFIG_SYNC_ABORTED: 已取消配置同步。\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE: 确认切换加密方式吗？\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY: \"您正在从“${from}”切换为“${to}”。\\n\\n注意：切换加密模式将清空您所有的云端历史版本记录。\\n这是因为旧的历史版本无法在新模式下被解密或验证。\\n切换后，系统将立即为您创建一份新的备份作为起点。\"\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM: 确认切换并清空历史\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL: 取消\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED: 已取消切换加密方式\nPICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON: \"配置同步失败：${reason}\"\nPICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY: PIN 错误次数过多，请稍后重试。\nPICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID: 本地配置文件格式错误，请检查配置文件。\nPICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS: 配置同步进行中…\nPICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING: 存在未解决的冲突，请先处理。\nPICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION: 未找到可处理的冲突会话。\nPICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE: 请为所有冲突项选择本地或云端。\nPICGO_CLOUD_CONFIG_SYNC_STARTING: 配置同步已开始…\nPICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE: \"冲突处理（${count} 项）\"\nPICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL: 全选本地\nPICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD: 全选云端\nPICGO_CLOUD_CONFIG_SYNC_RESET_ALL: 重置全部\nPICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION: 本地版本\nPICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION: 云端版本\nPICGO_CLOUD_CONFIG_SYNC_ABORT: 放弃同步\nPICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC: 确认并同步\nPICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED: （空）\nPICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED: 配置冲突已解决。\nPICGO_CLOUD_LAST_SYNC_TIME: \"上次本地同步时间：${time}\"\nPICGO_CLOUD_LAST_SYNC_TIME_NONE: 无\nPICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE: 需要重启吗？\nPICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE: 部分配置可能需要重启后生效，是否立即重启？\nPICGO_CLOUD_CONFIG_SYNC_RESTART_NOW: 立即重启\nPICGO_CLOUD_CONFIG_SYNC_RESTART_LATER: 稍后\nINPUT_BOX_CONFIRM_MISMATCH: 两次输入不一致，请重新输入。\nPICGO_SPONSOR_TEXT: PicGo是免费开源的软件，如果你喜欢它，对你有帮助，不妨请我喝杯咖啡？\nALIPAY: 支付宝\nWECHATPAY: 微信支付\nCHOOSE_PICBED: 选择图床\nCOPY_PICBED_CONFIG: 复制图床配置\nCOPY_PICBED_CONFIG_SUCCEED: 复制图床配置成功\nINPUT: 输入框\nCANCEL: 取消\nCONFIRM: 确定\nCHOOSE_SHOWED_PICBED: 请选择显示的图床\nCHOOSE_PASTE_FORMAT: 请选择粘贴的格式\nSEARCH: 搜索\nCOPY: 复制\nDELETE: 删除\nSELECT_ALL: 全选\nCHANGE_IMAGE_URL: 修改图片URL\nCHANGE_IMAGE_URL_SUCCEED: 修改图片URL成功\nCOPY_LINK_SUCCEED: 复制链接成功\nBATCH_COPY_LINK_SUCCEED: 批量复制链接成功\nFILE_RENAME: 文件改名\nCOPY_FILE_PATH: 复制文件路径\nOPEN_FILE_PATH: 打开文件路径\nSUCCESS: 成功\nFAILED: 失败\n\n# settings\n\nSETTINGS: 设置\nSETTINGS_OPEN_CONFIG_FILE: 打开配置文件\nSETTINGS_CLICK_TO_OPEN: 点击打开\nSETTINGS_SET_LOG_FILE: 设置日志文件\nSETTINGS_CLICK_TO_SET: 点击设置\nSETTINGS_CLICK_TO_CHECK: 点击检查\nSETTINGS_SET_SHORTCUT: 设置快捷键\nSETTINGS_URL_REWRITE: URL 重写\nSETTINGS_CUSTOM_LINK_FORMAT: 自定义链接格式\nSETTINGS_SET_PROXY_AND_MIRROR: 设置代理和镜像地址\nSETTINGS_SET_SERVER: 设置Server\nSETTINGS_CHECK_UPDATE: 检查更新\nSETTINGS_OPEN_UPDATE_HELPER: 打开更新助手\nSETTINGS_OPEN: 开\nSETTINGS_CLOSE: 关\nSETTINGS_ACCEPT_BETA_UPDATE: 接受Beta版本更新\nSETTINGS_LAUNCH_ON_BOOT: 开机自启\nSETTINGS_RENAME_BEFORE_UPLOAD: 上传前重命名\nSETTINGS_TIMESTAMP_RENAME: 时间戳重命名\nSETTINGS_OPEN_UPLOAD_TIPS: 开启上传提示\nSETTINGS_NOTIFICATION_SOUND: 开启通知提示音\nSETTINGS_MINI_WINDOW_ON_TOP: Mini窗口置顶\nSETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: 上传后自动复制URL\nSETTINGS_TIPS_PLACEHOLDER_URL: 用占位符 $url 来表示url的位置\nSETTINGS_TIPS_PLACEHOLDER_FILENAME: 用占位符 $fileName 来表示文件名的位置\nSETTINGS_TIPS_PLACEHOLDER_EXTNAME: 用占位符 $extName 来表示文件格式的位置\nSETTINGS_TIPS_SUCH_AS: 如\nSETTINGS_UPLOAD_PROXY: 上传代理\nSETTINGS_PLUGIN_INSTALL_PROXY: 插件安装代理\nSETTINGS_PLUGIN_INSTALL_MIRROR: 插件安装镜像\nSETTINGS_CURRENT_VERSION: 当前版本\nSETTINGS_NEWEST_VERSION: 最新版本\nSETTINGS_GETING: 正在获取中\nSETTINGS_TIPS_HAS_NEW_VERSION: PicGo更新啦，请点击确定打开下载页面\nSETTINGS_LOG_FILE: 日志文件\nSETTINGS_LOG_LEVEL: 日志记录等级\nSETTINGS_LOG_FILE_SIZE: 日志文件大小\nSETTINGS_SET_PICGO_SERVER: 设置PicGo-Server\nSETTINGS_TIPS_SERVER_NOTICE: 如果你不知道Server的作用，请阅读文档，或者不用修改配置。\nSETTINGS_ENABLE_SERVER: 是否开启Server\nSETTINGS_SET_SERVER_HOST: 设置监听地址\nSETTINGS_SET_SERVER_PORT: 设置监听端口\nSETTINGS_TIP_PLACEHOLDER_HOST: 推荐默认地址:127.0.0.1\nSETTINGS_TIP_PLACEHOLDER_PORT: 推荐默认端口:36677\nSETTINGS_LOG_LEVEL_ALL: 全部-All\nSETTINGS_LOG_LEVEL_SUCCESS: 成功-Success\nSETTINGS_LOG_LEVEL_ERROR: 错误-Error\nSETTINGS_LOG_LEVEL_INFO: 普通-Info\nSETTINGS_LOG_LEVEL_WARN: 提醒-Warn\nSETTINGS_LOG_LEVEL_NONE: 不记录日志-None\nSETTINGS_RESULT: 设置结果\nSETTINGS_DEFAULT_PICBED: 设置默认图床\nSETTINGS_SET_DEFAULT_PICBED: 设为默认图床\nSETTINGS_NOT_CONFIG_OPTIONS: 暂无配置项\nSETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: 使用内置剪贴板上传\nSETTINGS_CHOOSE_LANGUAGE: 选择语言\nBUILTIN_CLIPBOARD_TIPS: 使用内置剪贴板函数而不是调用脚本获取剪贴板图片\nUPLOADER_CONFIG_NAME: 图床配置名\n\n# url rewrite\n\nURL_REWRITE_HELP: 用于重写上传后的图片 URL。规则按顺序匹配，命中的第一条匹配生效。\nURL_REWRITE_ADD_RULE: 新增规则\nURL_REWRITE_EDIT_RULE: 编辑规则\nURL_REWRITE_EMPTY: 暂无规则\nURL_REWRITE_ORDER: 顺序\nURL_REWRITE_MATCH: 匹配\nURL_REWRITE_REPLACE: 替换\nURL_REWRITE_FLAGS: 标志\nURL_REWRITE_ENABLED: 启用\nURL_REWRITE_ACTIONS: 操作\nURL_REWRITE_MOVE_UP: 上移\nURL_REWRITE_MOVE_DOWN: 下移\nURL_REWRITE_EDIT: 编辑\nURL_REWRITE_DELETE: 删除\nURL_REWRITE_DELETE_CONFIRM: 确定删除该规则？\nURL_REWRITE_MATCH_TIPS: 支持正则（JavaScript RegExp）\nURL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path\nURL_REWRITE_REPLACE_TIPS: 替换内容（支持 $1、$2...）\nURL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath\nURL_REWRITE_OPTIONS: 选项\nURL_REWRITE_RULE_ENABLED: 启用该规则\nURL_REWRITE_FLAG_GLOBAL_LABEL: 全局（g）\nURL_REWRITE_FLAG_GLOBAL_DESC: 替换所有找到的内容，而不仅仅是第一个\nURL_REWRITE_FLAG_IGNORE_CASE_LABEL: 忽略大小写（i）\nURL_REWRITE_FLAG_IGNORE_CASE_DESC: 匹配时不区分字母大小写（例如 JPG 等同于 jpg）\nURL_REWRITE_MATCH_REQUIRED: 匹配规则不能为空\nURL_REWRITE_REPLACE_REQUIRED: 替换内容不能为空\nURL_REWRITE_INVALID_REGEX: 正则表达式不合法\nURL_REWRITE_PREVIEW_TITLE: 预览\nURL_REWRITE_PREVIEW_TIPS: 输入一个 URL，查看当前规则的重写结果（按顺序匹配，仅第一条命中的规则生效）\nURL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png\nURL_REWRITE_PREVIEW_RUN: 预览\nURL_REWRITE_PREVIEW_OUTPUT: 输出 URL\nURL_REWRITE_PREVIEW_INPUT_REQUIRED: 请输入要预览的 URL\nURL_REWRITE_PREVIEW_RULE_INVALID: 规则无效\nURL_REWRITE_PREVIEW_MATCHED_RULE: 命中规则\nURL_REWRITE_PREVIEW_NO_MATCH: 没有规则匹配\nUPLOADER_CONFIG_PLACEHOLDER: 请输入配置名称\nSELECTED_SETTING_HINT: 已选中\nSETTINGS_ENCODE_OUTPUT_URL: 输出（复制） URL 时进行转义\nSETTINGS_SHOW_DOCK_ICON: 显示 Dock 栏图标\nSETTINGS_SHOW_MENUBAR_ICON: 显示顶部栏图标\nSETTINGS_SHOW_MENUBAR_ICON_TIPS: 若“显示 Dock 栏图标”和“显示顶部栏图标”都关闭，将会无法找到 PicGo 主界面。需要手动修改配置文件里的 showDockIcon 或 showMenubarIcon 为 true 才能恢复。\nSETTINGS_STARTUP_MODE: 启动模式\nSETTINGS_STARTUP_MODE_MAIN_WINDOW: 打开主窗口\nSETTINGS_STARTUP_MODE_MINI_WINDOW: 打开 Mini 窗口\nSETTINGS_STARTUP_MODE_HIDE: 静默启动\n\n# shortcut-page\n\nSHORTCUT_NAME: 快捷键名称\nSHORTCUT_BIND: 快捷键绑定\nSHORTCUT_STATUS: 状态\nSHORTCUT_ENABLED: 已启用\nSHORTCUT_DISABLED: 已禁用\nSHORTCUT_SOURCE: 来源\nSHORTCUT_HANDLE: 操作\nSHORTCUT_ENABLE: 启用\nSHORTCUT_DISABLE: 禁用\nSHORTCUT_EDIT: 编辑\nSHORTCUT_CHANGE_UPLOAD: 修改上传快捷键\n\n# gallery-page\nGALLERY_URL_REWRITE_TITLE: 重写选中图片 URL\nGALLERY_URL_REWRITE_RESULT_TITLE: 重写图片 URL 结果\nGALLERY_URL_REWRITE_WARN_NO_SELECTION: 你必须先选中至少一张图片\n\nGALLERY_URL_REWRITE_APPLY_GLOBAL_RULES: 应用全局 URL 重写规则\nGALLERY_URL_REWRITE_GLOBAL_RULES_COUNT: 全局规则数量\nGALLERY_URL_REWRITE_TEMP_RULE_TIPS: 可选，留空则不使用临时规则（临时规则优先级高于全局规则）。\nGALLERY_URL_REWRITE_TEMP_RULE_REQUIRED: 临时规则需要同时填写「匹配」和「替换」\nGALLERY_URL_REWRITE_NO_RULES_TO_APPLY: 没有可用的全局规则，且未填写临时规则\nGALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: 是否将临时规则写入全局 URL 重写规则列表？\nGALLERY_URL_REWRITE_APPLY_AND_SAVE: 应用并写入\nGALLERY_URL_REWRITE_APPLY_ONLY: 仅应用\nGALLERY_URL_REWRITE_NO_CHANGES: 没有任何 URL 被修改\nGALLERY_URL_REWRITE_EMPTY_RESULT_WARN: 重写结果为空，已跳过\n\n# tray-page\n\nWAIT_TO_UPLOAD: 等待上传\nALREADY_UPLOAD: 已上传\n\n# upload-page\n\nPICTURE_UPLOAD: 图片上传\nDRAG_FILE_TO_HERE: 将文件拖拽到此处，或\nCLICK_TO_UPLOAD: 点击上传\nLINK_FORMAT: 链接格式\nCUSTOM: 自定义\nCLIPBOARD_PICTURE: 剪贴板图片\nTIPS_DRAG_VALID_PICTURE_OR_URL: 请拖入合法的图片文件或者图片URL地址\nTIPS_INPUT_URL: 请输入URL\nTIPS_HTTP_PREFIX: http:// 或者 https:// 开头，支持上传多条 URL（请换行输入）\nTIPS_INPUT_VALID_URL: 请输入合法的URL\n\n# plugins\n\nPLUGIN_SEARCH_PLACEHOLDER: 搜索npm上的PicGo插件，或者点击上方按钮查看优秀插件列表\nPLUGIN_INSTALL: 安装\nPLUGIN_INSTALLING: 安装中\nPLUGIN_INSTALLED: 已安装\nPLUGIN_DOING_SOMETHING: 进行中\nPLUGIN_LIST: 插件列表\nPLUGIN_IMPORT_LOCAL: 导入本地插件\n\n# tips\n\nTIPS_REMOVE_LINK: 此操作将把该图片移出相册, 是否继续?\nTIPS_WILL_REMOVE_CHOOSED_IMAGES: 将在相册中移除刚才选中的 ${m} 张图片，是否继续？\nTIPS_MUST_CONTAINS_URL: 必须含有$url 或 $fileName 或 $extName\nTIPS_NETWORK_ERROR: 网络错误暂时无法获取\nTIPS_NEED_RELOAD: 需要重启生效\nTIPS_PLEASE_CHOOSE_LOG_LEVEL: 请选择日志记录等级\nTIPS_SET_SUCCEED: 设置成功\nTIPS_PLUGIN_NOT_GUI_IMPLEMENT: 该插件未对可视化界面进行优化, 是否继续安装?\nTIPS_CLICK_NOTIFICATION_TO_RELOAD: 请点击此通知重启应用以生效\nTIPS_GET_PLUGIN_LIST_FAILED: 获取插件列表失败\n\n# ---renderer i18n end---\n\n# plugins\nPLUGIN_INSTALL_SUCCEED: 插件安装成功\nPLUGIN_INSTALL_FAILED: 插件安装失败\nPLUGIN_UNINSTALL_SUCCEED: 插件卸载成功\nPLUGIN_UNINSTALL_FAILED: 插件卸载失败\nPLUGIN_UPDATE_SUCCEED: 插件更新成功\nPLUGIN_UPDATE_FAILED: 插件更新失败\nPLUGIN_IMPORT_SUCCEED: 插件导入成功\nPLUGIN_IMPORT_FAILED: 插件导入失败\nENABLE_PLUGIN: 启用插件\nDISABLE_PLUGIN: 禁用插件\nUNINSTALL_PLUGIN: 卸载插件\nUPDATE_PLUGIN: 更新插件\n\n# toolbox\nTOOLBOX: 工具箱\nTOOLBOX_TITLE: 排查 PicGo 运行时问题\nTOOLBOX_SUB_TITLE: 立即扫描以下项目，修复使用问题\nTOOLBOX_CHECK_CONFIG_FILE_BROKEN: 检查配置文件是否损坏\nTOOLBOX_CHECK_GALLERY_FILE_BROKEN: 检查相册文件是否损坏\nTOOLBOX_CHECK_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: 检查剪贴板图片上传是否存在问题\nTOOLBOX_CHECK_PROBLEM_WITH_PROXY: 检查代理设置是否正常\nTOOLBOX_FIX_DONE_NEED_RELOAD: 修复完成，需要重启生效，是否重启\nTOOLBOX_CANT_AUTO_FIX: 无法自动修复，请自行修复以下问题\nTOOLBOX_START_SCAN: 开始扫描\nTOOLBOX_RE_SCAN: 重新扫描\nTOOLBOX_START_FIX: 开始修复\nTOOLBOX_SUCCESS_TIPS: 恭喜你，没有检查出问题\nTOOLBOX_CHECK_CONFIG_FILE_PATH_TIPS: 配置文件路径是：${path}\nTOOLBOX_CHECK_CONFIG_FILE_BROKEN_TIPS: 配置文件已损坏\nTOOLBOX_CHECK_GALLERY_FILE_PATH_TIPS: 相册文件路径是：${path}\nTOOLBOX_CHECK_GALLERY_FILE_BROKEN_TIPS: 相册文件已损坏\nTOOLBOX_CHECK_PROXY_SUCCESS_TIPS: 代理设置正常\nTOOLBOX_CHECK_PROXY_NO_PROXY_TIPS: 无代理设置\nTOOLBOX_CHECK_PROXY_PROXY_IS_NOT_CORRECT: 代理设置不正确\nTOOLBOX_CHECK_PROXY_PROXY_IS_NOT_WORKING: 代理设置不可用\nTOOLBOX_CHECK_CLIPBOARD_FILE_PATH_TIPS: 剪贴板图片临时文件夹路径是：${path}\nTOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS: 剪贴板图片临时文件夹不存在：${path}\nTOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS: 请自行创建文件夹：${path}\n\n# tips\nTIPS_NOTICE: 注意\nTIPS_WARNING: 警告\nTIPS_ERROR: 发生错误\nTIPS_SKIPPED_INVALID_URLS: 已跳过 ${n} 条非法 URL，请查看日志了解详情\nTIPS_TOO_MANY_URLS_CONFIRM: 你将一次上传 ${n} 条 URL，可能会引起卡顿，建议分批上传。是否继续？\nTIPS_NO_VALID_URLS: 未检测到合法的 URL\nTIPS_INSTALL_NODE_AND_RELOAD_PICGO: 请安装Node.js并重启PicGo再继续操作\nTIPS_PLUGIN_REMOVE_GALLERY_ITEM: 有插件正在试图删除一些相册图片，是否继续\nTIPS_PLUGIN_OVERWRITE_GALLERY: 有插件正在试图覆盖相册列表，是否继续\nTIPS_UPLOAD_NOT_PICTURES: 剪贴板最新的一条记录不是图片\nTIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicGo 配置文件损坏，已经恢复为默认配置\nTIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicGo 配置文件损坏，已经恢复为备份配置\nTIPS_PICGO_BACKUP_FILE_VERSION: \"备份文件版本: ${v}\"\nTIPS_CUSTOM_CONFIG_FILE_PATH_ERROR: 自定义文件解析出错，请检查路径内容是否正确\nTIPS_SHORTCUT_MODIFIED_SUCCEED: 快捷键已经修改成功\nTIPS_SHORTCUT_MODIFIED_CONFLICT: 快捷键冲突，请重新设置\nTIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED: 自定义链接格式已经修改成功\nTIPS_FIND_NEW_VERSION: 发现新版本${v}，更新了很多功能，是否去下载最新的版本？\nTIPS_DELETE_UPLOADER_CONFIG: 是否要删除这个配置？\nTIPS_COPY_UPLOADER_CONFIG: 是否要复制这个配置？\nTIPS_UPLOADER_CONFIG_NAME_EMPTY: 配置名称不能为空\nTIPS_UPLOADER_CONFIG_NOT_FOUND: 未找到该配置\nTIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST: 无法删除最后一个配置\n\n# privacy\nPRIVACY: \"使用前请阅读并同意隐私政策 ${privacyUrl} 与服务条款 ${termsUrl}，同意后方可使用。\"\nPRIVACY_TIPS: 请同意隐私协议，否则无法上传。\nQUIT: 退出\n"
  },
  {
    "path": "public/i18n/zh-TW.yml",
    "content": "LANG_DISPLAY_LABEL: 繁體中文\nABOUT: 關於\nOPEN_MAIN_WINDOW: 打開主視窗\nCHOOSE_DEFAULT_PICBED: 選擇預設圖床\nOPEN_UPDATE_HELPER: 開啟更新助手\nPRIVACY_TERMS_AGREEMENT: 隱私與條款協議\nRELOAD_APP: 重啟程式\nUPLOAD_SUCCEED: 上傳成功\nUPLOAD_FAILED: 上傳失敗\nUPLOAD_PROGRESS: 上傳進度\nOPERATION_SUCCEED: 操作成功\nOPERATION_FAILED: 操作失敗\nUPLOADING: 正在上傳\nQUICK_UPLOAD: 快速上傳\nUPLOAD_BY_CLIPBOARD: 剪貼簿圖片上傳\nHIDE_WINDOW: 隱藏視窗\nSPONSOR_PICGO: 贊助 PicGo\nSHOW_PICBED_QRCODE: 產生圖床配置 QRCODE\nPICBED_QRCODE: 圖床配置 QRCODE\nENABLE: 啟用\nDISABLE: 禁用\nCONFIG_THING: 設定${c}\nFIND_NEW_VERSION: 發現新版本\nNO_MORE_NOTICE: 以後不再提醒\nSHOW_DEVTOOLS: 開啟開發者工具\nCURRENT_PICBED: 當前圖床\nOPEN_TOOLBOX: 開啟修復工具箱\n\n# ---renderer i18n begin---\n\nCHOOSE_YOUR_DEFAULT_PICBED: 選擇 ${d} 作為你的預設圖床：\nUPLOAD_AREA: 上傳區\nGALLERY: 相簿\nPICBEDS_SETTINGS: 圖床設定\nPICGO_SETTINGS: PicGo設定\nPLUGIN_SETTINGS: 插件設定\nPICGO_CLOUD_TITLE: PicGo Cloud\nPICGO_CLOUD_ERROR_TITLE: PicGo Cloud 錯誤\nPICGO_CLOUD_NOT_LOGGED_IN: 尚未登入 PicGo Cloud\nPICGO_CLOUD_LOGIN: 登入\nPICGO_CLOUD_LOGOUT: 登出\nPICGO_CLOUD_CANCEL_LOGIN: 取消登入\nPICGO_CLOUD_RETRY: 重試\nPICGO_CLOUD_CONFIG_SYNC: 配置同步\nPICGO_CLOUD_LOGIN_IN_PROGRESS: 登入進行中，請在瀏覽器完成登入。\nPICGO_CLOUD_LOGGED_IN_AS: \"已登入：${user}\"\nPICGO_CLOUD_OPEN: 打開 PicGo Cloud\nPICGO_CLOUD_LOGIN_TIMEOUT: 登入逾時，請重試。\nPICGO_CLOUD_LOGIN_FAILED: 登入失敗，請重試。\nPICGO_CLOUD_AGREE_PREFIX: 我已閱讀並同意 PicGo 的\nPICGO_CLOUD_TERMS_OF_SERVICE: 服務條款\nPICGO_CLOUD_AGREE_AND: 以及\nPICGO_CLOUD_PRIVACY_POLICY: 隱私政策\nPICGO_CLOUD_LOGIN_EXPIRED: 登入失效，請重新登入。\nPICGO_CLOUD_ENCRYPTION_MODE_LABEL: 加密模式\nPICGO_CLOUD_ENCRYPTION_MODE_AUTO: 自動\nPICGO_CLOUD_ENCRYPTION_MODE_SERVER: 服務端加密\nPICGO_CLOUD_ENCRYPTION_MODE_E2E: 端到端加密\nPICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO: 自動：跟隨雲端上一次配置的加密方式（預設服務端加密）。\nPICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER: 服務端加密：我們會在服務端對你的配置加密後再儲存，不需要 PIN。\nPICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E: 端到端加密：需要你輸入 PIN 來加密/解密資料。PicGo 不儲存 PIN，遺失將無法找回資料。\nPICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC: 查看完整文檔\nPICGO_CLOUD_E2E_CHECKBOX_LABEL: 開啟 E2E 加密\nPICGO_CLOUD_E2E_ENABLE_WARNING_TITLE: 開啟 E2E 加密\nPICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE: E2E 加密需要你自行妥善保管 PIN。PIN 遺失將無法恢復加密資料。PicGo 不會儲存 PIN 或任何恢復資訊。\nPICGO_CLOUD_REMOTE_E2E_AUTO_ENABLED: 偵測到遠端配置已開啟 E2E 加密，已自動在本地開啟 E2E。\nPICGO_CLOUD_E2E_PIN_SETUP_TITLE: 設定 E2E PIN\nPICGO_CLOUD_E2E_PIN_DECRYPT_TITLE: 輸入 E2E PIN\nPICGO_CLOUD_E2E_PIN_RETRY_TITLE: \"PIN 錯誤，請重試（第 ${retryCount} 次）。\"\nPICGO_CLOUD_E2E_PIN_PLACEHOLDER: PIN\nPICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER: 確認 PIN\nPICGO_CLOUD_CONFIG_SYNC_SUCCESS: 配置同步成功。\nPICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED: 偵測到配置衝突，請選擇解決方案。\nPICGO_CLOUD_CONFIG_SYNC_FAILED: 配置同步失敗。\nPICGO_CLOUD_CONFIG_SYNC_ABORTED: 已取消配置同步。\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE: 確認切換加密方式嗎？\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY: \"您正在從「${from}」切換為「${to}」。\\n\\n注意：切換加密模式將清空您所有的雲端歷史版本記錄。\\n這是因為舊的歷史版本無法在新模式下被解密或驗證。\\n切換後，系統將立即為您建立一份新的備份作為起點。\"\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM: 確認切換並清空歷史\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL: 取消\nPICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED: 已取消切換加密方式\nPICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON: \"配置同步失敗：${reason}\"\nPICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY: PIN 錯誤次數過多，請稍後重試。\nPICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID: 本地配置檔格式錯誤，請檢查配置檔。\nPICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS: 配置同步進行中…\nPICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING: 存在未解決的衝突，請先處理。\nPICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION: 未找到可處理的衝突工作階段。\nPICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE: 請為所有衝突項選擇本地或雲端。\nPICGO_CLOUD_CONFIG_SYNC_STARTING: 配置同步已開始…\nPICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE: \"衝突處理（${count} 項）\"\nPICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL: 全選本地\nPICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD: 全選雲端\nPICGO_CLOUD_CONFIG_SYNC_RESET_ALL: 重置全部\nPICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION: 本地版本\nPICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION: 雲端版本\nPICGO_CLOUD_CONFIG_SYNC_ABORT: 放棄同步\nPICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC: 確認並同步\nPICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED: （空）\nPICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED: 配置衝突已解決。\nPICGO_CLOUD_LAST_SYNC_TIME: \"上次本地同步時間：${time}\"\nPICGO_CLOUD_LAST_SYNC_TIME_NONE: 無\nPICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE: 需要重啟嗎？\nPICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE: 部分配置可能需要重啟後生效，是否立即重啟？\nPICGO_CLOUD_CONFIG_SYNC_RESTART_NOW: 立即重啟\nPICGO_CLOUD_CONFIG_SYNC_RESTART_LATER: 稍後\nINPUT_BOX_CONFIRM_MISMATCH: 兩次輸入不一致，請重新輸入。\nPICGO_SPONSOR_TEXT: PicGo是開放原始碼的軟體，如果你喜歡它，對你有幫助，不妨請我喝杯咖啡？\nALIPAY: 支付寶\nWECHATPAY: 微信支付\nCHOOSE_PICBED: 選擇圖床\nCOPY_PICBED_CONFIG: 複製圖床設定\nCOPY_PICBED_CONFIG_SUCCEED: 複製圖床設定成功\nINPUT: 輸入框\nCANCEL: 取消\nCONFIRM: 確定\nCHOOSE_SHOWED_PICBED: 請選擇顯示的圖床\nCHOOSE_PASTE_FORMAT: 請選擇貼上的格式\nSEARCH: 搜尋\nCOPY: 複製\nDELETE: 刪除\nSELECT_ALL: 全選\nCHANGE_IMAGE_URL: 修改圖片URL\nCHANGE_IMAGE_URL_SUCCEED: 修改圖片URL成功\nCOPY_LINK_SUCCEED: 複製連結成功\nBATCH_COPY_LINK_SUCCEED: 批量複製連結成功\nFILE_RENAME: 檔案改名\nCOPY_FILE_PATH: 複製檔案路徑\nOPEN_FILE_PATH: 打開檔案路徑\nSUCCESS: 成功\nFAILED: 失敗\n\n# settings\n\nSETTINGS: 設定\nSETTINGS_OPEN_CONFIG_FILE: 打開設定檔案\nSETTINGS_CLICK_TO_OPEN: 點擊打開\nSETTINGS_SET_LOG_FILE: 設定記錄檔案\nSETTINGS_CLICK_TO_SET: 點擊設定\nSETTINGS_CLICK_TO_CHECK: 點擊檢查\nSETTINGS_SET_SHORTCUT: 設定快捷鍵\nSETTINGS_URL_REWRITE: URL 重寫\nSETTINGS_CUSTOM_LINK_FORMAT: 自訂連結格式\nSETTINGS_SET_PROXY_AND_MIRROR: 設定PROXY和鏡像地址\nSETTINGS_SET_SERVER: 設定Server\nSETTINGS_CHECK_UPDATE: 檢查更新\nSETTINGS_OPEN_UPDATE_HELPER: 打開更新助手\nSETTINGS_OPEN: 開\nSETTINGS_CLOSE: 關\nSETTINGS_ACCEPT_BETA_UPDATE: 接受Beta版本更新\nSETTINGS_LAUNCH_ON_BOOT: 開機時啟動\nSETTINGS_RENAME_BEFORE_UPLOAD: 上傳前重新命名\nSETTINGS_TIMESTAMP_RENAME: 以時間戳命名\nSETTINGS_OPEN_UPLOAD_TIPS: 開啟上傳提示\nSETTINGS_NOTIFICATION_SOUND: 開啟通知提示音\nSETTINGS_MINI_WINDOW_ON_TOP: Mini視窗置頂\nSETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: 上傳後自動複製URL\nSETTINGS_TIPS_PLACEHOLDER_URL: 用佔位符 $url 來表示URL的位置\nSETTINGS_TIPS_PLACEHOLDER_FILENAME: 用佔位符 $fileName 來表示檔案名稱的位置\nSETTINGS_TIPS_PLACEHOLDER_EXTNAME: 用佔位符 $extName 來表示檔案格式的位置\nSETTINGS_TIPS_SUCH_AS: 如\nSETTINGS_UPLOAD_PROXY: 上傳PROXY\nSETTINGS_PLUGIN_INSTALL_PROXY: 插件安裝PROXY\nSETTINGS_PLUGIN_INSTALL_MIRROR: 插件安裝鏡像\nSETTINGS_CURRENT_VERSION: 當前版本\nSETTINGS_NEWEST_VERSION: 最新版本\nSETTINGS_GETING: 正在取得中\nSETTINGS_TIPS_HAS_NEW_VERSION: PicGo更新啦，請點擊確定開啟下載頁面\nSETTINGS_LOG_FILE: 記錄檔案\nSETTINGS_LOG_LEVEL: 記錄等级\nSETTINGS_LOG_FILE_SIZE: 記錄檔案大小\nSETTINGS_SET_PICGO_SERVER: 設定PicGo-Server\nSETTINGS_TIPS_SERVER_NOTICE: 如果你不知道Server的作用，請閱讀文檔，或者不用修改設定。\nSETTINGS_ENABLE_SERVER: 是否開啟Server\nSETTINGS_SET_SERVER_HOST: 設定監聽地址\nSETTINGS_SET_SERVER_PORT: 設定監聽端口\nSETTINGS_TIP_PLACEHOLDER_HOST: 推薦預設地址:127.0.0.1\nSETTINGS_TIP_PLACEHOLDER_PORT: 推薦預設端口:36677\nSETTINGS_LOG_LEVEL_ALL: 全部-All\nSETTINGS_LOG_LEVEL_SUCCESS: 成功-Success\nSETTINGS_LOG_LEVEL_ERROR: 錯誤-Error\nSETTINGS_LOG_LEVEL_INFO: 普通-Info\nSETTINGS_LOG_LEVEL_WARN: 提醒-Warn\nSETTINGS_LOG_LEVEL_NONE: 不記錄-None\nSETTINGS_RESULT: 設定結果\nSETTINGS_DEFAULT_PICBED: 設定預設圖床\nSETTINGS_SET_DEFAULT_PICBED: 設為預設圖床\nSETTINGS_NOT_CONFIG_OPTIONS: 暫無設定選項\nSETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: 使用內建剪貼簿上傳\nSETTINGS_CHOOSE_LANGUAGE: 選擇語言\nBUILTIN_CLIPBOARD_TIPS: 使用內建剪貼簿函數而不是調用腳本取得剪貼簿內的照片\nUPLOADER_CONFIG_NAME: 圖床配置名\n\n# url rewrite\n\nURL_REWRITE_HELP: 用於重寫上傳後的圖片 URL。規則按順序匹配，命中的第一條匹配生效。\nURL_REWRITE_ADD_RULE: 新增規則\nURL_REWRITE_EDIT_RULE: 編輯規則\nURL_REWRITE_EMPTY: 暫無規則\nURL_REWRITE_ORDER: 順序\nURL_REWRITE_MATCH: 匹配\nURL_REWRITE_REPLACE: 替換\nURL_REWRITE_FLAGS: 標誌\nURL_REWRITE_ENABLED: 啟用\nURL_REWRITE_ACTIONS: 操作\nURL_REWRITE_MOVE_UP: 上移\nURL_REWRITE_MOVE_DOWN: 下移\nURL_REWRITE_EDIT: 編輯\nURL_REWRITE_DELETE: 刪除\nURL_REWRITE_DELETE_CONFIRM: 確定刪除該規則？\nURL_REWRITE_MATCH_TIPS: 支援正則（JavaScript RegExp）\nURL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path\nURL_REWRITE_REPLACE_TIPS: 替換內容（支援 $1、$2...）\nURL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath\nURL_REWRITE_OPTIONS: 選項\nURL_REWRITE_RULE_ENABLED: 啟用該規則\nURL_REWRITE_FLAG_GLOBAL_LABEL: 全域（g）\nURL_REWRITE_FLAG_GLOBAL_DESC: 替換所有找到的內容，而不僅僅是第一個\nURL_REWRITE_FLAG_IGNORE_CASE_LABEL: 忽略大小寫（i）\nURL_REWRITE_FLAG_IGNORE_CASE_DESC: 匹配時不區分字母大小寫（例如 JPG 等同於 jpg）\nURL_REWRITE_MATCH_REQUIRED: 匹配規則不能為空\nURL_REWRITE_REPLACE_REQUIRED: 替換內容不能為空\nURL_REWRITE_INVALID_REGEX: 正則表達式不合法\nURL_REWRITE_PREVIEW_TITLE: 預覽\nURL_REWRITE_PREVIEW_TIPS: 輸入一個 URL，查看目前規則的重寫結果（按順序匹配，僅第一條命中的規則生效）\nURL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png\nURL_REWRITE_PREVIEW_RUN: 預覽\nURL_REWRITE_PREVIEW_OUTPUT: 輸出 URL\nURL_REWRITE_PREVIEW_INPUT_REQUIRED: 請輸入要預覽的 URL\nURL_REWRITE_PREVIEW_RULE_INVALID: 規則無效\nURL_REWRITE_PREVIEW_MATCHED_RULE: 命中規則\nURL_REWRITE_PREVIEW_NO_MATCH: 沒有規則匹配\nUPLOADER_CONFIG_PLACEHOLDER: 請輸入配置名稱\nSELECTED_SETTING_HINT: 已選中\nSETTINGS_ENCODE_OUTPUT_URL: 輸出（複製） URL 時進行轉義\nSETTINGS_SHOW_DOCK_ICON: 顯示 Dock 欄圖示\nSETTINGS_SHOW_MENUBAR_ICON: 顯示頂部欄圖示\nSETTINGS_SHOW_MENUBAR_ICON_TIPS: 若「顯示 Dock 欄圖示」與「顯示頂部欄圖示」都關閉，將會無法找到 PicGo 主介面。需要手動修改配置檔案裡的 showDockIcon 或 showMenubarIcon 為 true 才能恢復。\nSETTINGS_STARTUP_MODE: 啟動模式\nSETTINGS_STARTUP_MODE_MAIN_WINDOW: 打開主視窗\nSETTINGS_STARTUP_MODE_MINI_WINDOW: 打開 Mini 視窗\nSETTINGS_STARTUP_MODE_HIDE: 靜默啟動\n\n# shortcut-page\n\nSHORTCUT_NAME: 快捷鍵名稱\nSHORTCUT_BIND: 快捷鍵綁定\nSHORTCUT_STATUS: 狀態\nSHORTCUT_ENABLED: 已啟用\nSHORTCUT_DISABLED: 已禁用\nSHORTCUT_SOURCE: 來源\nSHORTCUT_HANDLE: 操作\nSHORTCUT_ENABLE: 啟用\nSHORTCUT_DISABLE: 禁用\nSHORTCUT_EDIT: 編輯\nSHORTCUT_CHANGE_UPLOAD: 修改上傳快捷鍵\n\n# gallery-page\nGALLERY_URL_REWRITE_TITLE: 重寫選中圖片 URL\nGALLERY_URL_REWRITE_RESULT_TITLE: 重寫圖片 URL 結果\nGALLERY_URL_REWRITE_WARN_NO_SELECTION: 你必須先選中至少一張圖片\n\nGALLERY_URL_REWRITE_APPLY_GLOBAL_RULES: 套用全域 URL 重寫規則\nGALLERY_URL_REWRITE_GLOBAL_RULES_COUNT: 全域規則數量\nGALLERY_URL_REWRITE_TEMP_RULE_TIPS: 可選，留空則不使用臨時規則（臨時規則優先級高於全域規則）。\nGALLERY_URL_REWRITE_TEMP_RULE_REQUIRED: 臨時規則需要同時填寫「匹配」和「替換」\nGALLERY_URL_REWRITE_NO_RULES_TO_APPLY: 沒有可用的全域規則，且未填寫臨時規則\nGALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: 是否將臨時規則寫入全域 URL 重寫規則列表？\nGALLERY_URL_REWRITE_APPLY_AND_SAVE: 套用並寫入\nGALLERY_URL_REWRITE_APPLY_ONLY: 僅套用\nGALLERY_URL_REWRITE_NO_CHANGES: 沒有任何 URL 被修改\nGALLERY_URL_REWRITE_EMPTY_RESULT_WARN: 重寫結果為空，已跳過\n\n# tray-page\n\nWAIT_TO_UPLOAD: 等待上傳\nALREADY_UPLOAD: 已上傳\n\n# upload-page\n\nPICTURE_UPLOAD: 圖片上傳\nDRAG_FILE_TO_HERE: 將檔案拖曳到此處，或\nCLICK_TO_UPLOAD: 點擊上傳\nLINK_FORMAT: 連結格式\nCUSTOM: 自訂\nCLIPBOARD_PICTURE: 剪貼簿圖片\nTIPS_DRAG_VALID_PICTURE_OR_URL: 請拖入合法的圖片檔案或者圖片URL地址\nTIPS_INPUT_URL: 請輸入URL\nTIPS_HTTP_PREFIX: http:// 或者 https:// 開頭，支援上傳多條 URL（請換行輸入）\nTIPS_INPUT_VALID_URL: 請輸入合法的URL\n\n# plugins\n\nPLUGIN_SEARCH_PLACEHOLDER: 搜尋npm上的PicGo插件，或者點擊上方按鈕查看優秀插件列表\nPLUGIN_INSTALL: 安裝\nPLUGIN_INSTALLING: 安裝中\nPLUGIN_INSTALLED: 已安裝\nPLUGIN_DOING_SOMETHING: 進行中\nPLUGIN_LIST: 插件列表\nPLUGIN_IMPORT_LOCAL: 導入本地插件\n\n# tips\n\nTIPS_REMOVE_LINK: 此操作將在相簿中移除該圖片，是否繼續？\nTIPS_WILL_REMOVE_CHOOSED_IMAGES: 將在相簿中移除剛才選中的 ${m} 張圖片，是否繼續？\nTIPS_MUST_CONTAINS_URL: 必須含有$url 或 $fileName 或 $extName\nTIPS_NETWORK_ERROR: 網路錯誤，暫時無法取得\nTIPS_NEED_RELOAD: 需要重新啟動生效\nTIPS_PLEASE_CHOOSE_LOG_LEVEL: 請選擇記錄等級\nTIPS_SET_SUCCEED: 設定成功\nTIPS_PLUGIN_NOT_GUI_IMPLEMENT: 該插件未對GUI進行優化，是否繼續安裝？\nTIPS_CLICK_NOTIFICATION_TO_RELOAD: 請點擊此通知重新啟動程式以生效\nTIPS_GET_PLUGIN_LIST_FAILED: 取得插件列表失敗\n\n# ---renderer i18n end---\n\n# plugins\nPLUGIN_INSTALL_SUCCEED: 插件安裝成功\nPLUGIN_INSTALL_FAILED: 插件安裝失敗\nPLUGIN_UNINSTALL_SUCCEED: 插件卸載成功\nPLUGIN_UNINSTALL_FAILED: 插件卸載失敗\nPLUGIN_UPDATE_SUCCEED: 插件更新成功\nPLUGIN_UPDATE_FAILED: 插件更新失敗\nPLUGIN_IMPORT_SUCCEED: 插件導入成功\nPLUGIN_IMPORT_FAILED: 插件導入失敗\nENABLE_PLUGIN: 啟用插件\nDISABLE_PLUGIN: 禁用插件\nUNINSTALL_PLUGIN: 卸載插件\nUPDATE_PLUGIN: 更新插件\n\n# toolbox\nTOOLBOX: 工具箱\nTOOLBOX_TITLE: 排查 PicGo 執行時問題\nTOOLBOX_SUB_TITLE: 立即掃描以下項目,修復使用問題\nTOOLBOX_CHECK_CONFIG_FILE_BROKEN: 檢查配置文件是否損壞\nTOOLBOX_CHECK_GALLERY_FILE_BROKEN: 檢查相冊文件是否損壞\nTOOLBOX_CHECK_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: 檢查剪貼板圖片上傳是否存在問題\nTOOLBOX_CHECK_PROBLEM_WITH_PROXY: 檢查代理設置是否正常\nTOOLBOX_FIX_DONE_NEED_RELOAD: 修復完成,需要重啓生效,是否重啓\nTOOLBOX_CANT_AUTO_FIX: 無法自動修復,請自行修復以下問題\nTOOLBOX_START_SCAN: 開始掃描\nTOOLBOX_RE_SCAN: 重新掃描\nTOOLBOX_START_FIX: 開始修復\nTOOLBOX_SUCCESS_TIPS: 恭喜你,沒有檢查出問題\nTOOLBOX_CHECK_CONFIG_FILE_PATH_TIPS: 配置文件路徑是：${path}\nTOOLBOX_CHECK_CONFIG_FILE_BROKEN_TIPS: 配置文件已損壞\nTOOLBOX_CHECK_GALLERY_FILE_PATH_TIPS: 相冊文件路徑是：${path}\nTOOLBOX_CHECK_GALLERY_FILE_BROKEN_TIPS: 相冊文件已損壞\nTOOLBOX_CHECK_PROXY_SUCCESS_TIPS: 代理設置正常\nTOOLBOX_CHECK_PROXY_NO_PROXY_TIPS: 無代理設置\nTOOLBOX_CHECK_PROXY_PROXY_IS_NOT_CORRECT: 代理設置不正確\nTOOLBOX_CHECK_PROXY_PROXY_IS_NOT_WORKING: 代理設置不可用\nTOOLBOX_CHECK_CLIPBOARD_FILE_PATH_TIPS: 剪貼板圖片臨時文件夾路徑是：${path}\nTOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS: 剪貼板圖片臨時文件夾不存在：${path}\nTOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS: 請自行創建文件夾:${path}\n\n# tips\nTIPS_NOTICE: 注意\nTIPS_WARNING: 警告\nTIPS_ERROR: 發生錯誤\nTIPS_SKIPPED_INVALID_URLS: 已跳過 ${n} 條非法 URL，請查看日誌了解詳情\nTIPS_TOO_MANY_URLS_CONFIRM: 你將一次上傳 ${n} 條 URL，可能會引起卡頓，建議分批上傳。是否繼續？\nTIPS_NO_VALID_URLS: 未偵測到合法的 URL\nTIPS_INSTALL_NODE_AND_RELOAD_PICGO: 請安裝Node.js並重新啟動PicGo再繼續操作\nTIPS_PLUGIN_REMOVE_GALLERY_ITEM: 有插件正在試圖刪除一些相簿圖片，是否繼續？\nTIPS_PLUGIN_OVERWRITE_GALLERY: 有插件正在試圖覆蓋相簿列表，是否繼續？\nTIPS_UPLOAD_NOT_PICTURES: 剪貼簿最新的一條記錄不是圖片\nTIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicGo 設定檔案已損壞，已經恢復為預設設定\nTIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicGo 設定檔案已損壞，已經恢復為備份設定\nTIPS_PICGO_BACKUP_FILE_VERSION: \"備份檔案版本: ${v}\"\nTIPS_CUSTOM_CONFIG_FILE_PATH_ERROR: 自訂設定檔案解析出錯，請檢查路徑內容是否正確\nTIPS_SHORTCUT_MODIFIED_SUCCEED: 快捷鍵已經修改成功\nTIPS_SHORTCUT_MODIFIED_CONFLICT: 快捷鍵衝突，請重新設定\nTIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED: 自訂連結格式已經修改成功\nTIPS_FIND_NEW_VERSION: 發現新版本${v}，更新了很多功能，是否去下載最新的版本？\nTIPS_DELETE_UPLOADER_CONFIG: 是否要刪除這個配置？\nTIPS_COPY_UPLOADER_CONFIG: 是否要複製這個配置？\nTIPS_UPLOADER_CONFIG_NAME_EMPTY: 配置名稱不能為空\nTIPS_UPLOADER_CONFIG_NOT_FOUND: 未找到該配置\nTIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST: 無法刪除最後一個配置\n\n# privacy\nPRIVACY: \"使用前請閱讀並同意隱私政策 ${privacyUrl} 與服務條款 ${termsUrl}，同意後方可使用。\"\nPRIVACY_TIPS: 請同意隱私協議，否則無法上傳。\nQUIT: 退出\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"referrer\" content=\"never\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n    <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\">\n  </head>\n  <body>\n    <noscript>\n      <strong>We're sorry but picgo-new doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\n    </noscript>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "public/linux.sh",
    "content": "#!/bin/sh\nif [ \"$XDG_SESSION_TYPE\" = \"x11\" ]; then\n  # require xclip(see http://stackoverflow.com/questions/592620/check-if-a-program-exists-from-a-bash-script/677212#677212)\n  command -v xclip >/dev/null 2>&1 || { echo >&1 \"no xclip\"; exit 1; }\n  # write image in clipboard to file (see http://unix.stackexchange.com/questions/145131/copy-image-from-clipboard-to-file)\n  filePath=`xclip -selection clipboard -o 2>/dev/null | grep ^file:// | cut -c8-`\n  if [ ! -n \"$filePath\" ] ;then\n    if\n        xclip -selection clipboard -target image/png -o >/dev/null 2>&1\n    then\n        xclip -selection clipboard -target image/png -o >$1 2>/dev/null\n        echo $1\n    else\n        echo \"no image\"\n    fi\n  else\n    echo $filePath\n  fi\nelif [ \"$XDG_SESSION_TYPE\" = \"wayland\" ]; then\n  command -v wl-paste >/dev/null 2>&1 || { echo >&1 \"no wl-clipboard\"; exit 1; }\n  isImage=`wl-paste --list-types | grep image`\n  if [ -n \"$isImage\" ]; then\n    wl-paste --type image/png > $1 2>/dev/null\n    echo $1\n  else\n    echo \"no image\"\n    exit 1\n  fi\nelse\n  # fallback for unsupported session types\n  echo >&2 \"Error: Unsupported session type '$XDG_SESSION_TYPE'.\"\n  echo >&2 \"Solution: The variable of XDG_SESSION_TYPE must set as 'x11' or 'wayland'.\"\n  exit 1\nfi\n"
  },
  {
    "path": "public/windows.ps1",
    "content": "\nparam($imagePath)\n\n# Adapted from https://github.com/octan3/img-clipboard-dump/blob/master/dump-clipboard-png.ps1\n\nAdd-Type -Assembly PresentationCore\n$img = [Windows.Clipboard]::GetImage()\n\nif ($img -eq $null) {\n    \"no image\"\n    Exit 1\n}\n\nif (-not $imagePath) {\n    \"no image\"\n    Exit 1\n}\n\n$fcb = new-object Windows.Media.Imaging.FormatConvertedBitmap($img, [Windows.Media.PixelFormats]::Rgb24, $null, 0)\n$stream = [IO.File]::Open($imagePath, \"OpenOrCreate\")\n$encoder = New-Object Windows.Media.Imaging.PngBitmapEncoder\n$encoder.Frames.Add([Windows.Media.Imaging.BitmapFrame]::Create($fcb)) | out-null\n$encoder.Save($stream) | out-null\n$stream.Dispose() | out-null\n\n$imagePath\n"
  },
  {
    "path": "public/windows10.ps1",
    "content": "# Adapted from https://github.com/octan3/img-clipboard-dump/blob/master/dump-clipboard-png.ps1\nparam($imagePath)\n\n# https://github.com/PowerShell/PowerShell/issues/7233\n# fix the output encoding bug\n[console]::InputEncoding = [console]::OutputEncoding = New-Object System.Text.UTF8Encoding\n\nAdd-Type -Assembly PresentationCore\nfunction main {\n    $img = [Windows.Clipboard]::GetImage()\n\n    if ($img -eq $null) {\n        \"no image\"\n        Exit 1\n    }\n\n    if (-not $imagePath) {\n        \"no image\"\n        Exit 1\n    }\n\n    $fcb = new-object Windows.Media.Imaging.FormatConvertedBitmap($img, [Windows.Media.PixelFormats]::Rgb24, $null, 0)\n    $stream = [IO.File]::Open($imagePath, \"OpenOrCreate\")\n    $encoder = New-Object Windows.Media.Imaging.PngBitmapEncoder\n    $encoder.Frames.Add([Windows.Media.Imaging.BitmapFrame]::Create($fcb)) | out-null\n    $encoder.Save($stream) | out-null\n    $stream.Dispose() | out-null\n\n    $imagePath\n    Exit 1\n}\n\ntry {\n    # For WIN10\n    $file = Get-Clipboard -Format FileDropList\n    if ($file -ne $null) {\n        Convert-Path $file\n        Exit 1\n    }\n} catch {\n    # For WIN7 WIN8 WIN10\n    main\n}\n\nmain"
  },
  {
    "path": "public/wsl.sh",
    "content": "#!/bin/sh\n# grab the paths\nscriptPath=$(echo $0 | awk '{ print substr( $0, 1, length($0)-6 ) }')\"windows10.ps1\"\nimagePath=$(echo $1 | awk '{ print substr( $0, 1, length($0)-18 ) }')\nimageName=$(echo $1 | awk '{ print substr( $0, length($0)-17, length($0) ) }')\n\n# run the powershell script\nres=$(powershell.exe -noprofile -noninteractive -nologo -sta -executionpolicy unrestricted -file $(wslpath -w $scriptPath) $(wslpath -w $imagePath)\"\\\\\"$imageName)\n\n# note that there is a return symbol in powershell result\nnoImage=$(echo \"no image\\r\")\n\n# check whether image exists\nif [ \"$res\" = \"$noImage\" ] ;then\n    echo \"no image\"\nelse\n    echo $(wslpath -u $res)\nfi\n"
  },
  {
    "path": "scripts/config.js",
    "content": "// different platform has different format\n\n// macos (dmg, x64 + arm64)\nconst darwin = [{\n  appNameWithPrefix: 'PicGo',\n  ext: 'dmg',\n  arch: 'arm64',\n  'version-file': 'latest-mac.yml'\n}, {\n  appNameWithPrefix: 'PicGo',\n  ext: 'dmg',\n  arch: 'x64',\n  'version-file': 'latest-mac.yml'\n}]\n\n// linux (AppImage, deb, snap)\nconst linux = [{\n  appNameWithPrefix: 'PicGo',\n  ext: 'AppImage',\n  arch: 'arm64',\n  'version-file': 'latest-linux-arm64.yml'\n}, {\n  appNameWithPrefix: 'PicGo',\n  ext: 'AppImage',\n  arch: 'x86_64',\n  'version-file': 'latest-linux.yml'\n}, {\n  appNameWithPrefix: 'PicGo',\n  ext: 'deb',\n  arch: 'arm64',\n  'version-file': 'latest-linux-arm64.yml'\n}, {\n  appNameWithPrefix: 'PicGo',\n  ext: 'deb',\n  arch: 'amd64',\n  'version-file': 'latest-linux.yml'\n}, {\n  appNameWithPrefix: 'PicGo',\n  ext: 'snap',\n  arch: 'amd64',\n  'version-file': 'latest-linux.yml'\n}]\n\n// windows (nsis, x64 + ia32 + arm64)\nconst win32 = [{\n  appNameWithPrefix: 'PicGo',\n  ext: 'exe',\n  arch: 'ia32',\n  'version-file': 'latest.yml'\n}, {\n  appNameWithPrefix: 'PicGo',\n  ext: 'exe',\n  arch: 'x64',\n  'version-file': 'latest.yml'\n}, {\n  appNameWithPrefix: 'PicGo',\n  ext: 'exe',\n  arch: 'arm64',\n  'version-file': 'latest.yml'\n}]\n\nmodule.exports = {\n  darwin,\n  linux,\n  win32\n}\n"
  },
  {
    "path": "scripts/cos-link.js",
    "content": "const pkg = require('../package.json')\nconst version = pkg.version\n\nconst generateURL = (arch, ext, prefix = 'PicGo-') => `https://release.picgo.app/${version}/${prefix}${version}${arch}${ext}`\n\nconst windows = [\n  { label: '32 bit', arch: '-ia32', ext: '.exe' },\n  { label: '64 bit', arch: '-x64', ext: '.exe' },\n  { label: 'ARM64', arch: '-arm64', ext: '.exe' }\n]\n\nconst macos = [\n  { label: 'Intel', arch: '-x64', ext: '.dmg' },\n  { label: 'Apple Silicon', arch: '-arm64', ext: '.dmg' }\n]\n\nconst linux = {\n  AppImage: [\n    { label: '64 bit', arch: '-amd64', ext: '.AppImage' },\n    { label: 'ARM64', arch: '-arm64', ext: '.AppImage' }\n  ],\n  Deb: [\n    { label: '64 bit', arch: '-amd64', ext: '.deb' },\n    { label: 'ARM64', arch: '-arm64', ext: '.deb' }\n  ],\n  Snap: [\n    { label: '64 bit', arch: '-amd64', ext: '.snap' }\n  ]\n}\n\nconst renderLine = (items) => items.map(({ label, arch, ext }) => `[${label}](${generateURL(arch, ext)})`).join(' | ')\n\nconst sections = [\n  `### Windows\\n- ${renderLine(windows)}`,\n  `### macOS\\n- ${renderLine(macos)}`,\n  `### Linux\\n- AppImage: ${renderLine(linux.AppImage)}\\n- Deb: ${renderLine(linux.Deb)}\\n- Snap: ${renderLine(linux.Snap)}`\n]\n\nconsole.log(sections.join('\\n\\n'))\n"
  },
  {
    "path": "scripts/gen-i18n-types.js",
    "content": "/* eslint-disable @stylistic/indent */\nconst yaml = require('js-yaml')\nconst path = require('path')\nconst fs = require('fs')\nconst languageFileName = 'zh-CN.yml' // use zh-CN for type is OK\nconst i18nFolder = path.join(__dirname, '../public/i18n')\nconst typeFolder = path.join(__dirname, '../src/universal/types')\nconst languageFile = path.join(i18nFolder, languageFileName)\n\nconst langFile = fs.readFileSync(languageFile, 'utf8')\n\nconst obj = yaml.load(langFile)\n\nconst keys = Object.keys(obj)\n\nconst types =\n`interface ILocales {\n  ${keys.map(key => `${key}: string`).join('\\n  ')}\n}\ntype ILocalesKey = keyof ILocales\n`\n\nfs.writeFileSync(path.join(typeFolder, 'i18n.d.ts'), types)\n"
  },
  {
    "path": "scripts/merge-artifacts.js",
    "content": "/**\n * Merge artifacts from different platforms and architectures\n * Also merge latest*.yml files for electron-updater\n */\n\nconst fs = require('fs')\nconst path = require('path')\nconst yaml = require('js-yaml')\n\nconst ARTIFACTS_DIR = path.join(__dirname, '../artifacts')\nconst DIST_DIR = path.join(__dirname, '../dist')\n\n// yml 文件分组规则\nconst YML_MERGE_RULES = {\n  // macOS: 合并 x64 和 arm64 的 latest-mac.yml\n  'latest-mac.yml': ['latest-mac.yml'],\n  // Windows: 合并所有架构的 latest.yml\n  'latest.yml': ['latest.yml'],\n  // Linux x64: latest-linux.yml\n  'latest-linux.yml': ['latest-linux.yml'],\n  // Linux arm64: latest-linux-arm64.yml\n  'latest-linux-arm64.yml': ['latest-linux-arm64.yml']\n}\n\n/**\n * 递归查找指定文件名的所有文件\n */\nfunction findFiles(dir, filename) {\n  const results = []\n\n  if (!fs.existsSync(dir)) {\n    return results\n  }\n\n  const items = fs.readdirSync(dir)\n\n  for (const item of items) {\n    const fullPath = path.join(dir, item)\n    const stat = fs.statSync(fullPath)\n\n    if (stat.isDirectory()) {\n      results.push(...findFiles(fullPath, filename))\n    } else if (item === filename) {\n      results.push(fullPath)\n    }\n  }\n\n  return results\n}\n\n/**\n * 合并多个 yml 文件\n */\nfunction mergeYmlFiles(files) {\n  if (files.length === 0) return null\n  if (files.length === 1) {\n    return yaml.load(fs.readFileSync(files[0], 'utf8'))\n  }\n\n  const contents = files.map(f => yaml.load(fs.readFileSync(f, 'utf8')))\n\n  // 以第一个为基准，合并 files 数组\n  const merged = {\n    version: contents[0].version,\n    files: [],\n    releaseDate: contents[0].releaseDate\n  }\n\n  for (const content of contents) {\n    if (content.files && Array.isArray(content.files)) {\n      merged.files.push(...content.files)\n    }\n  }\n\n  // 去重（根据 sha512）\n  const seen = new Set()\n  merged.files = merged.files.filter(file => {\n    const key = file.sha512\n    if (seen.has(key)) return false\n    seen.add(key)\n    return true\n  })\n\n  // 设置 path/sha512/size 为第一个文件（electron-updater 兼容性）\n  if (merged.files.length > 0) {\n    merged.path = merged.files[0].url\n    merged.sha512 = merged.files[0].sha512\n    merged.size = merged.files[0].size\n  }\n\n  return merged\n}\n\n/**\n * 复制所有构建产物到 dist 目录\n */\nfunction copyArtifacts() {\n  console.log('📁 Copying all artifacts to dist...\\n')\n\n  if (!fs.existsSync(ARTIFACTS_DIR)) {\n    console.log('⚠️  No artifacts directory found')\n    return\n  }\n\n  const platformDirs = fs.readdirSync(ARTIFACTS_DIR)\n\n  for (const platformDir of platformDirs) {\n    const platformPath = path.join(ARTIFACTS_DIR, platformDir)\n    const stat = fs.statSync(platformPath)\n\n    if (!stat.isDirectory()) continue\n\n    console.log(`📦 Processing ${platformDir}...`)\n    const files = fs.readdirSync(platformPath)\n\n    for (const file of files) {\n      const srcPath = path.join(platformPath, file)\n      const destPath = path.join(DIST_DIR, file)\n      const fileStat = fs.statSync(srcPath)\n\n      // 跳过目录和 yml 文件（yml 文件会单独处理合并）\n      if (fileStat.isDirectory()) continue\n      if (file.endsWith('.yml')) continue\n\n      // 如果目标文件已存在且大小相同，跳过\n      if (fs.existsSync(destPath)) {\n        const destStat = fs.statSync(destPath)\n        if (destStat.size === fileStat.size) {\n          console.log(`   ⏭️  Skipped (exists): ${file}`)\n          continue\n        }\n      }\n\n      fs.copyFileSync(srcPath, destPath)\n      console.log(`   ✅ Copied: ${file}`)\n    }\n  }\n}\n\n/**\n * 合并 yml 文件\n */\nfunction mergeYmlFilesFromArtifacts() {\n  console.log('\\n🔀 Merging yml files...\\n')\n\n  for (const [outputName, sourceNames] of Object.entries(YML_MERGE_RULES)) {\n    const allFiles = []\n\n    for (const sourceName of sourceNames) {\n      const files = findFiles(ARTIFACTS_DIR, sourceName)\n      allFiles.push(...files)\n    }\n\n    if (allFiles.length === 0) {\n      console.log(`⏭️  No ${outputName} found, skipping...`)\n      continue\n    }\n\n    console.log(`📄 Found ${allFiles.length} ${outputName} file(s):`)\n    allFiles.forEach(f => console.log(`   - ${path.relative(ARTIFACTS_DIR, f)}`))\n\n    const merged = mergeYmlFiles(allFiles)\n\n    if (merged) {\n      const outputPath = path.join(DIST_DIR, outputName)\n      fs.writeFileSync(outputPath, yaml.dump(merged, { lineWidth: -1 }))\n      console.log(`✅ Merged -> ${outputName}`)\n\n      if (merged.files) {\n        console.log(`   Files: ${merged.files.map(f => f.url).join(', ')}`)\n      }\n      console.log('')\n    }\n  }\n}\n\nasync function main() {\n  console.log('🚀 Starting artifact merge process...\\n')\n\n  // 确保 dist 目录存在\n  if (!fs.existsSync(DIST_DIR)) {\n    fs.mkdirSync(DIST_DIR, { recursive: true })\n  }\n\n  // 1. 复制所有构建产物\n  copyArtifacts()\n\n  // 2. 合并 yml 文件\n  mergeYmlFilesFromArtifacts()\n\n  console.log('🎉 Artifact merge completed!')\n}\n\nmain().catch(err => {\n  console.error('❌ Error:', err)\n  process.exit(1)\n})\n"
  },
  {
    "path": "scripts/notarize.js",
    "content": "require('dotenv').config()\n\nconst { notarize } = require('@electron/notarize')\nconst { APPLE_ID, APPLE_TEAM_ID, APPLE_APP_SPECIFIC_PASSWORD } = process.env\nconst APP_BUNDLE_ID = 'com.molunerfinn.picgo'\n\nasync function main(context) {\n  const { electronPlatformName, appOutDir, packager } = context\n\n  if (\n    electronPlatformName !== 'darwin' ||\n    !APPLE_ID ||\n    !APPLE_APP_SPECIFIC_PASSWORD ||\n    !APPLE_TEAM_ID\n  ) {\n    console.log('Skip notarization.')\n    return\n  }\n\n  const appName = packager.appInfo.productFilename\n  const appPath = `${appOutDir}/${appName}.app`\n\n  const now = Date.now()\n\n  console.log('Starting Apple notarization for', appPath)\n\n  await notarize({\n    appPath,\n    appBundleId: APP_BUNDLE_ID,\n    appleId: APPLE_ID,\n    appleIdPassword: APPLE_APP_SPECIFIC_PASSWORD,\n    teamId: APPLE_TEAM_ID\n  })\n\n  console.log('Finished Apple notarization for', appPath, `in ${(Date.now() - now) / 1000}s`)\n}\n\n\nmodule.exports = main\n"
  },
  {
    "path": "scripts/update-win-yaml.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst crypto = require('crypto')\nconst yaml = require('js-yaml')\n\nconst distDir = path.join(__dirname, '../dist')\nconst yamlPath = path.join(distDir, 'latest.yml')\n\nif (!fs.existsSync(yamlPath)) {\n  console.log('⚠️ latest.yml not found in dist/. Skipping update.')\n  process.exit(0)\n}\n\nconsole.log(`Reading ${yamlPath}...`)\nconst yamlContent = fs.readFileSync(yamlPath, 'utf8')\nlet doc\n\ntry {\n  doc = yaml.load(yamlContent)\n} catch (e) {\n  console.error('❌ Failed to parse latest.yml:', e)\n  process.exit(1)\n}\n\n// Get all .exe files in dist directory\nconst files = fs.readdirSync(distDir).filter(f => f.endsWith('.exe'))\n\nif (files.length === 0) {\n  console.log('⚠️ No .exe files found in dist/.')\n  process.exit(0)\n}\n\nlet updated = false\n\nfiles.forEach(file => {\n  const filePath = path.join(distDir, file)\n  \n  // 1. Calculate new Hash and Size\n  const buffer = fs.readFileSync(filePath)\n  const hash = crypto.createHash('sha512').update(buffer).digest('base64')\n  const size = fs.statSync(filePath).size\n\n  console.log(`Processing ${file}:`)\n  console.log(`  -> New Hash: ${hash}`)\n  console.log(`  -> New Size: ${size}`)\n\n  // 2. Update entries in 'files' list\n  if (Array.isArray(doc.files)) {\n    const fileEntry = doc.files.find(f => f.url === file)\n    if (fileEntry) {\n      fileEntry.sha512 = hash\n      fileEntry.size = size\n      updated = true\n      console.log('  -> Updated file entry in yaml object')\n    }\n  }\n\n  // 3. Update root path entry (if exists)\n  // electron-builder's latest.yml usually has root path, sha512, size \n  // corresponding to the main file of current build (usually x64 or current arch)\n  if (doc.path === file) {\n    doc.sha512 = hash\n    doc.size = size \n    updated = true\n    console.log('  -> Updated root path entry in yaml object')\n  }\n})\n\nif (updated) {\n  // 4. Dump back to file\n  // lineWidth: -1 prevents long strings from wrapping, keeping it clean\n  const newYamlContent = yaml.dump(doc, { lineWidth: -1 })\n  fs.writeFileSync(yamlPath, newYamlContent, 'utf8')\n  console.log('✅ latest.yml updated successfully.')\n  console.log('New yaml content:')\n  console.log(newYamlContent)\n} else {\n  console.log('⚠️ No matching entries found in latest.yml to update.')\n}"
  },
  {
    "path": "scripts/upload-dist.js",
    "content": "// upload dist bundled-app to r2\n// upload version file to cos\n\nrequire('dotenv').config()\nconst fs = require('fs')\nconst pkg = require('../package.json')\nconst configList = require('./config')\nconst mime = require('mime-types')\nconst path = require('path')\nconst distPath = path.join(__dirname, '../dist')\nconst S3Client = require('@aws-sdk/client-s3').S3Client\nconst Upload = require('@aws-sdk/lib-storage').Upload\nconst uploadToDev = process.argv.includes('--dev')\n\nconst S3_BUCKET = 'release'\nconst S3_LEGACY_BUCKET = 'picgo'\nconst VERSION = pkg.version\nconst DEV_DIST_PREFIX = 'dev/'\nconst FILE_PATH =  uploadToDev ? `${DEV_DIST_PREFIX}${VERSION}/` : `${VERSION}/`\nconst S3_SECRET_ID = process.env.PICGO_ENV_S3_SECRET_ID\nconst S3_SECRET_KEY = process.env.PICGO_ENV_S3_SECRET_KEY\nconst S3_ACCOUNT_ID = process.env.PICGO_ENV_S3_ACCOUNT_ID\nconst S3_LEGACY_SECRET_ID = process.env.PICGO_ENV_S3_LEGACY_SECRET_ID\nconst S3_LEGACY_SECRET_KEY = process.env.PICGO_ENV_S3_LEGACY_SECRET_KEY\nconst S3_LEGACY_ACCOUNT_ID = process.env.PICGO_ENV_S3_LEGACY_ACCOUNT_ID\n\nconst S3Options = {\n  credentials: {\n    accessKeyId: S3_SECRET_ID,\n    secretAccessKey: S3_SECRET_KEY\n  },\n  endpoint: `https://${S3_ACCOUNT_ID}.r2.cloudflarestorage.com`,\n  sslEnabled: true,\n  region: 'auto'\n}\n\n// for legacy release file fetch\nconst S3LegacyOptions = {\n  credentials: {\n    accessKeyId: S3_LEGACY_SECRET_ID,\n    secretAccessKey: S3_LEGACY_SECRET_KEY\n  },\n  endpoint: `https://${S3_LEGACY_ACCOUNT_ID}.r2.cloudflarestorage.com`,\n  sslEnabled: true,\n  region: 'auto'\n}\n\n/**\n * 检查是否使用 --all 参数（上传所有平台）\n */\nfunction shouldUploadAll() {\n  return process.argv.includes('--all')\n}\n\n/**\n * 获取要上传的配置列表\n */\nfunction getUploadConfigs() {\n  if (shouldUploadAll()) {\n    // 合并所有平台的配置\n    return [\n      ...configList.darwin,\n      ...configList.win32,\n      ...configList.linux\n    ]\n  }\n  // 原有逻辑：根据当前平台\n  const platform = process.platform\n  return configList[platform] || []\n}\n\n/**\n * 上传单个文件到 S3\n */\nasync function uploadFileToS3(client, bucket, key, filePath, contentType = 'application/octet-stream') {\n  const upload = new Upload({\n    client,\n    params: {\n      Bucket: bucket,\n      Key: key,\n      Body: fs.createReadStream(filePath),\n      ContentType: contentType\n    }\n  })\n\n  upload.on('httpUploadProgress', progress => {\n    const percent = progress.total ? Math.round((progress.loaded / progress.total) * 100) : 0\n    process.stdout.write(`\\r   Progress: ${progress.loaded}/${progress.total || '?'} (${percent}%)`)\n  })\n\n  await upload.done()\n  console.log('') // 换行\n}\n\nconst uploadDist = async () => {\n  try {\n    const configs = getUploadConfigs()\n\n    if (configs.length === 0) {\n      console.warn('[PicGo] No upload config found!')\n      return\n    }\n\n    console.log(`[PicGo] Upload mode: ${shouldUploadAll() ? 'ALL PLATFORMS' : process.platform}`)\n    console.log(`[PicGo] Version: ${VERSION}`)\n    console.log(`[PicGo] Total files to upload: ${configs.length}\\n`)\n\n    const uploadedVersionFiles = new Set()\n    const client = new S3Client(S3Options)\n    const legacyClient = new S3Client(S3LegacyOptions)\n\n    for (const [index, config] of configs.entries()) {\n      const fileName = `${config.appNameWithPrefix}-${VERSION}-${config.arch}.${config.ext}`\n      const filePath = path.join(distPath, fileName)\n      let versionFileName = config['version-file']\n\n      console.log(`[${index + 1}/${configs.length}] Processing ${fileName}`)\n\n      // 上传构建产物\n      if (fs.existsSync(filePath)) {\n        console.log(`   Uploading to S3: ${FILE_PATH}${fileName}`)\n        await uploadFileToS3(client, S3_BUCKET, `${FILE_PATH}${fileName}`, filePath)\n        console.log(`   ✅ Uploaded: ${fileName}`)\n      } else {\n        console.warn(`   ⚠️  File not found: ${fileName}`)\n      }\n\n      let versionFilePath = path.join(distPath, versionFileName)\n      // Beta 版本使用不同的 yml 文件名\n      if (VERSION.toLowerCase().includes('beta') && fs.existsSync(versionFilePath)) {\n        versionFileName = versionFileName.replace('.yml', '.beta.yml')\n        const betaVersionFilePath = path.join(distPath, versionFileName)\n        // change to beta version file path\n        fs.renameSync(versionFilePath, betaVersionFilePath)\n        versionFilePath = betaVersionFilePath\n      }\n\n      // 上传版本文件（每个 yml 只上传一次）\n      if (!uploadedVersionFiles.has(versionFileName) && fs.existsSync(versionFilePath)) {\n        console.log(`   Uploading version file: ${versionFileName}`)\n        const versionFileNameFinal = uploadToDev ? `${DEV_DIST_PREFIX}${versionFileName}` : versionFileName\n\n        // 上传到主 bucket\n        await uploadFileToS3(\n          client,\n          S3_BUCKET,\n          versionFileNameFinal,\n          versionFilePath,\n          mime.lookup(versionFileName) || 'text/yaml'\n        )\n\n        // 上传到 legacy bucket\n        await uploadFileToS3(\n          legacyClient,\n          S3_LEGACY_BUCKET,\n          versionFileNameFinal,\n          versionFilePath,\n          mime.lookup(versionFileName) || 'text/yaml'\n        )\n\n        uploadedVersionFiles.add(versionFileName)\n        console.log(`   ✅ Version file uploaded: ${versionFileName}`)\n      }\n\n      console.log('')\n    }\n\n    console.log('[PicGo] 🎉 All uploads completed!')\n  } catch (e) {\n    console.error('[PicGo] ❌ Upload error:', e)\n    process.exit(1)\n  }\n}\n\nconst main = async () => {\n  await uploadDist()\n}\n\nmain()\n"
  },
  {
    "path": "src/__tests__/main/cloud-config-sync.spec.ts",
    "content": "import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\nimport type { IpcMainInvokeEvent } from 'electron'\nimport os from 'node:os'\nimport path from 'node:path'\nimport fs from 'fs-extra'\nimport { IRPCActionType } from '~/universal/types/enum'\nimport { IPicGoCloudConfigSyncToastType } from '#/types/cloudConfigSync'\n\ntype OnAskEncryptionSwitch = (context: { from: string; to: string }) => Promise<boolean>\n\ntype SyncImplementation = (askSwitch?: OnAskEncryptionSwitch) => Promise<{ status: string; message?: string }>\n\nconst showMessageBoxMock = vi.fn()\nconst showInputBoxMock = vi.fn()\nconst getUserInfoMock = vi.fn()\nconst getConfigMock = vi.fn()\nconst saveConfigMock = vi.fn()\nconst i18nTranslateMock = vi.fn((key: string) => key)\n\nlet baseDir = ''\nlet syncImplementation: SyncImplementation | null = null\n\nconst createInvokeEvent = (): IpcMainInvokeEvent => {\n  const event = {\n    sender: {\n      send: vi.fn()\n    }\n  }\n  return event as unknown as IpcMainInvokeEvent\n}\n\nvi.mock('@core/picgo', () => {\n  return {\n    default: {\n      get baseDir () {\n        return baseDir\n      },\n      getConfig: getConfigMock,\n      saveConfig: saveConfigMock,\n      cloud: {\n        getUserInfo: getUserInfoMock,\n        login: vi.fn(),\n        logout: vi.fn(),\n        disposeLoginFlow: vi.fn()\n      },\n      i18n: {\n        translate: i18nTranslateMock\n      }\n    }\n  }\n})\n\nvi.mock('apis/gui', () => {\n  return {\n    default: {\n      getInstance: () => ({\n        showInputBox: showInputBoxMock,\n        showMessageBox: showMessageBoxMock\n      })\n    }\n  }\n})\n\nvi.mock('apis/core/picgo/logger', () => {\n  return {\n    default: {\n      info: vi.fn(),\n      warn: vi.fn(),\n      error: vi.fn()\n    }\n  }\n})\n\nvi.mock('picgo', () => {\n  const SyncStatus = {\n    SUCCESS: 'success',\n    CONFLICT: 'conflict',\n    FAILED: 'failed'\n  }\n  const EncryptionMethod = {\n    AUTO: 'auto',\n    SSE: 'sse',\n    E2EE: 'e2ee'\n  }\n  const E2EAskPinReason = {\n    SETUP: 'setup',\n    DECRYPT: 'decrypt',\n    RETRY: 'retry'\n  }\n  const ConflictType = {\n    CONFLICT: 'conflict'\n  }\n\n  class ConfigSyncManager {\n    private readonly onAskEncryptionSwitch?: OnAskEncryptionSwitch\n\n    constructor (_ctx: unknown, options: { onAskEncryptionSwitch?: OnAskEncryptionSwitch } = {}) {\n      this.onAskEncryptionSwitch = options.onAskEncryptionSwitch\n    }\n\n    async sync (): Promise<{ status: string; message?: string }> {\n      if (syncImplementation) {\n        return syncImplementation(this.onAskEncryptionSwitch)\n      }\n      return { status: SyncStatus.SUCCESS }\n    }\n  }\n\n  return {\n    ConfigSyncManager,\n    ConflictType,\n    E2EAskPinReason,\n    EncryptionMethod,\n    SyncStatus\n  }\n})\n\ndescribe('config sync encryption switch confirmation (main)', () => {\n  beforeEach(async () => {\n    vi.clearAllMocks()\n    syncImplementation = null\n    getConfigMock.mockReturnValue(undefined)\n    getUserInfoMock.mockResolvedValue({ user: 'tester' })\n    i18nTranslateMock.mockImplementation((key: string) => key)\n    baseDir = await fs.mkdtemp(path.join(os.tmpdir(), 'picgo-gui-config-sync-'))\n  })\n\n  afterEach(async () => {\n    if (baseDir) {\n      await fs.remove(baseDir)\n    }\n  })\n\n  it('prompts for confirmation and proceeds on confirm', async () => {\n    showMessageBoxMock.mockResolvedValue({ result: 0, checkboxChecked: false })\n    syncImplementation = async (askSwitch) => {\n      if (askSwitch) {\n        const confirmed = await askSwitch({ from: 'e2ee', to: 'sse' })\n        if (!confirmed) {\n          return { status: 'failed', message: i18nTranslateMock('CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED') }\n        }\n      }\n      return { status: 'success', message: 'ok' }\n    }\n\n    const { cloudRouter } = await import('../../main/events/rpc/routes/cloud')\n    const handler = cloudRouter.routes().get(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_START)\n    const res = await handler?.([], createInvokeEvent())\n\n    expect(showMessageBoxMock).toHaveBeenCalledTimes(1)\n    expect(showMessageBoxMock).toHaveBeenCalledWith({\n      title: 'PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE',\n      message: 'PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY',\n      type: 'warning',\n      buttons: [\n        'PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM',\n        'PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL'\n      ]\n    })\n    expect(res?.success).toBe(true)\n    expect(res?.data.toastType).toBe(IPicGoCloudConfigSyncToastType.SUCCESS)\n    expect(res?.data.message).toBe('PICGO_CLOUD_CONFIG_SYNC_SUCCESS')\n  })\n\n  it('maps encryption-switch cancel to warning', async () => {\n    showMessageBoxMock.mockResolvedValue({ result: 1, checkboxChecked: false })\n    syncImplementation = async (askSwitch) => {\n      if (askSwitch) {\n        const confirmed = await askSwitch({ from: 'sse', to: 'e2ee' })\n        if (!confirmed) {\n          return { status: 'failed', message: i18nTranslateMock('CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED') }\n        }\n      }\n      return { status: 'success', message: 'ok' }\n    }\n\n    const { cloudRouter } = await import('../../main/events/rpc/routes/cloud')\n    const handler = cloudRouter.routes().get(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_START)\n    const res = await handler?.([], createInvokeEvent())\n\n    expect(showMessageBoxMock).toHaveBeenCalledTimes(1)\n    expect(res?.success).toBe(true)\n    expect(res?.data.toastType).toBe(IPicGoCloudConfigSyncToastType.WARNING)\n    expect(res?.data.message).toBe('PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED')\n  })\n})\n"
  },
  {
    "path": "src/__tests__/main/getLatestVersion.spec.ts",
    "content": "import { beforeEach, describe, expect, it, vi } from 'vitest'\nimport { RELEASE_URL, RELEASE_URL_BACKUP } from '#/utils/static'\nimport { getLatestVersion } from '~/main/utils/getLatestVersion'\n\nconst { axiosGetMock } = vi.hoisted(() => {\n  return {\n    axiosGetMock: vi.fn()\n  }\n})\n\nvi.mock('axios', () => {\n  return {\n    default: {\n      get: axiosGetMock\n    }\n  }\n})\n\ndescribe('main/utils/getLatestVersion', () => {\n  beforeEach(() => {\n    axiosGetMock.mockReset()\n  })\n\n  it('returns stable release when beta channel is older', async () => {\n    axiosGetMock.mockResolvedValueOnce({\n      data: [\n        { tag_name: 'v2.5.2', prerelease: false, draft: false },\n        { tag_name: 'v2.4.2-beta.0', prerelease: true, draft: false }\n      ]\n    })\n\n    const version = await getLatestVersion(true)\n\n    expect(version).toBe('2.5.2')\n    expect(axiosGetMock).toHaveBeenCalledWith(RELEASE_URL, {\n      headers: {\n        Referer: 'https://github.com'\n      }\n    })\n  })\n\n  it('returns prerelease when beta channel has a newer version', async () => {\n    axiosGetMock.mockResolvedValueOnce({\n      data: [\n        { tag_name: 'v2.5.2', prerelease: false, draft: false },\n        { tag_name: 'v2.6.0-beta.1', prerelease: true, draft: false }\n      ]\n    })\n\n    const version = await getLatestVersion(true)\n\n    expect(version).toBe('2.6.0-beta.1')\n  })\n\n  it('ignores prerelease when beta updates are disabled', async () => {\n    axiosGetMock.mockResolvedValueOnce({\n      data: [\n        { tag_name: 'v2.6.0-beta.1', prerelease: true, draft: false },\n        { tag_name: 'v2.5.2', prerelease: false, draft: false }\n      ]\n    })\n\n    const version = await getLatestVersion(false)\n\n    expect(version).toBe('2.5.2')\n  })\n\n  it('fallback compares stable and beta backup metadata when beta updates are enabled', async () => {\n    axiosGetMock.mockRejectedValueOnce(new Error('network down'))\n    axiosGetMock.mockResolvedValueOnce({\n      data: 'version: 2.5.2'\n    })\n    axiosGetMock.mockResolvedValueOnce({\n      data: 'version: 2.4.2-beta.0'\n    })\n\n    const version = await getLatestVersion(true)\n\n    expect(version).toBe('2.5.2')\n    expect(axiosGetMock).toHaveBeenNthCalledWith(2, `${RELEASE_URL_BACKUP}/latest.yml`, {\n      headers: {\n        Referer: 'https://github.com'\n      }\n    })\n    expect(axiosGetMock).toHaveBeenNthCalledWith(3, `${RELEASE_URL_BACKUP}/latest.beta.yml`, {\n      headers: {\n        Referer: 'https://github.com'\n      }\n    })\n  })\n\n  it('fallback uses only stable backup metadata when beta updates are disabled', async () => {\n    axiosGetMock.mockRejectedValueOnce(new Error('network down'))\n    axiosGetMock.mockResolvedValueOnce({\n      data: 'version: 2.5.2'\n    })\n\n    const version = await getLatestVersion(false)\n\n    expect(version).toBe('2.5.2')\n    expect(axiosGetMock).toHaveBeenCalledTimes(2)\n    expect(axiosGetMock).toHaveBeenNthCalledWith(2, `${RELEASE_URL_BACKUP}/latest.yml`, {\n      headers: {\n        Referer: 'https://github.com'\n      }\n    })\n  })\n})\n"
  },
  {
    "path": "src/__tests__/main/server.spec.ts",
    "content": "import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\nimport fs from 'fs-extra'\nimport os from 'node:os'\nimport path from 'node:path'\n\ntype ServerConfig = {\n  port: number | string\n  host: string\n  enable: boolean\n}\n\ntype HonoContextLike = {\n  req: {\n    raw: Request\n    formData: () => Promise<FormData>\n  }\n  json: (data: unknown, status?: number) => Response\n}\n\ntype UploadHandler = (c: HonoContextLike) => Promise<Response>\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => {\n  return typeof value === 'object' && value !== null\n}\n\nconst createJsonContext = (req: Request): HonoContextLike => {\n  return {\n    req: {\n      raw: req,\n      formData: async () => new FormData()\n    },\n    json: (data: unknown, status: number = 200) => {\n      return new Response(JSON.stringify(data), {\n        status,\n        headers: {\n          'content-type': 'application/json'\n        }\n      })\n    }\n  }\n}\n\nconst readJson = async (res: Response): Promise<any> => {\n  const text = await res.text()\n  try {\n    return JSON.parse(text)\n  } catch {\n    return text\n  }\n}\n\nlet serverConfig: ServerConfig | undefined\nlet registeredUploadHandler: UploadHandler | undefined\nlet formImageDir: string\n\nconst getConfigMock = vi.fn((key?: string) => {\n  if (key === 'settings.server') return serverConfig\n  return undefined\n})\n\nconst saveConfigMock = vi.fn((patch: unknown) => {\n  if (!isRecord(patch)) return\n  const next = patch['settings.server']\n  if (isRecord(next) && 'port' in next && 'host' in next && 'enable' in next) {\n    serverConfig = next as unknown as ServerConfig\n  }\n})\n\nconst registerPostMock = vi.fn((routePath: string, handler: unknown, isInternal?: boolean) => {\n  if (routePath === '/upload' && typeof handler === 'function') {\n    registeredUploadHandler = handler as unknown as UploadHandler\n  }\n  return { routePath, isInternal }\n})\n\nconst listenMock = vi.fn()\nconst shutdownMock = vi.fn()\n\nconst loggerMock = {\n  info: vi.fn(),\n  warn: vi.fn(),\n  error: vi.fn()\n}\n\nconst getAvailableWindowMock = vi.fn()\nconst uploadClipboardFilesMock = vi.fn()\nconst uploadSelectedFilesMock = vi.fn()\n\nconst dbPathDirMock = vi.fn(() => path.join(os.tmpdir(), 'picgo-gui-store'))\nconst getFormImageFolderPathMock = vi.fn(() => formImageDir)\n\nconst cleanupFormUploaderFilesMock = vi.fn((fileInfoList?: unknown) => {\n  if (!Array.isArray(fileInfoList)) return\n  for (const item of fileInfoList) {\n    if (typeof item === 'string') {\n      try {\n        fs.removeSync(item)\n      } catch {\n        // ignore\n      }\n    }\n  }\n})\n\nvi.mock('@core/picgo', () => {\n  return {\n    default: {\n      getConfig: getConfigMock,\n      saveConfig: saveConfigMock,\n      server: {\n        registerPost: registerPostMock,\n        listen: listenMock,\n        shutdown: shutdownMock\n      }\n    }\n  }\n})\n\nvi.mock('@core/picgo/logger', () => {\n  return { default: loggerMock }\n})\n\nvi.mock('apis/app/window/windowManager', () => {\n  return {\n    default: {\n      getAvailableWindow: getAvailableWindowMock\n    }\n  }\n})\n\nvi.mock('apis/app/uploader/apis', () => {\n  return {\n    uploadClipboardFiles: uploadClipboardFilesMock,\n    uploadSelectedFiles: uploadSelectedFilesMock\n  }\n})\n\nvi.mock('apis/core/datastore/dbChecker', () => {\n  return {\n    dbPathDir: dbPathDirMock,\n    getFormImageFolderPath: getFormImageFolderPathMock\n  }\n})\n\nvi.mock('~/main/utils/cleanupFormUploaderFiles', () => {\n  return {\n    cleanupFormUploaderFiles: cleanupFormUploaderFilesMock\n  }\n})\n\ndescribe('main/server (GUI adapter to picgo-core)', () => {\n  beforeEach(async () => {\n    serverConfig = undefined\n    registeredUploadHandler = undefined\n    formImageDir = await fs.mkdtemp(path.join(os.tmpdir(), 'picgo-gui-form-'))\n\n    vi.clearAllMocks()\n    vi.resetModules()\n  })\n\n  afterEach(async () => {\n    await fs.remove(formImageDir)\n  })\n\n  it('backfills default settings.server when missing', async () => {\n    serverConfig = undefined\n\n    await import('../../main/server')\n\n    expect(saveConfigMock).toHaveBeenCalledWith({\n      'settings.server': {\n        port: 36677,\n        host: '127.0.0.1',\n        enable: true\n      }\n    })\n    expect(serverConfig).toEqual({\n      port: 36677,\n      host: '127.0.0.1',\n      enable: true\n    })\n  })\n\n  it('does not listen when settings.server.enable is false', async () => {\n    serverConfig = { port: 36677, host: '127.0.0.1', enable: false }\n    const mod = await import('../../main/server')\n    const server = mod.default\n\n    server.startup()\n\n    expect(registerPostMock).not.toHaveBeenCalled()\n    expect(listenMock).not.toHaveBeenCalled()\n  })\n\n  it('registers internal /upload override and delegates listen/shutdown to picgo.server', async () => {\n    serverConfig = { port: '36677', host: '127.0.0.1', enable: true }\n    listenMock.mockResolvedValue(36677)\n\n    const mod = await import('../../main/server')\n    const server = mod.default\n\n    server.startup()\n\n    expect(registerPostMock).toHaveBeenCalledTimes(1)\n    expect(registerPostMock).toHaveBeenCalledWith('/upload', expect.any(Function), true)\n    expect(listenMock).toHaveBeenCalledWith(36677, '127.0.0.1')\n\n    server.shutdown()\n    expect(shutdownMock).toHaveBeenCalledTimes(1)\n  })\n\n  it('implements GUI-compatible /upload JSON semantics with core-style status codes', async () => {\n    serverConfig = { port: 36677, host: '127.0.0.1', enable: true }\n    uploadClipboardFilesMock.mockResolvedValue('https://a.example/clipboard.png')\n    uploadSelectedFilesMock.mockResolvedValue(['https://a.example/a.png'])\n    getAvailableWindowMock.mockReturnValue({ webContents: {} })\n\n    const mod = await import('../../main/server')\n    const server = mod.default\n    server.startup()\n\n    expect(registeredUploadHandler).toBeDefined()\n    const handler = registeredUploadHandler!\n\n    const resEmpty = await handler(createJsonContext(new Request('http://127.0.0.1/upload', { method: 'POST' })))\n    expect(resEmpty.status).toBe(200)\n    expect(await readJson(resEmpty)).toEqual({ success: true, result: ['https://a.example/clipboard.png'] })\n\n    const resObj = await handler(createJsonContext(new Request('http://127.0.0.1/upload', { method: 'POST', body: '{}' })))\n    expect(resObj.status).toBe(200)\n    expect(await readJson(resObj)).toEqual({ success: true, result: ['https://a.example/clipboard.png'] })\n\n    const resEmptyList = await handler(createJsonContext(new Request('http://127.0.0.1/upload', {\n      method: 'POST',\n      body: JSON.stringify({ list: [] })\n    })))\n    expect(resEmptyList.status).toBe(200)\n    expect(await readJson(resEmptyList)).toEqual({ success: true, result: ['https://a.example/clipboard.png'] })\n\n    const resList = await handler(createJsonContext(new Request('http://127.0.0.1/upload', {\n      method: 'POST',\n      body: JSON.stringify({ list: ['/a.png'] })\n    })))\n    expect(resList.status).toBe(200)\n    expect(await readJson(resList)).toEqual({ success: true, result: ['https://a.example/a.png'] })\n\n    const resInvalid = await handler(createJsonContext(new Request('http://127.0.0.1/upload', { method: 'POST', body: '{' })))\n    expect(resInvalid.status).toBe(400)\n    expect(await readJson(resInvalid)).toMatchObject({ success: false })\n  })\n\n  it('writes multipart files to fixed temp folder and always cleans up (success/failure)', async () => {\n    serverConfig = { port: 36677, host: '127.0.0.1', enable: true }\n    getAvailableWindowMock.mockReturnValue({ webContents: {} })\n\n    const mod = await import('../../main/server')\n    const server = mod.default\n    server.startup()\n\n    const handler = registeredUploadHandler!\n\n    const makeMultipartContext = async (): Promise<{ ctx: HonoContextLike; expectedPath: string }> => {\n      const fd = new FormData()\n      fd.append('files', new Blob([Buffer.from('hello')], { type: 'image/png' }), 'a.png')\n\n      const req = new Request('http://127.0.0.1/upload', {\n        method: 'POST',\n        headers: {\n          'content-type': 'multipart/form-data'\n        }\n      })\n      const expectedPath = path.join(formImageDir, 'a.png')\n      return {\n        ctx: {\n          req: {\n            raw: req,\n            formData: async () => fd\n          },\n          json: (data: unknown, status: number = 200) => new Response(JSON.stringify(data), { status })\n        },\n        expectedPath\n      }\n    }\n\n    // success case\n    uploadSelectedFilesMock.mockImplementation(async (_webContents: unknown, list: Array<{ path: string }>) => {\n      for (const item of list) {\n        expect(await fs.pathExists(item.path)).toBe(true)\n      }\n      return ['https://a.example/form.png']\n    })\n\n    const { ctx: ctxSuccess, expectedPath: pathSuccess } = await makeMultipartContext()\n    const resSuccess = await handler(ctxSuccess)\n    expect(resSuccess.status).toBe(200)\n    expect(await readJson(resSuccess)).toEqual({ success: true, result: ['https://a.example/form.png'] })\n    expect(await fs.pathExists(pathSuccess)).toBe(false)\n\n    // failure case still cleans up\n    uploadSelectedFilesMock.mockRejectedValueOnce(new Error('fail'))\n\n    const { ctx: ctxFail, expectedPath: pathFail } = await makeMultipartContext()\n    const resFail = await handler(ctxFail)\n    expect(resFail.status).toBe(500)\n    expect(await readJson(resFail)).toMatchObject({ success: false })\n    expect(await fs.pathExists(pathFail)).toBe(false)\n  })\n})\n\n"
  },
  {
    "path": "src/__tests__/renderer/store/appConfig.spec.ts",
    "content": "import { beforeEach, describe, expect, it, vi } from 'vitest'\nimport { createApp } from 'vue'\nimport type { IConfig } from 'picgo'\nimport { getConfig, getPicBeds } from '@/utils/dataSender'\nimport { store, storeKey, type IStore } from '@/store'\n\nvi.mock('@/utils/dataSender', () => {\n  return {\n    getConfig: vi.fn(),\n    getPicBeds: vi.fn(),\n    saveConfig: vi.fn()\n  }\n})\n\nconst buildStore = (): IStore => {\n  const app = createApp({})\n  store.install(app)\n  return app._context.provides[storeKey as symbol] as IStore\n}\n\ndescribe('renderer/store appConfig', () => {\n  const getConfigMock = vi.mocked(getConfig)\n  const getPicBedsMock = vi.mocked(getPicBeds)\n\n  beforeEach(() => {\n    vi.clearAllMocks()\n  })\n\n  it('refreshAppConfig updates appConfig and defaultPicBed', async () => {\n    const config: IConfig = {\n      picBed: {\n        uploader: 'github',\n        current: 'smms'\n      },\n      picgoPlugins: {}\n    }\n    getConfigMock.mockResolvedValue(config)\n\n    const storeInstance = buildStore()\n    await storeInstance.refreshAppConfig()\n\n    expect(storeInstance.state.appConfig).toStrictEqual(config)\n    expect(storeInstance.state.defaultPicBed).toBe('github')\n  })\n\n  it('refreshPicBeds updates picBeds', async () => {\n    const picBeds: IPicBedType[] = [\n      { type: 'smms', name: 'SM.MS', visible: true },\n      { type: 'github', name: 'GitHub', visible: true }\n    ]\n    getPicBedsMock.mockResolvedValue(picBeds)\n\n    const storeInstance = buildStore()\n    await storeInstance.refreshPicBeds()\n\n    expect(storeInstance.state.picBeds).toEqual(picBeds)\n  })\n})\n"
  },
  {
    "path": "src/__tests__/renderer/utils/dataSender.spec.ts",
    "content": "import { beforeEach, describe, expect, it, vi } from 'vitest'\nimport { saveConfig } from '@/utils/dataSender'\nimport { PICGO_SAVE_CONFIG } from '#/events/constants'\nimport { ipcRenderer } from 'electron'\n\nvi.mock('electron', () => {\n  return {\n    ipcRenderer: {\n      invoke: vi.fn(),\n      on: vi.fn(),\n      once: vi.fn(),\n      send: vi.fn(),\n      removeListener: vi.fn()\n    }\n  }\n})\n\ndescribe('renderer/utils/dataSender', () => {\n  const ipcRendererMock = vi.mocked(ipcRenderer)\n\n  beforeEach(() => {\n    vi.clearAllMocks()\n    ipcRendererMock.invoke.mockResolvedValue(true)\n  })\n\n  it('invokes save config IPC after saveConfig', async () => {\n    await saveConfig('settings.language', 'en')\n\n    expect(ipcRendererMock.invoke).toHaveBeenCalledWith(PICGO_SAVE_CONFIG, {\n      'settings.language': 'en'\n    })\n  })\n})\n"
  },
  {
    "path": "src/__tests__/universal/utils/common.spec.ts",
    "content": "import { describe, expect, it } from 'vitest'\n\nimport { extractHttpUrlsFromText, parseNewlineSeparatedUrls } from '../../../universal/utils/common'\n\ndescribe('universal/utils/common', () => {\n  describe('parseNewlineSeparatedUrls', () => {\n    it('parses newline-separated urls, trims lines, ignores empty lines, and de-duplicates', () => {\n      const input = [\n        'https://a.example/1.png',\n        '',\n        '  https://a.example/2.png  ',\n        'https://a.example/1.png',\n        'not-a-url'\n      ].join('\\n')\n\n      const { urls, invalidLines } = parseNewlineSeparatedUrls(input)\n\n      expect(urls).toEqual([\n        'https://a.example/1.png',\n        'https://a.example/2.png'\n      ])\n      expect(invalidLines).toEqual(['not-a-url'])\n    })\n\n    it('ignores uri-list comment lines when source is uri-list', () => {\n      const input = [\n        '# comment',\n        'https://a.example/1.png',\n        '# another comment',\n        'https://a.example/2.png'\n      ].join('\\n')\n\n      const { urls, invalidLines } = parseNewlineSeparatedUrls(input, { source: 'uri-list' })\n\n      expect(urls).toEqual([\n        'https://a.example/1.png',\n        'https://a.example/2.png'\n      ])\n      expect(invalidLines).toEqual([])\n    })\n\n    it('supports NUL-separated drag payloads by normalizing to newlines', () => {\n      const input = `https://a.example/1.png\\u0000https://a.example/2.png`\n      const { urls, invalidLines } = parseNewlineSeparatedUrls(input)\n\n      expect(urls).toEqual([\n        'https://a.example/1.png',\n        'https://a.example/2.png'\n      ])\n      expect(invalidLines).toEqual([])\n    })\n\n    it('splits concatenated urls in a single line as a fallback', () => {\n      const input = 'https://a.example/1.pnghttps://b.example/2.webphttps://c.example/3.png'\n      const { urls, invalidLines } = parseNewlineSeparatedUrls(input)\n\n      expect(urls).toEqual([\n        'https://a.example/1.png',\n        'https://b.example/2.webp',\n        'https://c.example/3.png'\n      ])\n      expect(invalidLines).toEqual([])\n    })\n\n    it('does not split embedded urls in query parameters', () => {\n      const input = 'https://example.com/?url=https://a.example/1.png'\n      const { urls, invalidLines } = parseNewlineSeparatedUrls(input)\n\n      expect(urls).toEqual(['https://example.com/?url=https://a.example/1.png'])\n      expect(invalidLines).toEqual([])\n    })\n  })\n\n  describe('extractHttpUrlsFromText', () => {\n    it('extracts urls from mixed text, strips trailing punctuation, and de-duplicates', () => {\n      const input = [\n        'hello',\n        'https://a.example/1.png)',\n        'https://b.example/2.webp]',\n        'https://a.example/1.png'\n      ].join(' ')\n\n      expect(extractHttpUrlsFromText(input)).toEqual([\n        'https://a.example/1.png',\n        'https://b.example/2.webp'\n      ])\n    })\n\n    it('returns empty array when no urls are present', () => {\n      expect(extractHttpUrlsFromText('no urls here')).toEqual([])\n    })\n  })\n})\n\n"
  },
  {
    "path": "src/background.ts",
    "content": "import { initStaticPath } from '~/main/utils/env'\nimport { bootstrap } from '~/main/lifeCycle'\n\ninitStaticPath()\nbootstrap.launchApp()\n\n/**\n * Auto Updater\n *\n * Uncomment the following code below and install `electron-updater` to\n * support auto updating. Code Signing with a valid certificate is required.\n * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating\n*/\n\n// import { autoUpdater } from 'electron-updater'\n\n// autoUpdater.on('update-downloaded', () => {\n//   autoUpdater.quitAndInstall()\n// })\n\n// app.on('ready', () => {\n//   if (process.env.NODE_ENV === 'production') {\n//     autoUpdater.checkForUpdates()\n//   }\n// })\n"
  },
  {
    "path": "src/main/apis/README.md",
    "content": "# apis folder\n\n## core\n\nThe lowest level APIs that are not dependent on each other. The upper APIs depend on them.\n\n## app\n\nProvide key API interfaces for PicGo application, including uploader, window management, shortcut key system, remotes handler, etc\n\n## gui\n\nGuiApi for PicGo plugins.\n"
  },
  {
    "path": "src/main/apis/app/remoteNotice/index.ts",
    "content": "// get notice from remote\n// such as some notices for users; some updates for users\nimport fs from 'fs-extra'\nimport { app, clipboard, dialog, shell } from 'electron'\nimport { IRemoteNoticeActionType, IRemoteNoticeTriggerCount, IRemoteNoticeTriggerHook } from '#/types/enum'\nimport { lte, gte } from 'semver'\nimport path from 'path'\n\nimport axios from 'axios'\nimport windowManager from '../window/windowManager'\nimport { showNotification } from '~/main/utils/common'\nimport { isDev } from '~/universal/utils/common'\nimport { STORE_PATH } from '~/main/utils/env'\n\n// for test\nconst REMOTE_NOTICE_URL = isDev ? 'http://localhost:8181/remote-notice.json' : 'https://release.picgo.app/remote-notice.yml'\n\nconst REMOTE_NOTICE_LOCAL_STORAGE_FILE = 'picgo-remote-notice.json'\n\nconst REMOTE_NOTICE_LOCAL_STORAGE_PATH = path.join(STORE_PATH, REMOTE_NOTICE_LOCAL_STORAGE_FILE)\n\nclass RemoteNoticeHandler {\n  private remoteNotice: IRemoteNotice | null = null\n  private remoteNoticeLocalCountStorage: IRemoteNoticeLocalCountStorage | null = null\n\n  async init () {\n    this.remoteNotice = await this.getRemoteNoticeInfo()\n    this.initLocalCountStorage()\n  }\n\n  private initLocalCountStorage () {\n    const localCountStorage = {}\n    if (!fs.existsSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH)) {\n      fs.writeFileSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, JSON.stringify({}))\n    }\n    try {\n      const localCountStorage: IRemoteNoticeLocalCountStorage = fs.readJSONSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, 'utf8')\n      this.remoteNoticeLocalCountStorage = localCountStorage\n    } catch (e) {\n      console.log(e)\n      this.remoteNoticeLocalCountStorage = localCountStorage\n    }\n  }\n\n  private saveLocalCountStorage (newData?: IRemoteNoticeLocalCountStorage) {\n    if (newData) {\n      this.remoteNoticeLocalCountStorage = newData\n    }\n    fs.writeFileSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, JSON.stringify(this.remoteNoticeLocalCountStorage))\n  }\n\n  private async getRemoteNoticeInfo (): Promise<IRemoteNotice | null> {\n    try {\n      const noticeInfo = await axios({\n        method: 'get',\n        url: REMOTE_NOTICE_URL,\n        responseType: 'json'\n      }).then(res => res.data) as IRemoteNotice\n      return noticeInfo\n    } catch {\n      return null\n    }\n  }\n\n  /**\n   * if the notice is not shown or is always shown, then show the notice\n   * @param action\n   */\n  private checkActionCount (action: IRemoteNoticeAction) {\n    try {\n      if (!this.remoteNoticeLocalCountStorage) {\n        return true\n      }\n      const actionCount = this.remoteNoticeLocalCountStorage[action.id]\n      if (actionCount === undefined) {\n        if (action.triggerCount === IRemoteNoticeTriggerCount.ALWAYS) {\n          this.remoteNoticeLocalCountStorage[action.id] = 1 // if always, count number\n        } else {\n          this.remoteNoticeLocalCountStorage[action.id] = true\n        }\n        return true\n      } else {\n        // here is the count of action\n        // if not always show, then can't show\n        if (action.triggerCount !== IRemoteNoticeTriggerCount.ALWAYS) {\n          return false\n        } else {\n          const preCount = this.remoteNoticeLocalCountStorage[action.id]\n          if (typeof preCount !== 'number') {\n            this.remoteNoticeLocalCountStorage[action.id] = true\n            return true\n          } else {\n            this.remoteNoticeLocalCountStorage[action.id] = preCount + 1\n          }\n          return true\n        }\n      }\n    } finally {\n      this.saveLocalCountStorage()\n    }\n  }\n\n  private async doActions (actions: IRemoteNoticeAction[]) {\n    for (const action of actions) {\n      if (this.checkActionCount(action)) {\n        switch (action.type) {\n          case IRemoteNoticeActionType.SHOW_DIALOG: {\n          // SHOW DIALOG\n            const currentWindow = windowManager.getAvailableWindow()\n            dialog.showOpenDialog(currentWindow, action.data?.options)\n            break\n          }\n          case IRemoteNoticeActionType.SHOW_NOTICE:\n            showNotification({\n              title: action.data?.title || '',\n              body: action.data?.content || '',\n              clickToCopy: !!action.data?.copyToClipboard,\n              copyContent: action.data?.copyToClipboard || '',\n              callback () {\n                if (action.data?.url) {\n                  shell.openExternal(action.data.url)\n                }\n              }\n            })\n            break\n          case IRemoteNoticeActionType.OPEN_URL:\n          // OPEN URL\n            shell.openExternal(action.data?.url || '')\n            break\n          case IRemoteNoticeActionType.COMMON:\n          // DO COMMON CASE\n            if (action.data?.copyToClipboard) {\n              clipboard.writeText(action.data.copyToClipboard)\n            }\n            if (action.data?.url) {\n              shell.openExternal(action.data.url)\n            }\n            break\n          case IRemoteNoticeActionType.SHOW_MESSAGE_BOX: {\n            const currentWindow = windowManager.getAvailableWindow()\n            dialog.showMessageBox(currentWindow, {\n              title: action.data?.title || '',\n              message: action.data?.content || '',\n              type: 'info',\n              buttons: action.data?.buttons?.map(item => item.label) || ['Yes']\n            }).then(res => {\n              const button = action.data?.buttons?.[res.response]\n              if (button?.type === 'cancel') {\n              // do nothing\n              } else {\n                if (button?.action) {\n                  this.doActions([button?.action])\n                }\n              }\n            })\n            break\n          }\n        }\n      }\n    }\n  }\n\n  triggerHook (hook: IRemoteNoticeTriggerHook) {\n    if (!this.remoteNotice || !this.remoteNotice.list) {\n      return\n    }\n    const actions = this.remoteNotice.list\n      .filter(item => {\n        if (item.versionMatch) {\n          switch (item.versionMatch) {\n            case 'exact':\n              return item.versions.includes(app.getVersion())\n            case 'gte':\n              return item.versions.some(version => {\n                // appVersion >= version\n                return gte(app.getVersion(), version)\n              })\n            case 'lte':\n              return item.versions.some(version => {\n                // appVersion <= version\n                return lte(app.getVersion(), version)\n              })\n          }\n        }\n        return item.versions.includes(app.getVersion())\n      })\n      .map(item => item.actions)\n      .reduce((pre, cur) => pre.concat(cur), [])\n      .filter(item => item.hooks.includes(hook))\n    this.doActions(actions)\n  }\n}\n\nconst remoteNoticeHandler = new RemoteNoticeHandler()\n\nexport {\n  remoteNoticeHandler\n}\n"
  },
  {
    "path": "src/main/apis/app/shortKey/builtin.ts",
    "content": "import { SHORTKEY_COMMAND_UPLOAD } from '../../core/bus/constants'\nimport { uploadClipboardFiles } from '../uploader/apis'\n\nexport const BuiltinShortKeyMap: Record<string, FN> = {\n  [SHORTKEY_COMMAND_UPLOAD]: uploadClipboardFiles\n}\n"
  },
  {
    "path": "src/main/apis/app/shortKey/shortKeyHandler.ts",
    "content": "import bus from '@core/bus'\nimport {\n  globalShortcut\n} from 'electron'\nimport logger from '@core/picgo/logger'\nimport GuiApi from '../../gui'\nimport { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'\nimport shortKeyService from './shortKeyService'\nimport picgo from '@core/picgo'\nimport { BuiltinShortKeyMap } from './builtin'\nimport { SHORTKEY_BUILTIN_PREFIX } from '../../core/bus/constants'\n\nclass ShortKeyHandler {\n  private isInModifiedMode: boolean = false\n  constructor () {\n    bus.on(TOGGLE_SHORTKEY_MODIFIED_MODE, flag => {\n      this.isInModifiedMode = flag\n    })\n  }\n\n  init () {\n    this.initBuiltInShortKey()\n    this.initPluginsShortKey()\n  }\n\n  private initBuiltInShortKey () {\n    const commands = picgo.getConfig<IShortKeyConfigs>('settings.shortKey') || {}\n    Object.keys(commands)\n      .filter(item => item.includes('picgo:'))\n      .forEach(command => {\n        const config = commands[command]\n        // if disabled, don't register #534\n        logger.info(`register builtin shortKey command: [${command}] - [${config.key}]`)\n        if (config.enable) {\n          logger.info(`register builtin shortKey command: [${command}] - [${config.key}] successfully`)\n          globalShortcut.register(config.key, () => {\n            this.handler(command)\n          })\n        } else {\n          logger.warn(`builtin shortKey command: [${command}] - [${config.key}] register failed, it's disabled`)\n        }\n      })\n  }\n\n  private initPluginsShortKey () {\n    // get enabled plugin\n    const pluginList = picgo.pluginLoader.getList()\n    for (const item of pluginList) {\n      const plugin = picgo.pluginLoader.getPlugin(item)\n      // if a plugin has commands\n      if (plugin && plugin.commands) {\n        if (typeof plugin.commands !== 'function') {\n          logger.warn(`${item}'s commands is not a function`)\n          continue\n        }\n        const commands = plugin.commands(picgo) as IPluginShortKeyConfig[]\n        for (const cmd of commands) {\n          const command = `${item}:${cmd.name}`\n          const commandConfig = picgo.getConfig<IShortKeyConfig | undefined>(`settings.shortKey.${command}`)\n          if (commandConfig) {\n            // if disabled, don't register #534\n            if (commandConfig.enable) {\n              this.registerShortKey(commandConfig, command, cmd.handle, false)\n            }\n          } else {\n            this.registerShortKey(cmd, command, cmd.handle, true)\n          }\n        }\n      } else {\n        continue\n      }\n    }\n  }\n\n  private registerShortKey (config: IShortKeyConfig | IPluginShortKeyConfig, command: string, handler: IShortKeyHandler, writeFlag: boolean) {\n    shortKeyService.registerCommand(command, handler)\n    if (config.key) {\n      globalShortcut.register(config.key, () => {\n        this.handler(command)\n      })\n    } else {\n      logger.warn(`${command} do not provide a key to bind`)\n    }\n    // if the config file already had this command\n    // then writeFlag -> false\n    if (writeFlag) {\n      picgo.saveConfig({\n        [`settings.shortKey.${command}`]: {\n          enable: true,\n          name: config.name,\n          label: config.label,\n          key: config.key\n        }\n      })\n    }\n  }\n\n  // enable or disable shortKey\n  bindOrUnbindShortKey (item: IShortKeyConfig, from: string): boolean {\n    const command = `${from}:${item.name}`\n    if (item.enable === false) {\n      globalShortcut.unregister(item.key)\n      picgo.saveConfig({\n        [`settings.shortKey.${command}.enable`]: false\n      })\n      return true\n    } else {\n      if (globalShortcut.isRegistered(item.key)) {\n        return false\n      } else {\n        picgo.saveConfig({\n          [`settings.shortKey.${command}.enable`]: true\n        })\n        globalShortcut.register(item.key, () => {\n          this.handler(command)\n        })\n        return true\n      }\n    }\n  }\n\n  // update shortKey bindings\n  updateShortKey (item: IShortKeyConfig, oldKey: string, from: string): boolean {\n    const command = `${from}:${item.name}`\n    if (globalShortcut.isRegistered(item.key)) return false\n    globalShortcut.unregister(oldKey)\n    picgo.saveConfig({\n      [`settings.shortKey.${command}.key`]: item.key\n    })\n    globalShortcut.register(item.key, () => {\n      this.handler(`${from}:${item.name}`)\n    })\n    return true\n  }\n\n  private async handler (command: string) {\n    if (this.isInModifiedMode) {\n      logger.warn(`in modified mode, ignore shortKey command: [${command}]`)\n      return\n    }\n    if (command.includes(SHORTKEY_BUILTIN_PREFIX)) {\n      const handler = BuiltinShortKeyMap[command]\n      if (handler) {\n        logger.info(`get builtin shortKey handler for command: [${command}]`)\n        return handler()\n      } else {\n        logger.warn(`can't find builtin shortKey handler for command: [${command}]`)\n      }\n    } else if (command.includes('picgo-plugin-')) {\n      const handler = shortKeyService.getShortKeyHandler(command)\n      if (handler) {\n        logger.info(`get plugin shortKey handler for command: [${command}]`)\n        return handler(picgo, GuiApi.getInstance())\n      }\n    } else {\n      logger.warn(`can not find command: [${command}]`)\n    }\n  }\n\n  registerPluginShortKey (pluginName: string) {\n    const plugin = picgo.pluginLoader.getPlugin(pluginName)\n    if (plugin && plugin.commands) {\n      if (typeof plugin.commands !== 'function') {\n        logger.warn(`${pluginName}'s commands is not a function`)\n        return\n      }\n      const commands = plugin.commands(picgo) as IPluginShortKeyConfig[]\n      for (const cmd of commands) {\n        const command = `${pluginName}:${cmd.name}`\n        const commandConfig = picgo.getConfig<IShortKeyConfig | undefined>(`settings.shortKey.${command}`)\n        if (commandConfig) {\n          this.registerShortKey(commandConfig, command, cmd.handle, false)\n        } else {\n          this.registerShortKey(cmd, command, cmd.handle, true)\n        }\n      }\n    }\n  }\n\n  unregisterPluginShortKey (pluginName: string) {\n    const commands = picgo.getConfig<IShortKeyConfigs>('settings.shortKey') || {}\n    const keyList = Object.keys(commands)\n      .filter(command => command.includes(pluginName))\n      .map(command => {\n        return {\n          command,\n          key: commands[command].key\n        }\n      }) as IKeyCommandType[]\n    keyList.forEach(item => {\n      globalShortcut.unregister(item.key)\n      shortKeyService.unregisterCommand(item.command)\n      picgo.removeConfig('settings.shortKey', item.command)\n    })\n  }\n}\n\nexport default new ShortKeyHandler()\n"
  },
  {
    "path": "src/main/apis/app/shortKey/shortKeyService.ts",
    "content": "import logger from '@core/picgo/logger'\nclass ShortKeyService {\n  private commandList: Map<string, IShortKeyHandler> = new Map()\n  registerCommand (command: string, handler: IShortKeyHandler) {\n    this.commandList.set(command, handler)\n  }\n\n  unregisterCommand (command: string) {\n    this.commandList.delete(command)\n  }\n\n  getShortKeyHandler (command: string): IShortKeyHandler | null {\n    const handler = this.commandList.get(command)\n    if (handler) return handler\n    logger.warn(`cannot find command: ${command}`)\n    return null\n  }\n\n  getCommandList () {\n    return [...this.commandList.keys()]\n  }\n}\n\nexport default new ShortKeyService()\n"
  },
  {
    "path": "src/main/apis/app/system/index.ts",
    "content": "import {\n  app,\n  Menu,\n  Tray,\n  dialog,\n  clipboard,\n  nativeTheme\n} from 'electron'\nimport uploader from 'apis/app/uploader'\nimport picgo from '@core/picgo'\nimport { GalleryDB } from '~/main/apis/core/datastore'\nimport windowManager from 'apis/app/window/windowManager'\nimport { IPasteStyle, IWindowList } from '#/types/enum'\nimport pasteTemplate from '~/main/utils/pasteTemplate'\nimport pkg from 'root/package.json'\nimport { ensureFilePath, getClipboardFilePathList, handleCopyUrl, showNotification } from '~/main/utils/common'\nimport { privacyManager } from '~/main/utils/privacyManager'\nimport { T } from '~/main/i18n'\nimport { isMacOSVersionGreaterThanOrEqualTo } from '~/main/utils/getMacOSVersion'\nimport { buildPicBedListMenu } from '~/main/events/remotes/picBedListMenu'\nimport { isLinux, isMacOS } from '~/universal/utils/common'\nimport { getStaticPath } from '#/utils/staticPath'\nlet contextMenu: Menu | null\nlet menu: Menu | null\nlet tray: Tray | null\n// need to build new menu\nexport function createContextMenu () {\n  if (process.platform === 'darwin' || process.platform === 'win32') {\n    const submenu = buildPicBedListMenu()\n    contextMenu = Menu.buildFromTemplate([\n      {\n        label: T('ABOUT'),\n        click () {\n          dialog.showMessageBox({\n            title: 'PicGo',\n            message: 'PicGo',\n            detail: `Version: ${pkg.version}\\nAuthor: Molunerfinn\\nGithub: https://github.com/Molunerfinn/PicGo`\n          })\n        }\n      },\n      {\n        label: T('OPEN_MAIN_WINDOW'),\n        click () {\n          const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)\n          settingWindow!.show()\n          settingWindow!.focus()\n          if (windowManager.has(IWindowList.MINI_WINDOW)) {\n            windowManager.get(IWindowList.MINI_WINDOW)!.hide()\n          }\n        }\n      },\n      {\n        label: T('CHOOSE_DEFAULT_PICBED'),\n        type: 'submenu',\n        // @ts-ignore\n        submenu\n      },\n      // @ts-ignore\n      {\n        label: T('OPEN_UPDATE_HELPER'),\n        type: 'checkbox',\n        checked: picgo.getConfig<boolean>('settings.showUpdateTip') !== false,\n        click () {\n          const value = picgo.getConfig<boolean>('settings.showUpdateTip') !== false\n          picgo.saveConfig({ 'settings.showUpdateTip': !value })\n        }\n      },\n      {\n        label: T('PRIVACY_TERMS_AGREEMENT'),\n        click () {\n          privacyManager.show(false)\n        }\n      },\n      {\n        label: T('RELOAD_APP'),\n        click () {\n          app.relaunch()\n          app.exit(0)\n        }\n      },\n      // @ts-ignore\n      {\n        role: 'quit',\n        label: T('QUIT')\n      }\n    ])\n  } else if (process.platform === 'linux') {\n    // TODO 图床选择功能\n    // 由于在Linux难以像在Mac和Windows上那样在点击时构造ContextMenu，\n    // 暂时取消这个选单，避免引起和设置中启用的图床不一致\n\n    // TODO 重启应用功能\n    // 目前的实现无法正常工作\n\n    contextMenu = Menu.buildFromTemplate([\n      {\n        label: T('OPEN_MAIN_WINDOW'),\n        click () {\n          const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)\n          settingWindow!.show()\n          settingWindow!.focus()\n          if (windowManager.has(IWindowList.MINI_WINDOW)) {\n            windowManager.get(IWindowList.MINI_WINDOW)!.hide()\n          }\n        }\n      },\n      // @ts-ignore\n      {\n        label: T('OPEN_UPDATE_HELPER'),\n        type: 'checkbox',\n        checked: picgo.getConfig<boolean>('settings.showUpdateTip') !== false,\n        click () {\n          const value = picgo.getConfig<boolean>('settings.showUpdateTip') !== false\n          picgo.saveConfig({ 'settings.showUpdateTip': !value })\n        }\n      },\n      {\n        label: T('ABOUT'),\n        click () {\n          dialog.showMessageBox({\n            title: 'PicGo',\n            message: 'PicGo',\n            buttons: ['Ok'],\n            detail: `Version: ${pkg.version}\\nAuthor: Molunerfinn\\nGithub: https://github.com/Molunerfinn/PicGo`\n          })\n        }\n      },\n      // @ts-ignore\n      {\n        role: 'quit',\n        label: T('QUIT')\n      }\n    ])\n  }\n  return contextMenu!\n}\n\nconst getTrayIcon = () => {\n  if (process.platform === 'darwin') {\n    const isMacOSGreaterThan11 = isMacOSVersionGreaterThanOrEqualTo('11')\n    return isMacOSGreaterThan11\n      ? getStaticPath('menubar-newdarwinTemplate.png')\n      : getStaticPath('menubar.png')\n  } else {\n    return getStaticPath('menubar-nodarwin.png')\n  }\n}\n\nexport function createTray () {\n  const menubarPic = getTrayIcon()\n  tray = new Tray(menubarPic)\n  // click事件在Mac和Windows上可以触发（在Ubuntu上无法触发，Unity不支持）\n  if (process.platform === 'darwin' || process.platform === 'win32') {\n    tray.on('right-click', () => {\n      if (windowManager.has(IWindowList.TRAY_WINDOW)) {\n        windowManager.get(IWindowList.TRAY_WINDOW)!.hide()\n      }\n      createContextMenu()\n      setTimeout(() => {\n        tray!.popUpContextMenu(contextMenu!)\n      }, 0)\n    })\n    tray.on('click', (event, bounds) => {\n      if (process.platform === 'darwin') {\n        toggleWindow(bounds)\n        setTimeout(async () => {\n          const obj: ImgInfo[] = []\n          const imgPathList = getClipboardFilePathList()\n          if (imgPathList.length > 0) {\n            for (const imgPath of imgPathList) {\n              const decodePath = ensureFilePath(imgPath)\n              obj.push({\n                imgUrl: decodePath\n              })\n            }\n          } else {\n            const img = clipboard.readImage()\n            if (!img.isEmpty()) {\n              const imgUrl = img.toDataURL()\n              obj.push({\n                width: img.getSize().width,\n                height: img.getSize().height,\n                imgUrl\n              })\n            }\n          }\n          windowManager\n            .get(IWindowList.TRAY_WINDOW)!\n            .webContents.send('clipboardFiles', obj)\n        }, 0)\n      } else {\n        if (windowManager.has(IWindowList.TRAY_WINDOW)) {\n          windowManager.get(IWindowList.TRAY_WINDOW)!.hide()\n        }\n        const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)\n        settingWindow!.show()\n        settingWindow!.focus()\n        if (windowManager.has(IWindowList.MINI_WINDOW)) {\n          windowManager.get(IWindowList.MINI_WINDOW)!.hide()\n        }\n      }\n    })\n\n    tray.on('drag-enter', () => {\n      if (nativeTheme.shouldUseDarkColors) {\n        tray!.setImage(getStaticPath('upload-dark.png'))\n      } else {\n        tray!.setImage(getStaticPath('upload.png'))\n      }\n    })\n\n    tray.on('drag-end', () => {\n      const menubarPic = getTrayIcon()\n      tray!.setImage(menubarPic)\n    })\n\n    // drop-files only be supported in macOS\n    // so the tray window must be available\n    tray.on('drop-files', async (event, files: string[]) => {\n      const pasteStyle = picgo.getConfig<IPasteStyle>('settings.pasteStyle') || 'markdown'\n      const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!\n      const imgs = await uploader\n        .setWebContents(trayWindow.webContents)\n        .upload(files)\n      if (imgs !== false) {\n        const pasteText: string[] = []\n        for (let i = 0; i < imgs.length; i++) {\n          pasteText.push(\n            pasteTemplate(pasteStyle, imgs[i], picgo.getConfig<string>('settings.customLink'))\n          )\n          setTimeout(() => {\n            showNotification({\n              title: T('UPLOAD_SUCCEED'),\n              body: imgs[i].imgUrl!\n              // icon: files[i]\n            })\n          }, i * 100)\n          await GalleryDB.getInstance().insert(imgs[i])\n        }\n        handleCopyUrl(pasteText.join('\\n'))\n        trayWindow.webContents.send('dragFiles', imgs)\n      }\n    })\n    // toggleWindow()\n  } else if (isLinux) {\n    // click事件在Ubuntu上无法触发，Unity不支持（在Mac和Windows上可以触发）\n    // 需要使用 setContextMenu 设置菜单\n    createContextMenu()\n    tray!.setContextMenu(contextMenu)\n  }\n}\n\nconst destroyTray = () => {\n  if (tray) {\n    tray.removeAllListeners()\n    tray.destroy()\n    tray = null\n  }\n  if (windowManager.has(IWindowList.TRAY_WINDOW)) {\n    windowManager.get(IWindowList.TRAY_WINDOW)!.hide()\n  }\n}\n\n/**\n * macOS only: show/hide the menubar (tray) icon.\n * For other platforms this keeps the existing behavior (always show tray).\n */\nexport function handleMenubarIcon (visible?: boolean) {\n  if (!isMacOS) {\n    if (!tray) {\n      createTray()\n    }\n    return\n  }\n  const shouldShow = visible !== undefined ? visible : (picgo.getConfig<boolean>('settings.showMenubarIcon') !== false)\n  if (shouldShow) {\n    if (!tray) {\n      createTray()\n    }\n  } else {\n    destroyTray()\n  }\n}\n\nexport function handleDockIcon () {\n  if (isMacOS) {\n    if (picgo.getConfig<boolean>('settings.showDockIcon') !== false) {\n      app.dock?.show()\n      app.dock?.setMenu(createContextMenu())\n    } else {\n      app.dock?.hide()\n    }\n  }\n}\n\nexport function createMenu () {\n  if (menu) {\n    return menu\n  }\n  if (process.env.NODE_ENV !== 'development') {\n    const template = [\n      {\n        label: 'Edit',\n        submenu: [\n          { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },\n          {\n            label: 'Redo',\n            accelerator: 'Shift+CmdOrCtrl+Z',\n            selector: 'redo:'\n          },\n          { type: 'separator' },\n          { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },\n          { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },\n          { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },\n          {\n            label: 'Select All',\n            accelerator: 'CmdOrCtrl+A',\n            selector: 'selectAll:'\n          },\n          {\n            label: 'Quit',\n            accelerator: 'CmdOrCtrl+Q',\n            click () {\n              app.quit()\n            }\n          }\n        ]\n      }\n    ]\n    // @ts-ignore\n    menu = Menu.buildFromTemplate(template)\n    Menu.setApplicationMenu(menu)\n  }\n}\n\nconst toggleWindow = (bounds: IBounds) => {\n  const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!\n  if (trayWindow.isVisible()) {\n    trayWindow.hide()\n  } else {\n    trayWindow.setPosition(bounds.x - 98 + 11, bounds.y, false)\n    trayWindow.webContents.send('updateFiles')\n    trayWindow.show()\n    trayWindow.focus()\n  }\n}\n"
  },
  {
    "path": "src/main/apis/app/uploader/apis.ts",
    "content": "import {\n  WebContents\n} from 'electron'\nimport windowManager from 'apis/app/window/windowManager'\nimport { IPasteStyle, IRPCActionType, IWindowList } from '#/types/enum'\nimport uploader from '.'\nimport pasteTemplate from '~/main/utils/pasteTemplate'\nimport { GalleryDB } from '~/main/apis/core/datastore'\nimport { handleCopyUrl, handleUrlEncodeWithSetting, showNotification } from '~/main/utils/common'\nimport { T } from '~/main/i18n/index'\nimport logger from '@core/picgo/logger'\nimport picgo from '@core/picgo'\n// import dayjs from 'dayjs'\n\nconst handleClipboardUploading = async (): Promise<false | ImgInfo[]> => {\n  const useBuiltinClipboard = !!picgo.getConfig<boolean>('settings.useBuiltinClipboard')\n  const win = windowManager.getAvailableWindow()\n  if (useBuiltinClipboard) {\n    return await uploader.setWebContents(win!.webContents).uploadWithBuildInClipboard()\n  }\n  return await uploader.setWebContents(win!.webContents).upload()\n}\n\nexport const uploadClipboardFiles = async (): Promise<string> => {\n  logger.info('upload clipboard file')\n  const img = await handleClipboardUploading()\n  if (img !== false) {\n    if (img.length > 0) {\n      const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)\n      const pasteStyle = picgo.getConfig<IPasteStyle>('settings.pasteStyle') || 'markdown'\n      handleCopyUrl(pasteTemplate(pasteStyle, img[0], picgo.getConfig<string>('settings.customLink')))\n      setTimeout(() => {\n        showNotification({\n          title: T('UPLOAD_SUCCEED'),\n          body: img[0].imgUrl!\n          // icon: img[0].imgUrl\n        })\n      }, 100)\n      await GalleryDB.getInstance().insert(img[0])\n      // trayWindow just be created in mac/windows, not in linux\n      trayWindow?.webContents?.send('clipboardFiles', [])\n      trayWindow?.webContents?.send('uploadFiles', img)\n      if (windowManager.has(IWindowList.SETTING_WINDOW)) {\n        windowManager.get(IWindowList.SETTING_WINDOW)!.webContents?.send(IRPCActionType.UPDATE_GALLERY)\n      }\n      return handleUrlEncodeWithSetting(img[0].imgUrl as string)\n    } else {\n      showNotification({\n        title: T('UPLOAD_FAILED'),\n        body: T('TIPS_UPLOAD_NOT_PICTURES')\n      })\n      return ''\n    }\n  } else {\n    return ''\n  }\n}\n\nexport const uploadSelectedFiles = async (webContents: WebContents, files: IFileWithPath[]): Promise<string[]> => {\n  const input = files.map(item => item.path)\n  const imgs = await uploader.setWebContents(webContents).upload(input)\n  const result = []\n  if (imgs !== false) {\n    const pasteStyle = picgo.getConfig<IPasteStyle>('settings.pasteStyle') || 'markdown'\n    const pasteText: string[] = []\n    for (let i = 0; i < imgs.length; i++) {\n      pasteText.push(pasteTemplate(pasteStyle, imgs[i], picgo.getConfig<string>('settings.customLink')))\n      setTimeout(() => {\n        showNotification({\n          title: T('UPLOAD_SUCCEED'),\n          body: imgs[i].imgUrl!\n          // icon: files[i].path\n        })\n      }, i * 100)\n      await GalleryDB.getInstance().insert(imgs[i])\n      result.push(handleUrlEncodeWithSetting(imgs[i].imgUrl!))\n    }\n    handleCopyUrl(pasteText.join('\\n'))\n    // trayWindow just be created in mac/windows, not in linux\n    windowManager.get(IWindowList.TRAY_WINDOW)?.webContents?.send('uploadFiles', imgs)\n    if (windowManager.has(IWindowList.SETTING_WINDOW)) {\n      windowManager.get(IWindowList.SETTING_WINDOW)!.webContents?.send(IRPCActionType.UPDATE_GALLERY)\n    }\n    return result\n  } else {\n    return []\n  }\n}\n"
  },
  {
    "path": "src/main/apis/app/uploader/index.ts",
    "content": "import {\n  BrowserWindow,\n  ipcMain,\n  WebContents,\n  clipboard\n} from 'electron'\nimport dayjs from 'dayjs'\nimport picgo from '@core/picgo'\nimport windowManager from 'apis/app/window/windowManager'\nimport { IWindowList } from '#/types/enum'\nimport util from 'util'\nimport type { IPicGo } from 'picgo'\nimport { showNotification, getClipboardFilePathList } from '~/main/utils/common'\nimport { GET_RENAME_FILE_NAME, RENAME_FILE_NAME } from '~/universal/events/constants'\nimport logger from '@core/picgo/logger'\nimport { T } from '~/main/i18n'\nimport fse from 'fs-extra'\nimport path from 'path'\nimport { privacyManager } from '~/main/utils/privacyManager'\nimport writeFile from 'write-file-atomic'\nimport { CLIPBOARD_IMAGE_FOLDER } from '~/universal/utils/static'\nimport { IpcMainEvent } from 'electron/main'\nimport { dataReportManager } from '~/main/utils/dataReport'\n\nconst waitForRename = (window: BrowserWindow, id: number): Promise<string|null> => {\n  return new Promise((resolve) => {\n    const windowId = window.id\n    ipcMain.once(`${RENAME_FILE_NAME}${id}`, (evt: IpcMainEvent, newName: string) => {\n      resolve(newName)\n      window.close()\n    })\n    window.on('close', () => {\n      resolve(null)\n      ipcMain.removeAllListeners(`${RENAME_FILE_NAME}${id}`)\n      windowManager.deleteById(windowId)\n    })\n  })\n}\n\nclass Uploader {\n  private webContents: WebContents | null = null\n  // private uploading: boolean = false\n  constructor () {\n    this.init()\n  }\n\n  init () {\n    picgo.on('notification', (message: IShowNotificationOption | undefined) => {\n      if (!message) return\n      showNotification(message)\n    })\n\n    picgo.on('uploadProgress', (progress: any) => {\n      this.webContents?.send('uploadProgress', progress)\n    })\n    picgo.on('beforeTransform', () => {\n      if (picgo.getConfig<boolean>('settings.uploadNotification')) {\n        showNotification({\n          title: T('UPLOAD_PROGRESS'),\n          body: T('UPLOADING')\n        })\n      }\n    })\n    picgo.helper.beforeUploadPlugins.register('renameFn', {\n      handle: async (ctx: IPicGo) => {\n        const rename = picgo.getConfig<boolean>('settings.rename')\n        const autoRename = picgo.getConfig<boolean>('settings.autoRename')\n        if (autoRename || rename) {\n          await Promise.all(ctx.output.map(async (item, index) => {\n            let name: undefined | string | null\n            let fileName: string | undefined\n            if (autoRename) {\n              fileName = dayjs().add(index, 'ms').format('YYYYMMDDHHmmssSSS') + item.extname\n            } else {\n              fileName = item.fileName\n            }\n            if (rename) {\n              const window = windowManager.create(IWindowList.RENAME_WINDOW)!\n              logger.info('wait for rename window ready...')\n              ipcMain.on(GET_RENAME_FILE_NAME, (evt) => {\n                if (evt.sender.id === window.webContents.id) {\n                  logger.info('rename window ready, wait for rename...')\n                  window.webContents.send(RENAME_FILE_NAME, fileName, item.fileName, window.webContents.id)\n                }\n              })\n              name = await waitForRename(window, window.webContents.id)\n            }\n            item.fileName = name || fileName\n          }))\n        }\n      }\n    })\n  }\n\n  setWebContents (webContents: WebContents) {\n    this.webContents = webContents\n    return this\n  }\n\n  /**\n   * use electron's clipboard image to upload\n   */\n  async uploadWithBuildInClipboard (): Promise<ImgInfo[]|false> {\n    let filePath = ''\n    try {\n      const imgPath = getClipboardFilePathList()\n      if (!imgPath.length) {\n        const nativeImage = clipboard.readImage()\n        if (nativeImage.isEmpty()) {\n          return false\n        }\n        const buffer = nativeImage.toPNG()\n        const baseDir = picgo.baseDir\n        const fileName = `${dayjs().format('YYYYMMDDHHmmssSSS')}.png`\n        filePath = path.join(baseDir, CLIPBOARD_IMAGE_FOLDER, fileName)\n        await writeFile(filePath, buffer)\n        return await this.upload([filePath])\n      } else {\n        return await this.upload(imgPath)\n      }\n    } catch (e: any) {\n      logger.error(e)\n      return false\n    } finally {\n      if (filePath) {\n        fse.unlink(filePath)\n      }\n    }\n  }\n\n  async upload (img?: IUploadOption): Promise<ImgInfo[]|false> {\n    try {\n      const privacyCheckRes = await privacyManager.check()\n      if (!privacyCheckRes) {\n        throw Error(T('PRIVACY_TIPS'))\n      }\n      const startTime = Date.now()\n      const output = await picgo.upload(img)\n      if (Array.isArray(output) && output.some((item: ImgInfo) => item.imgUrl)) {\n        if (this.webContents) {\n          dataReportManager.reportUploadData(this.webContents, {\n            fromClipboard: !img,\n            duration: Date.now() - startTime,\n            outputList: output\n          })\n        }\n        return output.filter(item => item.imgUrl)\n      } else {\n        return false\n      }\n    } catch (e: any) {\n      logger.error(e)\n      setTimeout(() => {\n        showNotification({\n          title: T('UPLOAD_FAILED'),\n          body: util.format(e.stack),\n          clickToCopy: true\n        })\n      }, 500)\n      return false\n    } finally {\n      ipcMain.removeAllListeners(GET_RENAME_FILE_NAME)\n    }\n  }\n}\n\nexport default new Uploader()\n"
  },
  {
    "path": "src/main/apis/app/window/constants.ts",
    "content": "import { buildRendererUrl } from '~/main/utils/env'\n\nexport const TRAY_WINDOW_URL = buildRendererUrl()\n\nexport const SETTING_WINDOW_URL = buildRendererUrl('main-page/upload')\n\nexport const MINI_WINDOW_URL = buildRendererUrl('mini-page')\n\nexport const RENAME_WINDOW_URL = buildRendererUrl('rename-page')\n\nexport const TOOLBOX_WINDOW_URL = buildRendererUrl('toolbox-page')\n"
  },
  {
    "path": "src/main/apis/app/window/windowList.ts",
    "content": "// import path from 'path'\nimport {\n  SETTING_WINDOW_URL,\n  TRAY_WINDOW_URL,\n  MINI_WINDOW_URL,\n  RENAME_WINDOW_URL,\n  TOOLBOX_WINDOW_URL\n} from './constants'\nimport { IStartupMode, IWindowList } from '#/types/enum'\nimport bus from '@core/bus'\nimport { CREATE_APP_MENU } from '@core/bus/constants'\nimport { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'\nimport { app } from 'electron'\nimport { T } from '~/main/i18n'\nimport { isLinux, isWindows } from '~/universal/utils/common'\nimport { getStaticPath } from '#/utils/staticPath'\nimport picgo from '@core/picgo'\n// import { URLSearchParams } from 'url'\n\nconst windowList = new Map<IWindowList, IWindowListItem>()\n\nconst defaultWebPreferences = {\n  // preload: path.join(__dirname, '../preload/index.js'),\n  nodeIntegration: true,\n  contextIsolation: false,\n  nodeIntegrationInWorker: true,\n  backgroundThrottling: false\n}\n\nconst handleWindowParams = (windowURL: string) => {\n  // const [baseURL, hash = ''] = windowURL.split('#')\n  // const search = new URLSearchParams()\n  // const lang = i18n.getLanguage()\n  // search.append('lang', lang)\n  // return `${baseURL}?${search.toString()}#${hash}`\n  return windowURL\n}\n\nexport const isWindowShouldShowOnStartup = (currentWindow: IWindowList) => {\n  const startupMode = picgo.getConfig<IStartupMode | undefined>('settings.startupMode') || (isLinux ? IStartupMode.SHOW_MINI_WINDOW : isWindows ? IStartupMode.SHOW_MAIN_WINDOW : IStartupMode.HIDE)\n  switch (currentWindow) {\n    case IWindowList.MINI_WINDOW: {\n      return startupMode === IStartupMode.SHOW_MINI_WINDOW\n    }\n    case IWindowList.SETTING_WINDOW: {\n      return startupMode === IStartupMode.SHOW_MAIN_WINDOW\n    }\n    default: {\n      return false\n    }\n  }\n}\n\nwindowList.set(IWindowList.TRAY_WINDOW, {\n  isValid: process.platform !== 'linux',\n  multiple: false,\n  options () {\n    return {\n      height: 350,\n      width: 196, // 196\n      show: false,\n      frame: false,\n      fullscreenable: false,\n      resizable: false,\n      transparent: true,\n      vibrancy: 'ultra-dark',\n      webPreferences: {\n        ...defaultWebPreferences,\n        webSecurity: false\n      }\n    }\n  },\n  callback (window) {\n    window.loadURL(handleWindowParams(TRAY_WINDOW_URL))\n    window.on('blur', () => {\n      window.hide()\n    })\n  }\n})\n\nwindowList.set(IWindowList.SETTING_WINDOW, {\n  isValid: true,\n  multiple: false,\n  options () {\n    const showDockIcon = picgo.getConfig<boolean>('settings.showDockIcon') !== false\n    const options: IBrowserWindowOptions = {\n      height: 450,\n      width: 800,\n      show: false,\n      frame: true,\n      center: true,\n      fullscreenable: false,\n      resizable: false,\n      title: 'PicGo',\n      transparent: true,\n      skipTaskbar: !showDockIcon,\n      titleBarStyle: 'hidden',\n      webPreferences: {\n        ...defaultWebPreferences,\n        webSecurity: false\n      }\n    }\n    if (process.platform !== 'darwin') {\n      options.show = false\n      options.frame = false\n      options.icon = getStaticPath('logo.png')\n      options.skipTaskbar = false\n    }\n    return options\n  },\n  callback (window, windowManager) {\n    window.loadURL(handleWindowParams(SETTING_WINDOW_URL))\n    window.on('closed', () => {\n      bus.emit(TOGGLE_SHORTKEY_MODIFIED_MODE, false)\n      if (process.platform === 'linux') {\n        process.nextTick(() => {\n          app.quit()\n        })\n      }\n    })\n    bus.emit(CREATE_APP_MENU)\n    windowManager.create(IWindowList.MINI_WINDOW)\n  }\n})\n\nwindowList.set(IWindowList.MINI_WINDOW, {\n  isValid: process.platform !== 'darwin',\n  multiple: false,\n  options () {\n    const obj: IBrowserWindowOptions = {\n      height: 64,\n      width: 64,\n      show: isLinux,\n      frame: false,\n      fullscreenable: false,\n      skipTaskbar: true,\n      resizable: false,\n      transparent: process.platform !== 'linux',\n      icon: getStaticPath('logo.png'),\n      webPreferences: {\n        ...defaultWebPreferences\n      }\n    }\n\n    if (picgo.getConfig<boolean>('settings.miniWindowOnTop')) {\n      obj.alwaysOnTop = true\n    }\n    return obj\n  },\n  callback (window) {\n    window.loadURL(handleWindowParams(MINI_WINDOW_URL))\n  }\n})\n\nwindowList.set(IWindowList.RENAME_WINDOW, {\n  isValid: true,\n  multiple: true,\n  options () {\n    const options: IBrowserWindowOptions = {\n      height: 175,\n      width: 300,\n      show: true,\n      fullscreenable: false,\n      resizable: false,\n      backgroundColor: 'rgba(26,40,42,0.9)',\n      webPreferences: {\n        ...defaultWebPreferences\n      }\n    }\n    if (process.platform !== 'darwin') {\n      options.show = true\n      options.autoHideMenuBar = true\n    }\n    return options\n  },\n  async callback (window, windowManager) {\n    window.loadURL(handleWindowParams(RENAME_WINDOW_URL))\n    const currentWindow = windowManager.getAvailableWindow()\n    if (currentWindow && currentWindow.isVisible()) {\n    // bounds: { x: 821, y: 75, width: 800, height: 450 }\n      const bounds = currentWindow.getBounds()\n      const positionX = bounds.x + bounds.width / 2 - 150\n      let positionY\n      // if is the settingWindow\n      if (bounds.height > 400) {\n        positionY = bounds.y + bounds.height / 2 - 88\n      } else { // if is the miniWindow\n        positionY = bounds.y + bounds.height / 2\n      }\n      window.setPosition(positionX, positionY, false)\n    }\n  }\n})\n\nwindowList.set(IWindowList.TOOLBOX_WINDOW, {\n  isValid: true,\n  multiple: false,\n  options () {\n    const options: IBrowserWindowOptions = {\n      height: 450,\n      width: 800,\n      show: false,\n      frame: true,\n      center: true,\n      fullscreenable: false,\n      resizable: false,\n      backgroundColor: 'rgba(26,40,42,0.9)',\n      title: `PicGo-${T('TOOLBOX')}`,\n      icon: getStaticPath('logo.png'),\n      webPreferences: {\n        ...defaultWebPreferences,\n        webSecurity: false\n      }\n    }\n    if (process.platform !== 'darwin') {\n      options.autoHideMenuBar = true\n    }\n    return options\n  },\n  async callback (window, windowManager) {\n    window.loadURL(TOOLBOX_WINDOW_URL)\n    const currentWindow = windowManager.getAvailableWindow()\n    if (currentWindow && currentWindow.isVisible()) {\n    // bounds: { x: 821, y: 75, width: 800, height: 450 }\n      const bounds = currentWindow.getBounds()\n      const positionX = bounds.x + bounds.width / 2 - 400\n      let positionY\n      // if is the settingWindow\n      if (bounds.height > 400) {\n        positionY = bounds.y + bounds.height / 2 - 225\n      } else { // if is the miniWindow\n        positionY = bounds.y + bounds.height / 2\n      }\n      window.setPosition(positionX, positionY, false)\n    }\n  }\n})\n\nexport default windowList\n"
  },
  {
    "path": "src/main/apis/app/window/windowManager.ts",
    "content": "import {\n  BrowserWindow\n} from 'electron'\nimport windowList from './windowList'\nimport { IWindowList } from '#/types/enum'\n\nclass WindowManager implements IWindowManager {\n  private windowMap: Map<IWindowList | string, BrowserWindow> = new Map()\n  private windowIdMap: Map<number, IWindowList | string> = new Map()\n  create (name: IWindowList) {\n    const windowConfig: IWindowListItem = windowList.get(name)!\n    if (windowConfig.isValid) {\n      if (!windowConfig.multiple) {\n        if (this.has(name)) return this.windowMap.get(name)!\n      }\n      const window = new BrowserWindow(windowConfig.options())\n      const id = window.id\n      if (windowConfig.multiple) {\n        this.windowMap.set(`${name}_${window.id}`, window)\n        this.windowIdMap.set(window.id, `${name}_${window.id}`)\n      } else {\n        this.windowMap.set(name, window)\n        this.windowIdMap.set(window.id, name)\n      }\n      windowConfig.callback(window, this)\n      // https://github.com/electron/electron/issues/1594\n      window.on('page-title-updated', (evt) => {\n        evt.preventDefault()\n      })\n      window.on('close', () => {\n        this.deleteById(id)\n      })\n      return window\n    } else {\n      return null\n    }\n  }\n\n  get (name: IWindowList) {\n    if (this.has(name)) {\n      return this.windowMap.get(name)!\n    } else {\n      const window = this.create(name)\n      return window\n    }\n  }\n\n  has (name: IWindowList) {\n    return this.windowMap.has(name)\n  }\n\n  // useless\n  // delete (name: IWindowList) {\n  //   const window = this.windowMap.get(name)\n  //   if (window) {\n  //     this.windowIdMap.delete(window.id)\n  //     this.windowMap.delete(name)\n  //   }\n  // }\n  deleteById = (id: number) => {\n    const name = this.windowIdMap.get(id)\n    if (name) {\n      this.windowMap.delete(name)\n      this.windowIdMap.delete(id)\n    }\n  }\n\n  getAvailableWindow () {\n    const miniWindow = this.windowMap.get(IWindowList.MINI_WINDOW)\n    if (miniWindow && miniWindow.isVisible()) {\n      return miniWindow\n    } else {\n      const settingWindow = this.windowMap.get(IWindowList.SETTING_WINDOW)\n      const trayWindow = this.windowMap.get(IWindowList.TRAY_WINDOW)\n      return settingWindow || trayWindow || this.create(IWindowList.SETTING_WINDOW)!\n    }\n  }\n}\n\nexport default new WindowManager()\n"
  },
  {
    "path": "src/main/apis/core/bus/apis.ts",
    "content": "import bus from '.'\nimport {\n  UPLOAD_WITH_FILES,\n  UPLOAD_WITH_FILES_RESPONSE,\n  UPLOAD_WITH_CLIPBOARD_FILES,\n  UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE,\n  GET_WINDOW_ID,\n  GET_WINDOW_ID_RESPONSE,\n  GET_SETTING_WINDOW_ID,\n  GET_SETTING_WINDOW_ID_RESPONSE\n} from './constants'\n\nexport const uploadWithClipboardFiles = (): Promise<{\n  success: boolean,\n  result?: string[]\n}> => {\n  return new Promise((resolve) => {\n    bus.once(UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE, (result: string) => {\n      if (result) {\n        return resolve({\n          success: true,\n          result: [result]\n        })\n      } else {\n        return resolve({\n          success: false\n        })\n      }\n    })\n    bus.emit(UPLOAD_WITH_CLIPBOARD_FILES)\n  })\n}\n\nexport const uploadWithFiles = (pathList: IFileWithPath[]): Promise<{\n  success: boolean,\n  result?: string[]\n}> => {\n  return new Promise((resolve) => {\n    bus.once(UPLOAD_WITH_FILES_RESPONSE, (result: string[]) => {\n      if (result.length) {\n        return resolve({\n          success: true,\n          result\n        })\n      } else {\n        return resolve({\n          success: false\n        })\n      }\n    })\n    bus.emit(UPLOAD_WITH_FILES, pathList)\n  })\n}\n\n// get available window id:\n// miniWindow or settingWindow or trayWindow\nexport const getWindowId = (): Promise<number> => {\n  return new Promise((resolve) => {\n    bus.once(GET_WINDOW_ID_RESPONSE, (id: number) => {\n      resolve(id)\n    })\n    bus.emit(GET_WINDOW_ID)\n  })\n}\n\n// get settingWindow id:\nexport const getSettingWindowId = (): Promise<number> => {\n  return new Promise((resolve) => {\n    bus.once(GET_SETTING_WINDOW_ID_RESPONSE, (id: number) => {\n      resolve(id)\n    })\n    bus.emit(GET_SETTING_WINDOW_ID)\n  })\n}\n"
  },
  {
    "path": "src/main/apis/core/bus/constants.ts",
    "content": "export const GET_WINDOW_ID = 'GET_WINDOW_ID' // get a current window\nexport const GET_WINDOW_ID_RESPONSE = 'GET_WINDOW_ID_RESPONSE'\nexport const GET_SETTING_WINDOW_ID = 'GET_SETTING_WINDOW_ID' // get setting window\nexport const GET_SETTING_WINDOW_ID_RESPONSE = 'GET_SETTING_WINDOW_ID_RESPONSE'\nexport const UPLOAD_WITH_FILES = 'UPLOAD_WITH_FILES'\nexport const UPLOAD_WITH_FILES_RESPONSE = 'UPLOAD_WITH_FILES_RESPONSE'\nexport const UPLOAD_WITH_CLIPBOARD_FILES = 'UPLOAD_WITH_CLIPBOARD_FILES'\nexport const UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE = 'UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE'\nexport const CREATE_APP_MENU = 'CREATE_APP_MENU'\n\n// shortkey command\nexport const SHORTKEY_BUILTIN_PREFIX = 'picgo:'\nexport const SHORTKEY_COMMAND_UPLOAD = `${SHORTKEY_BUILTIN_PREFIX}upload`\n"
  },
  {
    "path": "src/main/apis/core/bus/index.ts",
    "content": "import { EventEmitter } from 'events'\n\nconst bus = new EventEmitter()\n\nexport default bus\n"
  },
  {
    "path": "src/main/apis/core/datastore/dbChecker.ts",
    "content": "import fs from 'fs-extra'\nimport writeFile from 'write-file-atomic'\nimport path from 'path'\nimport { getLogger } from '@core/utils/localLogger'\nimport dayjs from 'dayjs'\nimport { T } from '~/main/i18n'\nimport { FORM_IMAGE_FOLDER } from '~/universal/utils/static'\nimport { STORE_PATH } from '~/main/utils/env'\nconst configFilePath = path.join(STORE_PATH, 'data.json')\nconst configFileBackupPath = path.join(STORE_PATH, 'data.bak.json')\nexport const defaultConfigPath = configFilePath\nlet _configFilePath = ''\nlet hasCheckPath = false\n\nconst errorMsg = {\n  broken: T('TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT'),\n  brokenButBackup: T('TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP')\n}\n\n/** ensure notification list */\nif (!global.notificationList) global.notificationList = []\n\nfunction dbChecker () {\n  if (process.type !== 'renderer') {\n    // db save bak\n    try {\n      const { dbPath, dbBackupPath } = getGalleryDBPath()\n      if (fs.existsSync(dbPath)) {\n        fs.copyFileSync(dbPath, dbBackupPath)\n      }\n    } catch (e) {\n      console.error(e)\n    }\n\n    const configFilePath = dbPathChecker()\n    if (!fs.existsSync(configFilePath)) {\n      return\n    }\n    let configFile: string = '{}'\n    const optionsTpl = {\n      title: T('TIPS_NOTICE'),\n      body: ''\n    }\n    // config save bak\n    try {\n      configFile = fs.readFileSync(configFilePath, { encoding: 'utf-8' })\n      JSON.parse(configFile)\n    } catch (e) {\n      fs.unlinkSync(configFilePath)\n      if (fs.existsSync(configFileBackupPath)) {\n        try {\n          configFile = fs.readFileSync(configFileBackupPath, { encoding: 'utf-8' })\n          JSON.parse(configFile)\n          writeFile.sync(configFilePath, configFile, { encoding: 'utf-8' })\n          const stats = fs.statSync(configFileBackupPath)\n          optionsTpl.body = `${errorMsg.brokenButBackup}\\n${T('TIPS_PICGO_BACKUP_FILE_VERSION', {\n            v: dayjs(stats.mtime).format('YYYY-MM-DD HH:mm:ss')\n          })}`\n          global.notificationList?.push(optionsTpl)\n          return\n        } catch (e) {\n          optionsTpl.body = errorMsg.broken\n          global.notificationList?.push(optionsTpl)\n          return\n        }\n      }\n      optionsTpl.body = errorMsg.broken\n      global.notificationList?.push(optionsTpl)\n      return\n    }\n    writeFile.sync(configFileBackupPath, configFile, { encoding: 'utf-8' })\n  }\n}\n\n/**\n * Get config path\n */\nfunction dbPathChecker (): string {\n  if (_configFilePath) {\n    return _configFilePath\n  }\n  // defaultConfigPath\n  _configFilePath = defaultConfigPath\n  // if defaultConfig path is not exit\n  // do not parse the content of config\n  if (!fs.existsSync(defaultConfigPath)) {\n    return _configFilePath\n  }\n  try {\n    const configString = fs.readFileSync(defaultConfigPath, { encoding: 'utf-8' })\n    const config = JSON.parse(configString)\n    const userConfigPath: string = config.configPath || ''\n    if (userConfigPath) {\n      if (fs.existsSync(userConfigPath) && userConfigPath.endsWith('.json')) {\n        _configFilePath = userConfigPath\n        return _configFilePath\n      }\n    }\n    return _configFilePath\n  } catch (e) {\n    const picgoLogPath = path.join(STORE_PATH, 'picgo-gui-local.log')\n    const logger = getLogger(picgoLogPath)\n    if (!hasCheckPath) {\n      const optionsTpl = {\n        title: T('TIPS_NOTICE'),\n        body: T('TIPS_CUSTOM_CONFIG_FILE_PATH_ERROR')\n      }\n      global.notificationList?.push(optionsTpl)\n      hasCheckPath = true\n    }\n    logger('error', e)\n    _configFilePath = defaultConfigPath\n    return _configFilePath\n  }\n}\n\nfunction dbPathDir () {\n  return path.dirname(dbPathChecker())\n}\n\nfunction getGalleryDBPath (): {\n  dbPath: string\n  dbBackupPath: string\n} {\n  const configPath = dbPathChecker()\n  const dbPath = path.join(path.dirname(configPath), 'picgo.db')\n  const dbBackupPath = path.join(path.dirname(dbPath), 'picgo.bak.db')\n  return {\n    dbPath,\n    dbBackupPath\n  }\n}\n\nfunction getFormImageFolderPath (): string {\n  const STORE_PATH = dbPathDir()\n  const formImagesPath = path.join(STORE_PATH, FORM_IMAGE_FOLDER)\n  if (!fs.existsSync(formImagesPath)) {\n    fs.mkdirSync(formImagesPath, { recursive: true })\n  }\n  return formImagesPath\n}\n\nexport {\n  dbChecker,\n  dbPathChecker,\n  dbPathDir,\n  getGalleryDBPath,\n  getFormImageFolderPath\n}\n"
  },
  {
    "path": "src/main/apis/core/datastore/index.ts",
    "content": "import path from 'path'\nimport fs from 'fs-extra'\nimport { DBStore } from '@picgo/store'\nimport { getGalleryDBPath } from './dbChecker'\n\nconst DB_PATH: string = getGalleryDBPath().dbPath\nfs.ensureDirSync(path.dirname(DB_PATH))\n\nclass GalleryDB {\n  private static instance: DBStore\n  private constructor () {}\n\n  public static getInstance (): DBStore {\n    if (!GalleryDB.instance) {\n      GalleryDB.instance = new DBStore(DB_PATH, 'gallery')\n    }\n    return GalleryDB.instance\n  }\n}\n\nexport {\n  GalleryDB\n}\n"
  },
  {
    "path": "src/main/apis/core/picgo/index.ts",
    "content": "import { dbChecker, dbPathChecker } from 'apis/core/datastore/dbChecker'\nimport { shell } from 'electron'\nimport pkg from 'root/package.json'\nimport { PicGo } from 'picgo'\n\nconst CONFIG_PATH = dbPathChecker()\n\ndbChecker()\n\nconst picgo = new PicGo(CONFIG_PATH)\npicgo.saveConfig({\n  debug: true,\n  PICGO_ENV: 'GUI'\n})\n\nglobal.PICGO_GUI_VERSION = pkg.version\npicgo.GUI_VERSION = global.PICGO_GUI_VERSION\n\npicgo.openUrl = (url: string) => {\n  return shell.openExternal(url)\n}\n\n// const originPicGoSaveConfig = picgo.saveConfig.bind(picgo)\n\n// function flushDB () {\n//   db.flush()\n// }\n\n// const debounced = debounce(flushDB, 1000)\n\n// picgo.saveConfig = (config: IStringKeyMap) => {\n//   originPicGoSaveConfig(config)\n//   // flush electron's db\n//   debounced()\n// }\n\nexport default picgo\n"
  },
  {
    "path": "src/main/apis/core/picgo/logger.ts",
    "content": "import picgo from '.'\n\nconst logger = picgo.log\n\nexport default logger\n"
  },
  {
    "path": "src/main/apis/core/utils/localLogger.ts",
    "content": "import fs from 'fs-extra'\nimport dayjs from 'dayjs'\nimport util from 'util'\n\nconst checkLogFileIsLarge = (logPath: string): {\n  isLarge: boolean\n  logFileSize?: number\n  logFileSizeLimit?: number\n} => {\n  try {\n    if (fs.existsSync(logPath)) {\n      const logFileSize = fs.statSync(logPath).size\n      const logFileSizeLimit = 10 * 1024 * 1024 // 10 MB default\n      return {\n        isLarge: logFileSize > logFileSizeLimit,\n        logFileSize,\n        logFileSizeLimit\n      }\n    }\n    return {\n      isLarge: false\n    }\n  } catch (e) {\n    // why throw error???\n    console.log(e)\n    return {\n      isLarge: true\n    }\n  }\n}\n\nconst recreateLogFile = (logPath: string): void => {\n  try {\n    if (fs.existsSync(logPath)) {\n      fs.unlinkSync(logPath)\n      fs.createFileSync(logPath)\n    }\n  } catch (e) {\n    // do nothing\n  }\n}\n\n/**\n * for local log before picgo inited\n */\nconst getLogger = (logPath: string) => {\n  let hasUncathcedError = false\n  try {\n    if (!fs.existsSync(logPath)) {\n      fs.ensureFileSync(logPath)\n    }\n    if (checkLogFileIsLarge(logPath).isLarge) {\n      recreateLogFile(logPath)\n    }\n  } catch (e) {\n    console.log(e)\n    hasUncathcedError = true\n  }\n  return (type: string, ...msg: any[]) => {\n    if (hasUncathcedError) {\n      if (checkLogFileIsLarge(logPath).isLarge) {\n        recreateLogFile(logPath)\n      }\n      return\n    }\n    try {\n      let log = `${dayjs().format('YYYY-MM-DD HH:mm:ss')} [PicGo ${type.toUpperCase()}] `\n      msg.forEach((item: ILogArgvTypeWithError) => {\n        if (typeof item === 'object' && type === 'error') {\n          log += `\\n------Error Stack Begin------\\n${util.format(item.stack)}\\n-------Error Stack End------- `\n        } else {\n          if (typeof item === 'object') {\n            item = JSON.stringify(item)\n          }\n          log += `${item} `\n        }\n      })\n      log += '\\n'\n      console.log(log)\n      // A synchronized approach to avoid log msg sequence errors\n      if (checkLogFileIsLarge(logPath).isLarge) {\n        recreateLogFile(logPath)\n      }\n      if (!hasUncathcedError) {\n        fs.appendFileSync(logPath, log)\n      }\n    } catch (e) {\n      console.log(e)\n      hasUncathcedError = true\n    }\n  }\n}\n\nexport {\n  getLogger\n}\n"
  },
  {
    "path": "src/main/apis/gui/index.ts",
    "content": "import {\n  dialog,\n  BrowserWindow,\n  ipcMain\n} from 'electron'\nimport picgo from '@core/picgo'\nimport { GalleryDB } from 'apis/core/datastore'\nimport { dbPathChecker, defaultConfigPath, getGalleryDBPath } from 'apis/core/datastore/dbChecker'\nimport uploader from 'apis/app/uploader'\nimport pasteTemplate from '~/main/utils/pasteTemplate'\nimport { handleCopyUrl, showNotification as showMainNotification } from '~/main/utils/common'\nimport {\n  getWindowId,\n  getSettingWindowId\n} from '@core/bus/apis'\nimport {\n  SHOW_INPUT_BOX\n} from '~/universal/events/constants'\nimport { DBStore } from '@picgo/store'\nimport { T } from '~/main/i18n'\nimport { IPasteStyle, IRPCActionType } from '~/universal/types/enum'\n\nclass GuiApi implements IGuiApi {\n  private static instance: GuiApi\n  private windowId: number = -1\n  private settingWindowId: number = -1\n  private constructor () {\n    console.log('init gui api')\n  }\n\n  public static getInstance (): GuiApi {\n    if (!GuiApi.instance) {\n      GuiApi.instance = new GuiApi()\n    }\n    return GuiApi.instance\n  }\n\n  private async showSettingWindow () {\n    this.settingWindowId = await getSettingWindowId()\n    const settingWindow = BrowserWindow.fromId(this.settingWindowId)\n    if (settingWindow?.isVisible()) {\n      return true\n    }\n    settingWindow?.show()\n    return new Promise<void>((resolve) => {\n      setTimeout(() => {\n        resolve()\n      }, 1000) // TODO: a better way to wait page loaded.\n    })\n  }\n\n  private getWebContentsByWindowId (id: number) {\n    return BrowserWindow.fromId(id)?.webContents\n  }\n\n  async showInputBox (options: IShowInputBoxOption = {\n    title: '',\n    placeholder: ''\n  }) {\n    await this.showSettingWindow()\n    this.getWebContentsByWindowId(this.settingWindowId)?.send(SHOW_INPUT_BOX, options)\n    return new Promise<string>((resolve) => {\n      ipcMain.once(SHOW_INPUT_BOX, (event, value: string) => {\n        resolve(value)\n      })\n    })\n  }\n\n  async showFileExplorer (options: IShowFileExplorerOption = {}) {\n    this.windowId = await getWindowId()\n    const res = await dialog.showOpenDialog(BrowserWindow.fromId(this.windowId)!, options)\n    return res.filePaths || []\n  }\n\n  async upload (input: IUploadOption) {\n    this.windowId = await getWindowId()\n    const webContents = this.getWebContentsByWindowId(this.windowId)\n    const imgs = await uploader.setWebContents(webContents!).upload(input)\n    if (imgs !== false) {\n      const pasteStyle = picgo.getConfig<IPasteStyle>('settings.pasteStyle') || 'markdown'\n      const pasteText: string[] = []\n      for (let i = 0; i < imgs.length; i++) {\n        pasteText.push(pasteTemplate(pasteStyle, imgs[i], picgo.getConfig<string>('settings.customLink')))\n        setTimeout(() => {\n          showMainNotification({\n            title: T('UPLOAD_SUCCEED'),\n            body: imgs[i].imgUrl as string\n            // icon: imgs[i].imgUrl\n          })\n        }, i * 100)\n        await GalleryDB.getInstance().insert(imgs[i])\n      }\n      handleCopyUrl(pasteText.join('\\n'))\n      webContents?.send('uploadFiles', imgs)\n      webContents?.send(IRPCActionType.UPDATE_GALLERY)\n      return imgs\n    }\n    return []\n  }\n\n  showNotification (options: IShowNotificationOption = {\n    title: '',\n    body: ''\n  }) {\n    showMainNotification(options)\n  }\n\n  showMessageBox (options: IShowMessageBoxOption = {\n    title: '',\n    message: '',\n    type: 'info',\n    buttons: ['Yes', 'No']\n  }) {\n    return new Promise<IShowMessageBoxResult>(async (resolve) => {\n      this.windowId = await getWindowId()\n      dialog.showMessageBox(\n        BrowserWindow.fromId(this.windowId)!,\n        options\n      ).then((res) => {\n        resolve({\n          result: res.response,\n          checkboxChecked: res.checkboxChecked\n        })\n      })\n    })\n  }\n\n  /**\n   * v2.4.0+\n   * @param options\n   */\n  async showConfigDialog<T extends IStringKeyMap> (options: IPicGoPluginShowConfigDialogOption) {\n    await this.showSettingWindow()\n    this.getWebContentsByWindowId(this.settingWindowId)?.send(IRPCActionType.OPEN_CONFIG_DIALOG, options)\n    return new Promise<T | false>((resolve) => {\n      ipcMain.once(IRPCActionType.OPEN_CONFIG_DIALOG, (event, value: T | false) => {\n        resolve(value)\n      })\n    })\n  }\n\n  /**\n   * get picgo config/data path\n   */\n  async getConfigPath () {\n    const currentConfigPath = dbPathChecker()\n    const galleryDBPath = getGalleryDBPath().dbPath\n    return {\n      defaultConfigPath,\n      currentConfigPath,\n      galleryDBPath\n    }\n  }\n\n  get galleryDB (): DBStore {\n    return new Proxy<DBStore>(GalleryDB.getInstance(), {\n      get (target, prop: keyof DBStore) {\n        if (prop === 'overwrite') {\n          return new Proxy(GalleryDB.getInstance().overwrite, {\n            apply (target, ctx, args) {\n              return new Promise((resolve) => {\n                const guiApi = GuiApi.getInstance()\n                guiApi.showMessageBox({\n                  title: T('TIPS_WARNING'),\n                  message: T('TIPS_PLUGIN_REMOVE_GALLERY_ITEM'),\n                  type: 'info',\n                  buttons: ['Yes', 'No']\n                }).then(res => {\n                  if (res.result === 0) {\n                    resolve(Reflect.apply(target, ctx, args))\n                  } else {\n                    resolve(undefined)\n                  }\n                })\n              })\n            }\n          })\n        }\n        if (prop === 'removeById') {\n          return new Proxy(GalleryDB.getInstance().removeById, {\n            apply (target, ctx, args) {\n              return new Promise((resolve) => {\n                const guiApi = GuiApi.getInstance()\n                guiApi.showMessageBox({\n                  title: T('TIPS_WARNING'),\n                  message: T('TIPS_PLUGIN_REMOVE_GALLERY_ITEM'),\n                  type: 'info',\n                  buttons: ['Yes', 'No']\n                }).then(res => {\n                  if (res.result === 0) {\n                    resolve(Reflect.apply(target, ctx, args))\n                  } else {\n                    resolve(undefined)\n                  }\n                })\n              })\n            }\n          })\n        }\n        return Reflect.get(target, prop)\n      }\n    })\n  }\n}\n\nexport default GuiApi\n"
  },
  {
    "path": "src/main/events/busEventList.ts",
    "content": "import bus from '@core/bus'\nimport {\n  uploadClipboardFiles,\n  uploadSelectedFiles\n} from 'apis/app/uploader/apis'\nimport {\n  createMenu\n} from 'apis/app/system'\nimport { IWindowList } from '#/types/enum'\nimport windowManager from 'apis/app/window/windowManager'\nimport {\n  UPLOAD_WITH_FILES,\n  UPLOAD_WITH_FILES_RESPONSE,\n  UPLOAD_WITH_CLIPBOARD_FILES,\n  UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE,\n  GET_WINDOW_ID,\n  GET_WINDOW_ID_RESPONSE,\n  GET_SETTING_WINDOW_ID,\n  GET_SETTING_WINDOW_ID_RESPONSE,\n  CREATE_APP_MENU\n} from '@core/bus/constants'\nfunction initEventCenter () {\n  const eventList: any = {\n    [UPLOAD_WITH_CLIPBOARD_FILES]: busCallUploadClipboardFiles,\n    [UPLOAD_WITH_FILES]: busCallUploadFiles,\n    [GET_WINDOW_ID]: busCallGetWindowId,\n    [GET_SETTING_WINDOW_ID]: busCallGetSettingWindowId,\n    [CREATE_APP_MENU]: createMenu\n  }\n  for (const i in eventList) {\n    bus.on(i, eventList[i])\n  }\n}\n\nasync function busCallUploadClipboardFiles () {\n  const imgUrl = await uploadClipboardFiles()\n  bus.emit(UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE, imgUrl)\n}\n\nasync function busCallUploadFiles (pathList: IFileWithPath[]) {\n  const win = windowManager.getAvailableWindow()\n  const urls = await uploadSelectedFiles(win.webContents, pathList)\n  bus.emit(UPLOAD_WITH_FILES_RESPONSE, urls)\n}\n\nfunction busCallGetWindowId () {\n  const win = windowManager.getAvailableWindow()\n  bus.emit(GET_WINDOW_ID_RESPONSE, win.id)\n}\n\nfunction busCallGetSettingWindowId () {\n  const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!\n  bus.emit(GET_SETTING_WINDOW_ID_RESPONSE, settingWindow.id)\n}\n\nexport default {\n  listen () {\n    initEventCenter()\n  }\n}\n"
  },
  {
    "path": "src/main/events/ipcList.ts",
    "content": "import {\n  app,\n  ipcMain,\n  shell,\n  IpcMainEvent,\n  BrowserWindow\n} from 'electron'\nimport windowManager from 'apis/app/window/windowManager'\nimport { IPasteStyle, IRPCActionType, IWindowList } from '#/types/enum'\nimport uploader from 'apis/app/uploader'\nimport pasteTemplate from '~/main/utils/pasteTemplate'\nimport picgo from '@core/picgo'\nimport logger from '@core/picgo/logger'\nimport { GalleryDB } from '~/main/apis/core/datastore'\nimport server from '~/main/server'\nimport getPicBeds from '~/main/utils/getPicBeds'\nimport shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'\nimport bus from '@core/bus'\nimport {\n  TOGGLE_SHORTKEY_MODIFIED_MODE,\n  OPEN_DEVTOOLS,\n  SHOW_MINI_PAGE_MENU,\n  MINIMIZE_WINDOW,\n  CLOSE_WINDOW,\n  SHOW_MAIN_PAGE_MENU,\n  SHOW_UPLOAD_PAGE_MENU,\n  OPEN_USER_STORE_FILE,\n  OPEN_URL,\n  SHOW_PLUGIN_PAGE_MENU,\n  SET_MINI_WINDOW_POS,\n  GET_PICBEDS,\n  LOG_INVALID_URL_LINES\n} from '#/events/constants'\nimport {\n  uploadClipboardFiles,\n  uploadSelectedFiles\n} from '~/main/apis/app/uploader/apis'\nimport picgoCoreIPC from './picgoCoreIPC'\nimport { handleCopyUrl, showNotification } from '~/main/utils/common'\nimport { buildMainPageMenu, buildMiniPageMenu, buildPluginPageMenu, buildPicBedListMenu } from './remotes/menu'\nimport path from 'path'\nimport { T } from '~/main/i18n'\nimport { STORE_PATH } from '~/main/utils/env'\n\nexport default {\n  listen () {\n    picgoCoreIPC.listen()\n    // from macOS tray\n    ipcMain.on('uploadClipboardFiles', async () => {\n      const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!\n      // macOS use builtin clipboard is OK\n      const img = await uploader.setWebContents(trayWindow.webContents).uploadWithBuildInClipboard()\n      if (img !== false) {\n        const pasteStyle = picgo.getConfig<IPasteStyle>('settings.pasteStyle') || 'markdown'\n        handleCopyUrl(pasteTemplate(pasteStyle, img[0], picgo.getConfig<string>('settings.customLink')))\n        showNotification({\n          title: T('UPLOAD_SUCCEED'),\n          body: img[0].imgUrl!\n          // icon: file[0]\n          // icon: img[0].imgUrl\n        })\n        await GalleryDB.getInstance().insert(img[0])\n        trayWindow.webContents.send('clipboardFiles', [])\n        if (windowManager.has(IWindowList.SETTING_WINDOW)) {\n          windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send(IRPCActionType.UPDATE_GALLERY)\n        }\n      }\n      trayWindow.webContents.send('uploadFiles')\n    })\n\n    ipcMain.on('uploadClipboardFilesFromUploadPage', () => {\n      console.log('handle')\n      uploadClipboardFiles()\n    })\n\n    ipcMain.on('uploadChoosedFiles', async (evt: IpcMainEvent, files: IFileWithPath[]) => {\n      return uploadSelectedFiles(evt.sender, files)\n    })\n\n    ipcMain.on('updateShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, oldKey: string, from: string) => {\n      const result = shortKeyHandler.updateShortKey(item, oldKey, from)\n      evt.sender.send('updateShortKeyResponse', result)\n      if (result) {\n        showNotification({\n          title: T('OPERATION_SUCCEED'),\n          body: T('TIPS_SHORTCUT_MODIFIED_SUCCEED')\n        })\n      } else {\n        showNotification({\n          title: T('OPERATION_FAILED'),\n          body: T('TIPS_SHORTCUT_MODIFIED_CONFLICT')\n        })\n      }\n    })\n\n    ipcMain.on('bindOrUnbindShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, from: string) => {\n      const result = shortKeyHandler.bindOrUnbindShortKey(item, from)\n      if (result) {\n        showNotification({\n          title: T('OPERATION_SUCCEED'),\n          body: T('TIPS_SHORTCUT_MODIFIED_SUCCEED')\n        })\n      } else {\n        showNotification({\n          title: T('OPERATION_FAILED'),\n          body: T('TIPS_SHORTCUT_MODIFIED_CONFLICT')\n        })\n      }\n    })\n\n    ipcMain.on('updateCustomLink', () => {\n      showNotification({\n        title: T('OPERATION_SUCCEED'),\n        body: T('TIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED')\n      })\n    })\n\n    ipcMain.on('autoStart', (evt: IpcMainEvent, val: boolean) => {\n      app.setLoginItemSettings({\n        openAtLogin: val\n      })\n    })\n\n    ipcMain.on('openSettingWindow', () => {\n      windowManager.get(IWindowList.SETTING_WINDOW)!.show()\n      if (windowManager.has(IWindowList.MINI_WINDOW)) {\n        windowManager.get(IWindowList.MINI_WINDOW)!.hide()\n      }\n    })\n\n    ipcMain.on('openMiniWindow', () => {\n      const miniWindow = windowManager.get(IWindowList.MINI_WINDOW)!\n      const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!\n\n      if (picgo.getConfig<boolean>('settings.miniWindowOnTop')) {\n        miniWindow.setAlwaysOnTop(true)\n      }\n\n      miniWindow.show()\n      miniWindow.focus()\n      settingWindow.hide()\n    })\n\n    //  from mini window\n    ipcMain.on('syncPicBed', () => {\n      if (windowManager.has(IWindowList.SETTING_WINDOW)) {\n        windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')\n      }\n    })\n\n    ipcMain.on(GET_PICBEDS, (evt: IpcMainEvent) => {\n      const picBeds = getPicBeds()\n      evt.sender.send(GET_PICBEDS, picBeds)\n      evt.returnValue = picBeds\n    })\n\n    ipcMain.on(TOGGLE_SHORTKEY_MODIFIED_MODE, (evt: IpcMainEvent, val: boolean) => {\n      bus.emit(TOGGLE_SHORTKEY_MODIFIED_MODE, val)\n    })\n\n    ipcMain.on('updateServer', () => {\n      server.restart()\n    })\n    ipcMain.on(OPEN_DEVTOOLS, (event: IpcMainEvent) => {\n      event.sender.openDevTools({\n        mode: 'detach'\n      })\n    })\n    // menu & window methods\n    ipcMain.on(SHOW_MINI_PAGE_MENU, () => {\n      const window = windowManager.get(IWindowList.MINI_WINDOW)!\n      const menu = buildMiniPageMenu()\n      menu.popup({\n        window\n      })\n    })\n    ipcMain.on(SHOW_MAIN_PAGE_MENU, () => {\n      const window = windowManager.get(IWindowList.SETTING_WINDOW)!\n      const menu = buildMainPageMenu(window)\n      menu.popup({\n        window\n      })\n    })\n    ipcMain.on(SHOW_UPLOAD_PAGE_MENU, () => {\n      const window = windowManager.get(IWindowList.SETTING_WINDOW)!\n      const menu = buildPicBedListMenu()\n      menu.popup({\n        window\n      })\n    })\n    ipcMain.on(SHOW_PLUGIN_PAGE_MENU, (evt: IpcMainEvent, plugin: IPicGoPlugin) => {\n      const window = windowManager.get(IWindowList.SETTING_WINDOW)!\n      const menu = buildPluginPageMenu(plugin)\n      menu.popup({\n        window\n      })\n    })\n    ipcMain.on(MINIMIZE_WINDOW, () => {\n      const window = BrowserWindow.getFocusedWindow()\n      window?.minimize()\n    })\n    ipcMain.on(CLOSE_WINDOW, () => {\n      const window = BrowserWindow.getFocusedWindow()\n      if (process.platform === 'linux') {\n        window?.hide()\n      } else {\n        window?.close()\n      }\n    })\n    ipcMain.on(OPEN_USER_STORE_FILE, (evt: IpcMainEvent, filePath: string) => {\n      const abFilePath = path.join(STORE_PATH, filePath)\n      shell.openPath(abFilePath)\n    })\n    ipcMain.on(OPEN_URL, (evt: IpcMainEvent, url: string) => {\n      shell.openExternal(url)\n    })\n    ipcMain.on(SET_MINI_WINDOW_POS, (evt: IpcMainEvent, pos: IMiniWindowPos) => {\n      const window = BrowserWindow.getFocusedWindow()\n      window?.setBounds(pos)\n    })\n\n    ipcMain.on(LOG_INVALID_URL_LINES, (_evt: IpcMainEvent, lines: string[]) => {\n      if (!Array.isArray(lines) || !lines.length) return\n      lines.forEach((line, index) => {\n        logger.warn(`[Batch URL Upload] invalid url line #${index + 1}: ${line}`)\n      })\n    })\n  },\n  dispose () {}\n}\n"
  },
  {
    "path": "src/main/events/picgoCoreIPC.ts",
    "content": "import path from 'path'\nimport GuiApi from 'apis/gui'\nimport {\n  dialog,\n  shell,\n  IpcMainEvent,\n  ipcMain,\n  clipboard\n} from 'electron'\nimport fs from 'fs-extra'\nimport { IPasteStyle, IPicGoHelperType, IWindowList } from '#/types/enum'\nimport shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'\nimport picgo from '@core/picgo'\nimport { handleStreamlinePluginName, simpleClone } from '~/universal/utils/common'\nimport { IGuiMenuItem, PicGo as PicGoCore } from 'picgo'\nimport windowManager from 'apis/app/window/windowManager'\nimport { showNotification } from '~/main/utils/common'\nimport { dbPathChecker } from 'apis/core/datastore/dbChecker'\nimport {\n  PICGO_SAVE_CONFIG,\n  PICGO_GET_CONFIG,\n  PICGO_GET_DB,\n  PICGO_INSERT_DB,\n  PICGO_INSERT_MANY_DB,\n  PICGO_UPDATE_BY_ID_DB,\n  PICGO_GET_BY_ID_DB,\n  PICGO_REMOVE_BY_ID_DB,\n  PICGO_OPEN_FILE,\n  PASTE_TEXT,\n  OPEN_WINDOW,\n  GET_LANGUAGE_LIST,\n  SET_CURRENT_LANGUAGE,\n  GET_CURRENT_LANGUAGE,\n  GET_PICBED_CONFIG\n} from '#/events/constants'\n\nimport { GalleryDB } from 'apis/core/datastore'\nimport { IObject, IFilter } from '@picgo/store/dist/types'\nimport pasteTemplate from '../utils/pasteTemplate'\nimport { i18nManager, T } from '~/main/i18n'\nimport { notifyAppConfigUpdated } from '~/main/utils/appConfigNotifier'\nimport { rpcServer } from './rpc'\n\nconst STORE_PATH = path.dirname(dbPathChecker())\n\ninterface GuiMenuItem {\n  label: string\n  handle: (arg0: PicGoCore, arg1: GuiApi) => Promise<void>\n}\n\n// get uploader or transformer config\nconst getConfig = (name: string, type: IPicGoHelperType, ctx: PicGoCore) => {\n  let config: any[] = []\n  if (name === '') {\n    return config\n  } else {\n    const handler = ctx.helper[type].get(name)\n    if (handler) {\n      if (handler.config) {\n        config = handler.config(ctx)\n      }\n    }\n    return config\n  }\n}\n\nconst handleConfigWithFunction = (config: any[]) => {\n  for (const i in config) {\n    if (typeof config[i].default === 'function') {\n      config[i].default = config[i].default()\n    }\n    if (typeof config[i].choices === 'function') {\n      config[i].choices = config[i].choices()\n    }\n  }\n  return config\n}\n\nconst getPluginList = (): IPicGoPlugin[] => {\n  const pluginList = picgo.pluginLoader.getFullList()\n  const list = []\n  for (const i in pluginList) {\n    const plugin = picgo.pluginLoader.getPlugin(pluginList[i])!\n    const pluginPath = path.join(STORE_PATH, `/node_modules/${pluginList[i]}`)\n    const pluginPKG = fs.readJSONSync(path.join(pluginPath, 'package.json'), 'utf-8')\n    const uploaderName = plugin.uploader || ''\n    const transformerName = plugin.transformer || ''\n    let menu: Omit<IGuiMenuItem, 'handle'>[] = []\n    if (plugin.guiMenu) {\n      menu = plugin.guiMenu(picgo).map(item => ({\n        label: item.label\n      }))\n    }\n    let gui = false\n    if (pluginPKG.keywords && pluginPKG.keywords.length > 0) {\n      if (pluginPKG.keywords.includes('picgo-gui-plugin')) {\n        gui = true\n      }\n    }\n    const obj: IPicGoPlugin = {\n      name: handleStreamlinePluginName(pluginList[i]),\n      fullName: pluginList[i],\n      author: pluginPKG.author.name || pluginPKG.author,\n      description: pluginPKG.description,\n      logo: 'file://' + path.join(pluginPath, 'logo.png').split(path.sep).join('/'),\n      version: pluginPKG.version,\n      gui,\n      config: {\n        plugin: {\n          fullName: pluginList[i],\n          name: handleStreamlinePluginName(pluginList[i]),\n          config: plugin.config ? handleConfigWithFunction(plugin.config(picgo)) : []\n        },\n        uploader: {\n          name: uploaderName,\n          config: handleConfigWithFunction(getConfig(uploaderName, IPicGoHelperType.uploader, picgo))\n        },\n        transformer: {\n          name: transformerName,\n          config: handleConfigWithFunction(getConfig(uploaderName, IPicGoHelperType.transformer, picgo))\n        }\n      },\n      enabled: picgo.getConfig(`picgoPlugins.${pluginList[i]}`),\n      homepage: pluginPKG.homepage ? pluginPKG.homepage : '',\n      guiMenu: menu,\n      ing: false\n    }\n    list.push(obj)\n  }\n  return list\n}\n\nconst handleGetPluginList = () => {\n  ipcMain.on('getPluginList', (event: IpcMainEvent) => {\n    try {\n      const list = simpleClone(getPluginList())\n      // here can just send JS Object not function\n      // or will cause [Failed to serialize arguments] error\n      event.sender.send('pluginList', list)\n    } catch (e: any) {\n      event.sender.send('pluginList', [])\n      showNotification({\n        title: T('TIPS_GET_PLUGIN_LIST_FAILED'),\n        body: e.message\n      })\n      picgo.log.error(e)\n    }\n  })\n}\n\nconst handlePluginInstall = () => {\n  ipcMain.on('installPlugin', async (event: IpcMainEvent, fullName: string) => {\n    const dispose = handleNPMError()\n    const res = await picgo.pluginHandler.install([fullName])\n    event.sender.send('installPlugin', {\n      success: res.success,\n      body: fullName,\n      errMsg: res.success ? '' : res.body\n    })\n    if (res.success) {\n      shortKeyHandler.registerPluginShortKey(res.body[0])\n    } else {\n      showNotification({\n        title: T('PLUGIN_INSTALL_FAILED'),\n        body: res.body as string\n      })\n    }\n    event.sender.send('hideLoading')\n    dispose()\n  })\n}\n\nconst handlePluginUninstall = async (fullName: string) => {\n  const window = windowManager.get(IWindowList.SETTING_WINDOW)!\n  const dispose = handleNPMError()\n  const res = await picgo.pluginHandler.uninstall([fullName])\n  if (res.success) {\n    window.webContents.send('uninstallSuccess', res.body[0])\n    shortKeyHandler.unregisterPluginShortKey(res.body[0])\n  } else {\n    showNotification({\n      title: T('PLUGIN_UNINSTALL_FAILED'),\n      body: res.body as string\n    })\n  }\n  window.webContents.send('hideLoading')\n  dispose()\n}\n\nconst handlePluginUpdate = async (fullName: string) => {\n  const window = windowManager.get(IWindowList.SETTING_WINDOW)!\n  const dispose = handleNPMError()\n  const res = await picgo.pluginHandler.update([fullName])\n  if (res.success) {\n    window.webContents.send('updateSuccess', res.body[0])\n  } else {\n    showNotification({\n      title: T('PLUGIN_UPDATE_FAILED'),\n      body: res.body as string\n    })\n  }\n  window.webContents.send('hideLoading')\n  dispose()\n}\n\nconst handleNPMError = (): IDispose => {\n  const handler = (msg: string) => {\n    if (msg === 'NPM is not installed') {\n      dialog.showMessageBox({\n        title: T('TIPS_ERROR'),\n        message: T('TIPS_INSTALL_NODE_AND_RELOAD_PICGO'),\n        buttons: ['Yes']\n      }).then((res) => {\n        if (res.response === 0) {\n          shell.openExternal('https://nodejs.org/')\n        }\n      })\n    }\n  }\n  picgo.once('failed', handler)\n  return () => picgo.off('failed', handler)\n}\n\nconst handleGetPicBedConfig = () => {\n  ipcMain.on(GET_PICBED_CONFIG, (event: IpcMainEvent, type: string) => {\n    const name = picgo.helper.uploader.get(type)?.name || type\n    if (picgo.helper.uploader.get(type)?.config) {\n      const _config = picgo.helper.uploader.get(type)!.config!(picgo)\n      const config = handleConfigWithFunction(_config)\n      event.sender.send(GET_PICBED_CONFIG, config, name)\n    } else {\n      event.sender.send(GET_PICBED_CONFIG, [], name)\n    }\n  })\n}\n\n// TODO: remove it\nconst handlePluginActions = () => {\n  ipcMain.on('pluginActions', (event: IpcMainEvent, name: string, label: string) => {\n    const plugin = picgo.pluginLoader.getPlugin(name)\n    if (plugin?.guiMenu?.(picgo)?.length) {\n      const menu: GuiMenuItem[] = plugin.guiMenu(picgo)\n      menu.forEach(item => {\n        if (item.label === label) {\n          item.handle(picgo, GuiApi.getInstance())\n        }\n      })\n    }\n  })\n}\n\nconst handleRemoveFiles = () => {\n  ipcMain.on('removeFiles', (event: IpcMainEvent, files: ImgInfo[]) => {\n    setTimeout(() => {\n      picgo.emit('remove', files, GuiApi.getInstance())\n    }, 500)\n  })\n}\n\nconst handlePicGoSaveConfig = () => {\n  ipcMain.handle(PICGO_SAVE_CONFIG, (_event, data: IObj) => {\n    picgo.saveConfig(data)\n    notifyAppConfigUpdated()\n    return true\n  })\n}\n\nconst handlePicGoGetConfig = () => {\n  ipcMain.on(PICGO_GET_CONFIG, (event: IpcMainEvent, key: string | undefined, callbackId: string) => {\n    const result = picgo.getConfig(key)\n    event.sender.send(PICGO_GET_CONFIG, result, callbackId)\n  })\n}\n\nconst handleImportLocalPlugin = () => {\n  ipcMain.on('importLocalPlugin', async (event: IpcMainEvent) => {\n    const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!\n    const res = await dialog.showOpenDialog(settingWindow, {\n      properties: ['openDirectory']\n    })\n    const filePaths = res.filePaths\n    if (filePaths.length > 0) {\n      const res = await picgo.pluginHandler.install(filePaths)\n      if (res.success) {\n        try {\n          const list = simpleClone(getPluginList())\n          event.sender.send('pluginList', list)\n        } catch (e: any) {\n          event.sender.send('pluginList', [])\n          showNotification({\n            title: T('TIPS_GET_PLUGIN_LIST_FAILED'),\n            body: e.message\n          })\n        }\n        showNotification({\n          title: T('PLUGIN_IMPORT_SUCCEED'),\n          body: ''\n        })\n      } else {\n        showNotification({\n          title: T('PLUGIN_IMPORT_FAILED'),\n          body: res.body as string\n        })\n      }\n    }\n    event.sender.send('hideLoading')\n  })\n}\n\nconst handlePicGoGalleryDB = () => {\n  ipcMain.on(PICGO_GET_DB, async (event: IpcMainEvent, filter: IFilter, callbackId: string) => {\n    const dbStore = GalleryDB.getInstance()\n    const res = await dbStore.get(filter)\n    event.sender.send(PICGO_GET_DB, res, callbackId)\n  })\n\n  ipcMain.on(PICGO_INSERT_DB, async (event: IpcMainEvent, value: IObject, callbackId: string) => {\n    const dbStore = GalleryDB.getInstance()\n    const res = await dbStore.insert(value)\n    event.sender.send(PICGO_INSERT_DB, res, callbackId)\n  })\n\n  ipcMain.on(PICGO_INSERT_MANY_DB, async (event: IpcMainEvent, value: IObject[], callbackId: string) => {\n    const dbStore = GalleryDB.getInstance()\n    const res = await dbStore.insertMany(value)\n    event.sender.send(PICGO_INSERT_MANY_DB, res, callbackId)\n  })\n\n  ipcMain.on(PICGO_UPDATE_BY_ID_DB, async (event: IpcMainEvent, id: string, value: IObject[], callbackId: string) => {\n    const dbStore = GalleryDB.getInstance()\n    const res = await dbStore.updateById(id, value)\n    event.sender.send(PICGO_UPDATE_BY_ID_DB, res, callbackId)\n  })\n\n  ipcMain.on(PICGO_GET_BY_ID_DB, async (event: IpcMainEvent, id: string, callbackId: string) => {\n    const dbStore = GalleryDB.getInstance()\n    const res = await dbStore.getById(id)\n    event.sender.send(PICGO_GET_BY_ID_DB, res, callbackId)\n  })\n\n  ipcMain.on(PICGO_REMOVE_BY_ID_DB, async (event: IpcMainEvent, id: string, callbackId: string) => {\n    const dbStore = GalleryDB.getInstance()\n    const res = await dbStore.removeById(id)\n    event.sender.send(PICGO_REMOVE_BY_ID_DB, res, callbackId)\n  })\n\n  ipcMain.handle(PASTE_TEXT, async (_, item: ImgInfo, copy = true) => {\n    const pasteStyle = picgo.getConfig<IPasteStyle>('settings.pasteStyle') || IPasteStyle.MARKDOWN\n    const customLink = picgo.getConfig<string>('settings.customLink')\n    const txt = pasteTemplate(pasteStyle, item, customLink)\n    if (copy) {\n      clipboard.writeText(txt)\n    }\n    return txt\n  })\n}\n\nconst handleOpenFile = () => {\n  ipcMain.on(PICGO_OPEN_FILE, (event: IpcMainEvent, fileName: string) => {\n    const abFilePath = path.join(STORE_PATH, fileName)\n    shell.openPath(abFilePath)\n  })\n}\n\nconst handleOpenWindow = () => {\n  ipcMain.on(OPEN_WINDOW, (event: IpcMainEvent, windowName: IWindowList) => {\n    const window = windowManager.get(windowName)\n    if (window) {\n      window.show()\n    }\n  })\n}\n\nconst handleI18n = () => {\n  ipcMain.on(GET_LANGUAGE_LIST, (event: IpcMainEvent) => {\n    event.sender.send(GET_LANGUAGE_LIST, i18nManager.languageList)\n  })\n  ipcMain.on(SET_CURRENT_LANGUAGE, (event: IpcMainEvent, language: string) => {\n    i18nManager.setCurrentLanguage(language)\n    const { lang, locales } = i18nManager.getCurrentLocales()\n    picgo.i18n.setLanguage(lang)\n    if (process.platform === 'darwin') {\n      const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)\n      trayWindow?.webContents.send(SET_CURRENT_LANGUAGE, lang, locales)\n    }\n    const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)\n    settingWindow?.webContents.send(SET_CURRENT_LANGUAGE, lang, locales)\n    if (windowManager.has(IWindowList.MINI_WINDOW)) {\n      const miniWindow = windowManager.get(IWindowList.MINI_WINDOW)\n      miniWindow?.webContents.send(SET_CURRENT_LANGUAGE, lang, locales)\n    }\n    // event.sender.send(SET_CURRENT_LANGUAGE, lang, locales)\n  })\n  ipcMain.on(GET_CURRENT_LANGUAGE, (event: IpcMainEvent) => {\n    const { lang, locales } = i18nManager.getCurrentLocales()\n    event.sender.send(GET_CURRENT_LANGUAGE, lang, locales)\n  })\n}\n\nconst handleRPCActions = () => {\n  rpcServer.start()\n}\n\nexport default {\n  listen () {\n    handleGetPluginList()\n    handlePluginInstall()\n    handleGetPicBedConfig()\n    handlePluginActions()\n    handleRemoveFiles()\n    handlePicGoSaveConfig()\n    handlePicGoGetConfig()\n    handlePicGoGalleryDB()\n    handleImportLocalPlugin()\n    handleOpenFile()\n    handleOpenWindow()\n    handleI18n()\n    handleRPCActions()\n  },\n  // TODO: separate to single file\n  handlePluginUninstall,\n  handlePluginUpdate\n}\n"
  },
  {
    "path": "src/main/events/remotes/menu.ts",
    "content": "import windowManager from 'apis/app/window/windowManager'\nimport { IWindowList } from '#/types/enum'\nimport { Menu, BrowserWindow, app, dialog } from 'electron'\nimport picgo from '@core/picgo'\nimport {\n  uploadClipboardFiles\n} from '~/main/apis/app/uploader/apis'\nimport { privacyManager } from '~/main/utils/privacyManager'\nimport pkg from 'root/package.json'\nimport GuiApi from 'apis/gui'\nimport { PICGO_CONFIG_PLUGIN, PICGO_HANDLE_PLUGIN_DONE, PICGO_HANDLE_PLUGIN_ING, PICGO_TOGGLE_PLUGIN, SHOW_MAIN_PAGE_DONATION, SHOW_MAIN_PAGE_QRCODE } from '~/universal/events/constants'\nimport picgoCoreIPC from '~/main/events/picgoCoreIPC'\nimport { PicGo as PicGoCore } from 'picgo'\nimport { T } from '~/main/i18n'\nimport { buildPicBedListMenu } from './picBedListMenu'\n\ninterface GuiMenuItem {\n  label: string\n  handle: (arg0: PicGoCore, arg1: GuiApi) => Promise<void>\n}\n\nconst buildMiniPageMenu = () => {\n  const submenu = buildPicBedListMenu()\n  const template = [\n    {\n      label: T('OPEN_MAIN_WINDOW'),\n      click () {\n        windowManager.get(IWindowList.SETTING_WINDOW)!.show()\n        if (windowManager.has(IWindowList.MINI_WINDOW)) {\n          windowManager.get(IWindowList.MINI_WINDOW)!.hide()\n        }\n      }\n    },\n    {\n      label: T('CHOOSE_DEFAULT_PICBED'),\n      type: 'submenu',\n      submenu\n    },\n    {\n      label: T('UPLOAD_BY_CLIPBOARD'),\n      click () {\n        uploadClipboardFiles()\n      }\n    },\n    {\n      label: T('HIDE_WINDOW'),\n      click () {\n        BrowserWindow.getFocusedWindow()!.hide()\n      }\n    },\n    {\n      label: T('PRIVACY_TERMS_AGREEMENT'),\n      click () {\n        privacyManager.show(false)\n      }\n    },\n    {\n      label: T('RELOAD_APP'),\n      click () {\n        app.relaunch()\n        app.exit(0)\n      }\n    },\n    {\n      role: 'quit',\n      label: T('QUIT')\n    }\n  ]\n  // @ts-ignore\n  return Menu.buildFromTemplate(template)\n}\n\nconst buildMainPageMenu = (win: BrowserWindow) => {\n  const template = [\n    {\n      label: T('ABOUT'),\n      click () {\n        dialog.showMessageBox({\n          title: 'PicGo',\n          message: 'PicGo',\n          detail: `Version: ${pkg.version}\\nAuthor: Molunerfinn\\nGithub: https://github.com/Molunerfinn/PicGo`\n        })\n      }\n    },\n    {\n      label: T('SPONSOR_PICGO'),\n      click () {\n        win?.webContents?.send(SHOW_MAIN_PAGE_DONATION)\n      }\n    },\n    {\n      label: T('SHOW_PICBED_QRCODE'),\n      click () {\n        win?.webContents?.send(SHOW_MAIN_PAGE_QRCODE)\n      }\n    },\n    {\n      label: T('OPEN_TOOLBOX'),\n      click () {\n        const window = windowManager.create(IWindowList.TOOLBOX_WINDOW)\n        window?.show()\n      }\n    },\n    {\n      label: T('SHOW_DEVTOOLS'),\n      click () {\n        win?.webContents?.openDevTools({\n          mode: 'detach'\n        })\n      }\n    },\n    {\n      label: T('PRIVACY_TERMS_AGREEMENT'),\n      click () {\n        privacyManager.show(false)\n      }\n    }\n  ]\n  // @ts-ignore\n  return Menu.buildFromTemplate(template)\n}\n\n// TODO: separate to single file\n\nconst handleRestoreState = (item: string, name: string): void => {\n  if (item === 'uploader') {\n    const current = picgo.getConfig('picBed.current')\n    if (current === name) {\n      picgo.saveConfig({\n        'picBed.current': 'smms',\n        'picBed.uploader': 'smms'\n      })\n    }\n  }\n  if (item === 'transformer') {\n    const current = picgo.getConfig('picBed.transformer')\n    if (current === name) {\n      picgo.saveConfig({\n        'picBed.transformer': 'path'\n      })\n    }\n  }\n}\n\nconst buildPluginPageMenu = (plugin: IPicGoPlugin) => {\n  const menu = [{\n    label: T('ENABLE_PLUGIN'),\n    enabled: !plugin.enabled,\n    click () {\n      picgo.saveConfig({\n        [`picgoPlugins.${plugin.fullName}`]: true\n      })\n      const window = windowManager.get(IWindowList.SETTING_WINDOW)!\n      window.webContents.send(PICGO_TOGGLE_PLUGIN, plugin.fullName, true)\n    }\n  }, {\n    label: T('DISABLE_PLUGIN'),\n    enabled: plugin.enabled,\n    click () {\n      picgo.saveConfig({\n        [`picgoPlugins.${plugin.fullName}`]: false\n      })\n      const window = windowManager.get(IWindowList.SETTING_WINDOW)!\n      window.webContents.send(PICGO_HANDLE_PLUGIN_ING, plugin.fullName)\n      window.webContents.send(PICGO_TOGGLE_PLUGIN, plugin.fullName, false)\n      window.webContents.send(PICGO_HANDLE_PLUGIN_DONE, plugin.fullName)\n      if (plugin.config.transformer.name) {\n        handleRestoreState('transformer', plugin.config.transformer.name)\n      }\n      if (plugin.config.uploader.name) {\n        handleRestoreState('uploader', plugin.config.uploader.name)\n      }\n    }\n  }, {\n    label: T('UNINSTALL_PLUGIN'),\n    click () {\n      const window = windowManager.get(IWindowList.SETTING_WINDOW)!\n      window.webContents.send(PICGO_HANDLE_PLUGIN_ING, plugin.fullName)\n      picgoCoreIPC.handlePluginUninstall(plugin.fullName)\n    }\n  }, {\n    label: T('UPDATE_PLUGIN'),\n    click () {\n      const window = windowManager.get(IWindowList.SETTING_WINDOW)!\n      window.webContents.send(PICGO_HANDLE_PLUGIN_ING, plugin.fullName)\n      picgoCoreIPC.handlePluginUpdate(plugin.fullName)\n    }\n  }]\n  for (const i in plugin.config) {\n    if (i === 'uploader') {\n      continue\n    }\n    if (plugin.config[i].config.length > 0) {\n      const obj = {\n        label: T('CONFIG_THING', {\n          c: `${i} - ${plugin.config[i].fullName || plugin.config[i].name}`\n        }),\n        click () {\n          const window = windowManager.get(IWindowList.SETTING_WINDOW)!\n          const currentType = i\n          const configName = plugin.config[i].fullName || plugin.config[i].name\n          const config = plugin.config[i].config\n          window.webContents.send(PICGO_CONFIG_PLUGIN, currentType, configName, config)\n        }\n      }\n      menu.push(obj)\n    }\n  }\n\n  // handle transformer\n  if (plugin.config.transformer.name) {\n    const currentTransformer = picgo.getConfig<string>('picBed.transformer') || 'path'\n    const pluginTransformer = plugin.config.transformer.name\n    const obj = {\n      label: `${currentTransformer === pluginTransformer ? T('DISABLE') : T('ENABLE')}transformer - ${plugin.config.transformer.name}`,\n      click () {\n        const transformer = plugin.config.transformer.name\n        const currentTransformer = picgo.getConfig<string>('picBed.transformer') || 'path'\n        if (currentTransformer === transformer) {\n          picgo.saveConfig({\n            'picBed.transformer': 'path'\n          })\n        } else {\n          picgo.saveConfig({\n            'picBed.transformer': transformer\n          })\n        }\n      }\n    }\n    menu.push(obj)\n  }\n\n  // plugin custom menus\n  if (plugin.guiMenu) {\n    menu.push({\n      // @ts-ignore\n      type: 'separator'\n    })\n    for (const i of plugin.guiMenu) {\n      menu.push({\n        label: i.label,\n        click () {\n          // ipcRenderer.send('pluginActions', plugin.fullName, i.label)\n          const picgPlugin = picgo.pluginLoader.getPlugin(plugin.fullName)\n          if (picgPlugin?.guiMenu?.(picgo)?.length) {\n            const menu: GuiMenuItem[] = picgPlugin.guiMenu(picgo)\n            menu.forEach(item => {\n              if (item.label === i.label) {\n                item.handle(picgo, GuiApi.getInstance())\n              }\n            })\n          }\n        }\n      })\n    }\n  }\n\n  // @ts-ignore\n  return Menu.buildFromTemplate(menu)\n}\n\nexport {\n  buildMiniPageMenu,\n  buildMainPageMenu,\n  buildPicBedListMenu,\n  buildPluginPageMenu\n}\n"
  },
  {
    "path": "src/main/events/remotes/picBedListMenu.ts",
    "content": "import windowManager from 'apis/app/window/windowManager'\nimport { IWindowList } from '#/types/enum'\nimport { Menu } from 'electron'\nimport getPicBeds from '~/main/utils/getPicBeds'\nimport picgo from '@core/picgo'\nimport { T } from '~/main/i18n'\n\nexport const buildPicBedListMenu = () => {\n  const picBeds = getPicBeds()\n  const currentPicBed = picgo.getConfig('picBed.uploader')\n  const currentPicBedName = picBeds.find(item => item.type === currentPicBed)?.name\n  const picBedConfigList = picgo.getConfig<IUploaderConfig>('uploader')\n  const currentPicBedMenuItem = [{\n    label: `${T('CURRENT_PICBED')} - ${currentPicBedName}`,\n    enabled: false\n  }, {\n    type: 'separator'\n  }]\n  let submenu = picBeds.filter(item => item.visible).map(item => {\n    const configList = picBedConfigList?.[item.type]?.configList\n    const defaultId = picBedConfigList?.[item.type]?.defaultId\n    const hasSubmenu = !!configList\n    return {\n      label: item.name,\n      type: !hasSubmenu ? 'checkbox' : undefined,\n      checked: !hasSubmenu ? (currentPicBed === item.type) : undefined,\n      submenu: hasSubmenu\n        ? configList.map((config) => {\n          return {\n            label: config._configName || 'Default',\n            // if only one config, use checkbox, or radio will checked as default\n            // see: https://github.com/electron/electron/issues/21292\n            type: 'checkbox',\n            checked: config._id === defaultId && (item.type === currentPicBed),\n            click: function () {\n              try {\n                picgo.uploaderConfig.use(item.type, config._configName)\n              } catch (e) {\n                picgo.log.error(e instanceof Error ? e : new Error(String(e)))\n              }\n              if (windowManager.has(IWindowList.SETTING_WINDOW)) {\n                windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')\n              }\n            }\n          }\n        })\n        : undefined,\n      click: !hasSubmenu\n        ? function () {\n          picgo.saveConfig({\n            'picBed.current': item.type,\n            'picBed.uploader': item.type\n          })\n          if (windowManager.has(IWindowList.SETTING_WINDOW)) {\n            windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')\n          }\n        }\n        : undefined\n    }\n  })\n  // @ts-ignore\n  submenu = currentPicBedMenuItem.concat(submenu)\n  // @ts-ignore\n  return Menu.buildFromTemplate(submenu)\n}\n"
  },
  {
    "path": "src/main/events/rpc/index.ts",
    "content": "import { ipcMain, IpcMainEvent, IpcMainInvokeEvent } from 'electron'\nimport { IRPCActionType } from '~/universal/types/enum'\nimport { RPC_ACTIONS } from '#/events/constants'\nimport { configRouter } from './routes/config'\nimport { versionRouter } from './routes/version'\nimport { toolboxRouter } from './routes/toolbox'\nimport { systemRouter } from './routes/system'\nimport { galleryToolboxRouter } from './routes/galleryToolbox'\nimport { cloudRouter } from './routes/cloud'\nimport { fail, isIRPCResult, ok } from './utils'\n\nclass RPCServer implements IRPCServer {\n  private routes: IRPCRoutes = new Map()\n\n  private rpcEventHandler = async (event: IpcMainEvent, action: IRPCActionType, args: any[], callbackId?: string) => {\n    try {\n      const handler = this.routes.get(action)\n      if (!handler) {\n        return this.sendBack(event, action, null, callbackId)\n      }\n      const res = await handler?.(args, event)\n      this.sendBack(event, action, res, callbackId)\n    } catch (e) {\n      this.sendBack(event, action, null, callbackId)\n    }\n  }\n\n  private rpcInvokeHandler = async (_event: IpcMainInvokeEvent, action: IRPCActionType, args: any[] = []) => {\n    const handler = this.routes.get(action)\n    if (!handler) {\n      return fail(new Error(`RPC action not supported: ${action}`))\n    }\n    try {\n      const res = await handler(args, _event)\n      // For invoke-based RPC, normalize the return value to IRPCResult.\n      return isIRPCResult(res) ? res : ok(res)\n    } catch (e) {\n      return fail(e)\n    }\n  }\n\n  /**\n   * if sendback data is null, then it means that the action is not supported or error occurs\n   * if there is no callbackId, then do not send back\n   */\n  private sendBack (event: IpcMainEvent, action: IRPCActionType, data: any, callbackId?: string) {\n    if (callbackId) {\n      event.sender.send(RPC_ACTIONS, data, action, callbackId)\n    }\n  }\n\n  start () {\n    ipcMain.on(RPC_ACTIONS, this.rpcEventHandler)\n    ipcMain.handle(RPC_ACTIONS, this.rpcInvokeHandler)\n  }\n\n  use (routes: IRPCRoutes) {\n    for (const [action, handler] of routes) {\n      this.routes.set(action, handler)\n    }\n  }\n\n  stop () {\n    ipcMain.off(RPC_ACTIONS, this.rpcEventHandler)\n    ipcMain.removeHandler(RPC_ACTIONS)\n  }\n}\n\nconst rpcServer = new RPCServer()\n\nrpcServer.use(configRouter.routes())\nrpcServer.use(versionRouter.routes())\nrpcServer.use(toolboxRouter.routes())\nrpcServer.use(systemRouter.routes())\nrpcServer.use(galleryToolboxRouter.routes())\nrpcServer.use(cloudRouter.routes())\n\nexport {\n  rpcServer\n}\n"
  },
  {
    "path": "src/main/events/rpc/router.ts",
    "content": "import { IRPCActionType } from '~/universal/types/enum'\n\nexport class RPCRouter implements IRPCRouter {\n  private routeMap: IRPCRoutes = new Map()\n  add = <T>(action: IRPCActionType, handler: IRPCHandler<T>) => {\n    this.routeMap.set(action, handler)\n    return this\n  }\n\n  routes () {\n    return this.routeMap\n  }\n}\n"
  },
  {
    "path": "src/main/events/rpc/routes/cloud.ts",
    "content": "import { IRPCActionType } from '~/universal/types/enum'\nimport { RPCRouter } from '../router'\nimport picgo from '@core/picgo'\nimport type { IPicGoCloudUserInfo } from '#/types/cloud'\nimport { T } from '~/main/i18n'\nimport { fail, ok } from '../utils'\nimport GuiApi from 'apis/gui'\nimport fs from 'fs-extra'\nimport { parse } from 'comment-json'\nimport { cloneDeep, isPlainObject, set, unset } from 'lodash'\nimport path from 'path'\nimport logger from 'apis/core/picgo/logger'\nimport {\n  ConfigSyncManager,\n  ConflictType,\n  E2EAskPinReason,\n  EncryptionMethod,\n  SyncStatus,\n  type IDiffNode,\n  type IConfig\n} from 'picgo'\nimport {\n  IPicGoCloudConfigSyncConflictChoice,\n  IPicGoCloudConfigSyncRunStatus,\n  IPicGoCloudConfigSyncSessionStatus,\n  IPicGoCloudConfigSyncToastType,\n  IPicGoCloudEncryptionMethod,\n  type IPicGoCloudConfigSyncConflictItem,\n  type IPicGoCloudConfigSyncResolution,\n  type IPicGoCloudConfigSyncRunResult,\n  type IPicGoCloudConfigSyncState\n} from '#/types/cloudConfigSync'\n\nconst cloudRouter = new RPCRouter()\n\nconst LOGIN_TIMEOUT_MS = 5 * 60 * 1000\nconst USER_ABORTED_CODE = 'PICGO_CLOUD_CONFIG_SYNC_ABORTED'\n\n/**\n * Config sync session state MUST live in the main process (memory only) so the UI can re-hydrate\n * after window hide/show without losing an in-progress/conflict state.\n */\nlet configSyncSessionStatus: IPicGoCloudConfigSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.IDLE\nlet configSyncConflictDiffTree: IDiffNode | null = null\nlet configSyncConflictItems: IPicGoCloudConfigSyncConflictItem[] = []\nlet configSyncManager: ConfigSyncManager | null = null\n\nconst clearConfigSyncSession = (): void => {\n  configSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.IDLE\n  configSyncConflictDiffTree = null\n  configSyncConflictItems = []\n}\n\nconst logConfigSyncOutcome = (\n  stage: 'sync' | 'applyResolvedConfig',\n  status: SyncStatus,\n  message?: string,\n  meta: { conflictCount?: number } = {}\n): void => {\n  const prefix = `[PicGo Cloud][config-sync][${stage}]`\n  if (status === SyncStatus.SUCCESS) {\n    logger.info(`${prefix} success`)\n    return\n  }\n\n  if (status === SyncStatus.CONFLICT) {\n    const count = typeof meta.conflictCount === 'number' ? meta.conflictCount : 0\n    logger.warn(`${prefix} conflict`, `count=${count}`)\n    return\n  }\n\n  if (message === USER_ABORTED_CODE || message === 'Invalid PIN input') {\n    logger.warn(`${prefix} aborted`)\n    return\n  }\n\n  logger.error(`${prefix} failed`, message || '')\n}\n\nconst getLocalEncryptionMethod = (): IPicGoCloudEncryptionMethod | undefined => {\n  const value = picgo.getConfig<unknown>('settings.picgoCloud.encryptionMethod')\n  if (\n    value === IPicGoCloudEncryptionMethod.AUTO\n    || value === IPicGoCloudEncryptionMethod.SSE\n    || value === IPicGoCloudEncryptionMethod.E2EE\n  ) {\n    return value\n  }\n  return undefined\n}\n\nconst toSyncEncryptionMethod = (method?: IPicGoCloudEncryptionMethod): EncryptionMethod | undefined => {\n  if (method === IPicGoCloudEncryptionMethod.AUTO) return EncryptionMethod.AUTO\n  if (method === IPicGoCloudEncryptionMethod.SSE) return EncryptionMethod.SSE\n  if (method === IPicGoCloudEncryptionMethod.E2EE) return EncryptionMethod.E2EE\n  return undefined\n}\n\nconst getSnapshotUpdatedAt = async (): Promise<string | undefined> => {\n  try {\n    const snapshotPath = path.join(picgo.baseDir, 'config.snapshot.json')\n    if (!(await fs.pathExists(snapshotPath))) return undefined\n    const content = await fs.readFile(snapshotPath, 'utf8')\n    const parsed: unknown = parse(content)\n    if (!isPlainObject(parsed)) return undefined\n    const updatedAt = (parsed as { updatedAt?: unknown }).updatedAt\n    return typeof updatedAt === 'string' && updatedAt ? updatedAt : undefined\n  } catch {\n    return undefined\n  }\n}\n\nconst buildConfigSyncState = async (): Promise<IPicGoCloudConfigSyncState> => {\n  return {\n    sessionStatus: configSyncSessionStatus,\n    encryptionMethod: getLocalEncryptionMethod(),\n    lastSyncedAt: await getSnapshotUpdatedAt(),\n    conflicts: configSyncSessionStatus === IPicGoCloudConfigSyncSessionStatus.CONFLICT ? configSyncConflictItems : undefined\n  }\n}\n\nconst readLocalConfigWithComments = async (): Promise<IConfig> => {\n  if (!(await fs.pathExists(picgo.configPath))) {\n    return picgo.getConfig<IConfig>()\n  }\n  const content = await fs.readFile(picgo.configPath, 'utf8')\n  const parsed: unknown = parse(content)\n  if (!isPlainObject(parsed)) {\n    throw new Error(T('PICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID'))\n  }\n  return parsed as IConfig\n}\n\nconst extractConflictItems = (diffTree: IDiffNode): IPicGoCloudConfigSyncConflictItem[] => {\n  const items: IPicGoCloudConfigSyncConflictItem[] = []\n\n  const walk = (node: IDiffNode, pathSegments: string[]) => {\n    const nextSegments = node.key === 'root' ? pathSegments : [...pathSegments, node.key]\n\n    if (node.status === ConflictType.CONFLICT) {\n      // If the conflict is an object-level aggregation, surface leaf conflicts instead.\n      if (node.children && node.children.length > 0) {\n        node.children.forEach(child => walk(child, nextSegments))\n        return\n      }\n\n      items.push({\n        path: nextSegments.join('.'),\n        localValue: node.localValue,\n        remoteValue: node.remoteValue\n      })\n      return\n    }\n\n    if (node.children && node.children.length > 0) {\n      node.children.forEach(child => walk(child, nextSegments))\n    }\n  }\n\n  walk(diffTree, [])\n  return items\n}\n\nconst localizeConfigSyncResult = (status: SyncStatus, message: string | undefined): { message: string, toastType: IPicGoCloudConfigSyncToastType } => {\n  if (status === SyncStatus.SUCCESS) {\n    return {\n      message: T('PICGO_CLOUD_CONFIG_SYNC_SUCCESS'),\n      toastType: IPicGoCloudConfigSyncToastType.SUCCESS\n    }\n  }\n\n  if (status === SyncStatus.CONFLICT) {\n    return {\n      message: T('PICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED'),\n      toastType: IPicGoCloudConfigSyncToastType.INFO\n    }\n  }\n\n  const raw = message || T('PICGO_CLOUD_CONFIG_SYNC_FAILED')\n\n  const isEncryptionSwitchCancelled = message === picgo.i18n.translate('CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED')\n  if (raw === USER_ABORTED_CODE || raw === 'Invalid PIN input' || isEncryptionSwitchCancelled) {\n    return {\n      message: isEncryptionSwitchCancelled\n        ? T('PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED')\n        : T('PICGO_CLOUD_CONFIG_SYNC_ABORTED'),\n      toastType: IPicGoCloudConfigSyncToastType.WARNING\n    }\n  }\n\n  if (raw === 'Maximum retry attempts exceeded') {\n    return {\n      message: T('PICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY'),\n      toastType: IPicGoCloudConfigSyncToastType.ERROR\n    }\n  }\n\n  return {\n    message: T('PICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON', { reason: raw }),\n    toastType: IPicGoCloudConfigSyncToastType.ERROR\n  }\n}\n\nconst getEncryptionMethodLabel = (method: EncryptionMethod): string => {\n  if (method === EncryptionMethod.E2EE) return T('PICGO_CLOUD_ENCRYPTION_MODE_E2E')\n  return T('PICGO_CLOUD_ENCRYPTION_MODE_SERVER')\n}\n\nconst getConfigSyncManager = (): ConfigSyncManager => {\n  if (configSyncManager) return configSyncManager\n\n  const guiApi = GuiApi.getInstance()\n  configSyncManager = new ConfigSyncManager(picgo, {\n    onAskPin: async (reason: E2EAskPinReason, retryCount: number) => {\n      const inputOptions: IShowInputBoxOption = {\n        title: (() => {\n          if (reason === E2EAskPinReason.SETUP) return T('PICGO_CLOUD_E2E_PIN_SETUP_TITLE')\n          if (reason === E2EAskPinReason.DECRYPT) return T('PICGO_CLOUD_E2E_PIN_DECRYPT_TITLE')\n          return T('PICGO_CLOUD_E2E_PIN_RETRY_TITLE', { retryCount })\n        })(),\n        placeholder: T('PICGO_CLOUD_E2E_PIN_PLACEHOLDER'),\n        inputType: 'password',\n        width: 520,\n        confirm: reason === E2EAskPinReason.SETUP\n          ? { placeholder: T('PICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER') }\n          : undefined\n      }\n\n      const value = await guiApi.showInputBox(inputOptions)\n      if (!value) {\n        // Throw a sentinel code so we can treat it as a user-aborted flow in the GUI.\n        throw new Error(USER_ABORTED_CODE)\n      }\n      return value\n    },\n    onAskEncryptionSwitch: async ({ from, to }: { from: EncryptionMethod, to: EncryptionMethod }): Promise<boolean> => {\n      const title = T('PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE')\n      const message = T('PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY', {\n        from: getEncryptionMethodLabel(from),\n        to: getEncryptionMethodLabel(to)\n      })\n      const res = await guiApi.showMessageBox({\n        title,\n        message,\n        type: 'warning',\n        buttons: [\n          T('PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM'),\n          T('PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL')\n        ]\n      })\n      return res.result === 0\n    }\n  })\n\n  return configSyncManager\n}\n\nconst buildResolvedConfig = async (resolution: IPicGoCloudConfigSyncResolution): Promise<IConfig> => {\n  const base = await readLocalConfigWithComments()\n\n  for (const item of configSyncConflictItems) {\n    const choice = resolution[item.path]\n    if (choice === IPicGoCloudConfigSyncConflictChoice.CLOUD) {\n      if (item.remoteValue === undefined) {\n        unset(base, item.path)\n      } else {\n        set(base, item.path, cloneDeep(item.remoteValue))\n      }\n    }\n  }\n\n  return base\n}\n\nconst getUserInfo = async (): Promise<IPicGoCloudUserInfo | null> => {\n  return await picgo.cloud.getUserInfo()\n}\n\nconst loginWithTimeout = async (): Promise<void> => {\n  const loginPromise = picgo.cloud.login()\n\n  let timeoutId: ReturnType<typeof setTimeout> | undefined\n  const timeoutPromise = new Promise<never>((_resolve, reject) => {\n    timeoutId = setTimeout(() => {\n      picgo.cloud.disposeLoginFlow()\n      reject(new Error(T('PICGO_CLOUD_LOGIN_TIMEOUT')))\n    }, LOGIN_TIMEOUT_MS)\n  })\n\n  try {\n    await Promise.race([loginPromise, timeoutPromise])\n  } finally {\n    if (timeoutId) clearTimeout(timeoutId)\n    // Avoid unhandled rejection when timeout disposes the core login flow.\n    loginPromise.catch(() => {})\n  }\n}\n\ncloudRouter\n  .add(IRPCActionType.PICGO_CLOUD_GET_USER_INFO, async () => {\n    try {\n      const userInfo = await getUserInfo()\n      return ok(userInfo)\n    } catch (e) {\n      return fail(e)\n    }\n  })\n  .add(IRPCActionType.PICGO_CLOUD_LOGIN, async () => {\n    try {\n      await loginWithTimeout()\n      const userInfo = await getUserInfo()\n      if (!userInfo) {\n        return fail(T('PICGO_CLOUD_LOGIN_FAILED'))\n      }\n      return ok(userInfo)\n    } catch (e) {\n      return fail(e)\n    }\n  })\n  .add(IRPCActionType.PICGO_CLOUD_LOGOUT, async () => {\n    try {\n      picgo.cloud.logout()\n      clearConfigSyncSession()\n      return ok(true)\n    } catch (e) {\n      return fail(e)\n    }\n  })\n  .add(IRPCActionType.PICGO_CLOUD_DISPOSE_LOGIN_FLOW, async () => {\n    try {\n      picgo.cloud.disposeLoginFlow()\n      return ok(true)\n    } catch (e) {\n      return fail(e)\n    }\n  })\n  .add(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_GET_STATE, async () => {\n    try {\n      return ok(await buildConfigSyncState())\n    } catch (e) {\n      return fail(e)\n    }\n  })\n  .add(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_SET_E2E_PREFERENCE, async (args) => {\n    try {\n      const [mode] = args as [IPicGoCloudEncryptionMethod]\n      if (mode === IPicGoCloudEncryptionMethod.AUTO) {\n        picgo.saveConfig({\n          'settings.picgoCloud.encryptionMethod': IPicGoCloudEncryptionMethod.AUTO\n        })\n      } else if (mode === IPicGoCloudEncryptionMethod.SSE) {\n        picgo.saveConfig({\n          'settings.picgoCloud.encryptionMethod': IPicGoCloudEncryptionMethod.SSE\n        })\n      } else {\n        picgo.saveConfig({\n          'settings.picgoCloud.encryptionMethod': IPicGoCloudEncryptionMethod.E2EE\n        })\n      }\n      return ok(getLocalEncryptionMethod())\n    } catch (e) {\n      return fail(e)\n    }\n  })\n  .add(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_ABORT, async () => {\n    try {\n      clearConfigSyncSession()\n      return ok(await buildConfigSyncState())\n    } catch (e) {\n      return fail(e)\n    }\n  })\n  .add(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_START, async () => {\n    const fallbackState = await buildConfigSyncState()\n\n    if (configSyncSessionStatus === IPicGoCloudConfigSyncSessionStatus.SYNCING) {\n      logger.info('[PicGo Cloud][config-sync][sync] already in progress')\n      const runRes: IPicGoCloudConfigSyncRunResult = {\n        status: IPicGoCloudConfigSyncRunStatus.FAILED,\n        message: T('PICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS'),\n        toastType: IPicGoCloudConfigSyncToastType.INFO,\n        state: fallbackState\n      }\n      return ok(runRes)\n    }\n\n    if (configSyncSessionStatus === IPicGoCloudConfigSyncSessionStatus.CONFLICT) {\n      logger.info('[PicGo Cloud][config-sync][sync] pending conflict session')\n      const runRes: IPicGoCloudConfigSyncRunResult = {\n        status: IPicGoCloudConfigSyncRunStatus.CONFLICT,\n        message: T('PICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING'),\n        toastType: IPicGoCloudConfigSyncToastType.INFO,\n        state: fallbackState\n      }\n      return ok(runRes)\n    }\n\n    configSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.SYNCING\n\n    try {\n      const userInfo = await getUserInfo()\n      if (!userInfo) {\n        logger.warn('[PicGo Cloud][config-sync][sync] login expired')\n        clearConfigSyncSession()\n        const runRes: IPicGoCloudConfigSyncRunResult = {\n          status: IPicGoCloudConfigSyncRunStatus.FAILED,\n          message: T('PICGO_CLOUD_LOGIN_EXPIRED'),\n          toastType: IPicGoCloudConfigSyncToastType.WARNING,\n          authInvalidated: true,\n          state: await buildConfigSyncState()\n        }\n        return ok(runRes)\n      }\n\n      const manager = getConfigSyncManager()\n      const encryptionMethod = toSyncEncryptionMethod(getLocalEncryptionMethod())\n      const res = encryptionMethod\n        ? await manager.sync({ encryptionMethod })\n        : await manager.sync()\n\n      if (res.status === SyncStatus.CONFLICT && res.diffTree) {\n        configSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.CONFLICT\n        configSyncConflictDiffTree = res.diffTree\n        configSyncConflictItems = extractConflictItems(res.diffTree)\n        logConfigSyncOutcome('sync', res.status, res.message, { conflictCount: configSyncConflictItems.length })\n      } else {\n        clearConfigSyncSession()\n        logConfigSyncOutcome('sync', res.status, res.message)\n      }\n\n      const localized = localizeConfigSyncResult(res.status, res.message)\n      const runStatus = res.status === SyncStatus.SUCCESS\n        ? IPicGoCloudConfigSyncRunStatus.SUCCESS\n        : res.status === SyncStatus.CONFLICT\n          ? IPicGoCloudConfigSyncRunStatus.CONFLICT\n          : IPicGoCloudConfigSyncRunStatus.FAILED\n\n      const runRes: IPicGoCloudConfigSyncRunResult = {\n        status: runStatus,\n        message: localized.message,\n        toastType: localized.toastType,\n        shouldShowRestartPrompt: res.status === SyncStatus.SUCCESS,\n        state: await buildConfigSyncState()\n      }\n      return ok(runRes)\n    } catch (e) {\n      logger.error('[PicGo Cloud][config-sync][sync] error', e)\n      clearConfigSyncSession()\n      const localized = localizeConfigSyncResult(SyncStatus.FAILED, e instanceof Error ? e.message : String(e))\n      const runRes: IPicGoCloudConfigSyncRunResult = {\n        status: IPicGoCloudConfigSyncRunStatus.FAILED,\n        message: localized.message,\n        toastType: localized.toastType,\n        state: await buildConfigSyncState()\n      }\n      return ok(runRes)\n    }\n  })\n  .add(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_APPLY_RESOLUTION, async (args) => {\n    try {\n      const [resolution] = args as [IPicGoCloudConfigSyncResolution]\n\n      if (configSyncSessionStatus !== IPicGoCloudConfigSyncSessionStatus.CONFLICT || !configSyncConflictDiffTree) {\n        logger.warn('[PicGo Cloud][config-sync][applyResolvedConfig] no conflict session')\n        const runRes: IPicGoCloudConfigSyncRunResult = {\n          status: IPicGoCloudConfigSyncRunStatus.FAILED,\n          message: T('PICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION'),\n          toastType: IPicGoCloudConfigSyncToastType.ERROR,\n          state: await buildConfigSyncState()\n        }\n        return ok(runRes)\n      }\n\n      const expectedPaths = new Set(configSyncConflictItems.map(item => item.path))\n      const providedPaths = new Set(Object.keys(resolution))\n      for (const path of expectedPaths) {\n        if (!providedPaths.has(path)) {\n          logger.warn('[PicGo Cloud][config-sync][applyResolvedConfig] resolution incomplete')\n          const runRes: IPicGoCloudConfigSyncRunResult = {\n            status: IPicGoCloudConfigSyncRunStatus.FAILED,\n            message: T('PICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE'),\n            toastType: IPicGoCloudConfigSyncToastType.ERROR,\n            state: await buildConfigSyncState()\n          }\n          return ok(runRes)\n        }\n      }\n\n      const userInfo = await getUserInfo()\n      if (!userInfo) {\n        logger.warn('[PicGo Cloud][config-sync][applyResolvedConfig] login expired')\n        clearConfigSyncSession()\n        const runRes: IPicGoCloudConfigSyncRunResult = {\n          status: IPicGoCloudConfigSyncRunStatus.FAILED,\n          message: T('PICGO_CLOUD_LOGIN_EXPIRED'),\n          toastType: IPicGoCloudConfigSyncToastType.WARNING,\n          authInvalidated: true,\n          state: await buildConfigSyncState()\n        }\n        return ok(runRes)\n      }\n\n      configSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.SYNCING\n\n      const resolvedConfig = await buildResolvedConfig(resolution)\n\n      const encryptionMethod = getLocalEncryptionMethod()\n      const manager = getConfigSyncManager()\n      const applyRes = await manager.applyResolvedConfig(\n        resolvedConfig,\n        encryptionMethod === IPicGoCloudEncryptionMethod.E2EE\n          ? { useE2E: true }\n          : encryptionMethod === IPicGoCloudEncryptionMethod.SSE\n            ? { useE2E: false }\n            : {}\n      )\n\n      if (applyRes.status === SyncStatus.SUCCESS) {\n        clearConfigSyncSession()\n      } else {\n        // Keep conflict session so the user can retry.\n        configSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.CONFLICT\n      }\n\n      logConfigSyncOutcome('applyResolvedConfig', applyRes.status, applyRes.message, { conflictCount: configSyncConflictItems.length })\n\n      const localized = localizeConfigSyncResult(applyRes.status, applyRes.message)\n      const runStatus = applyRes.status === SyncStatus.SUCCESS\n        ? IPicGoCloudConfigSyncRunStatus.SUCCESS\n        : applyRes.status === SyncStatus.CONFLICT\n          ? IPicGoCloudConfigSyncRunStatus.CONFLICT\n          : IPicGoCloudConfigSyncRunStatus.FAILED\n\n      const runRes: IPicGoCloudConfigSyncRunResult = {\n        status: runStatus,\n        message: localized.message,\n        toastType: localized.toastType,\n        shouldShowRestartPrompt: applyRes.status === SyncStatus.SUCCESS,\n        state: await buildConfigSyncState()\n      }\n      return ok(runRes)\n    } catch (e) {\n      logger.error('[PicGo Cloud][config-sync][applyResolvedConfig] error', e)\n      // Keep the conflict session so user can retry from UI.\n      configSyncSessionStatus = IPicGoCloudConfigSyncSessionStatus.CONFLICT\n      const localized = localizeConfigSyncResult(SyncStatus.FAILED, e instanceof Error ? e.message : String(e))\n      const runRes: IPicGoCloudConfigSyncRunResult = {\n        status: IPicGoCloudConfigSyncRunStatus.FAILED,\n        message: localized.message,\n        toastType: localized.toastType,\n        state: await buildConfigSyncState()\n      }\n      return ok(runRes)\n    }\n  })\n\nexport {\n  cloudRouter\n}\n"
  },
  {
    "path": "src/main/events/rpc/routes/config.ts",
    "content": "import { IRPCActionType } from '~/universal/types/enum'\nimport { RPCRouter } from '../router'\nimport picgo from '@core/picgo'\nimport { T } from '~/main/i18n'\nimport { fail, ok } from '../utils'\nimport { notifyAppConfigUpdated } from '~/main/utils/appConfigNotifier'\n\nconst configRouter = new RPCRouter()\n\nconfigRouter\n  .add(IRPCActionType.GET_PICBED_CONFIG_LIST, async (args) => {\n    try {\n      const [type] = args as IGetUploaderConfigListArgs\n      const configList = picgo.uploaderConfig.getConfigList(type)\n      const activeConfig = picgo.uploaderConfig.getActiveConfig(type)\n      return ok({\n        configList,\n        defaultId: activeConfig?._id ?? ''\n      })\n    } catch (e) {\n      return fail(e)\n    }\n  })\n  .add(IRPCActionType.DELETE_PICBED_CONFIG, async (args) => {\n    try {\n      const [type, configName] = args as IDeleteUploaderConfigArgs\n      const existing = picgo.uploaderConfig.getConfigList(type)\n      if (existing.length <= 1) {\n        throw new Error(T('TIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST'))\n      }\n      picgo.uploaderConfig.remove(type, configName)\n      const configList = picgo.uploaderConfig.getConfigList(type)\n      const activeConfig = picgo.uploaderConfig.getActiveConfig(type)\n      notifyAppConfigUpdated()\n      return ok({\n        configList,\n        defaultId: activeConfig?._id ?? ''\n      })\n    } catch (e) {\n      return fail(e)\n    }\n  })\n  .add(IRPCActionType.COPY_UPLOADER_CONFIG, async (args) => {\n    try {\n      const [type, configName, newConfigName] = args as ICopyUploaderConfigArgs\n      picgo.uploaderConfig.copy(type, configName, newConfigName)\n      const configList = picgo.uploaderConfig.getConfigList(type)\n      const activeConfig = picgo.uploaderConfig.getActiveConfig(type)\n      notifyAppConfigUpdated()\n      return ok({\n        configList,\n        defaultId: activeConfig?._id ?? ''\n      })\n    } catch (e) {\n      return fail(e)\n    }\n  })\n  .add(IRPCActionType.SELECT_UPLOADER, async (args) => {\n    try {\n      const [type, configName] = args as ISelectUploaderConfigArgs\n      const activeConfig = picgo.uploaderConfig.use(type, configName)\n      notifyAppConfigUpdated()\n      return ok(activeConfig._id)\n    } catch (e) {\n      return fail(e)\n    }\n  })\n  .add(IRPCActionType.UPDATE_UPLOADER_CONFIG, async (args) => {\n    try {\n      const [type, configId, config] = args as IUpdateUploaderConfigArgs\n      const configName = typeof config._configName === 'string' ? config._configName : ''\n      if (configId && !configName) {\n        throw new Error(T('TIPS_UPLOADER_CONFIG_NAME_EMPTY'))\n      }\n\n      let oldConfigName = ''\n      if (configId) {\n        const configList = picgo.uploaderConfig.getConfigList(type)\n        const existConfig = configList.find(item => item._id === configId)\n        if (!existConfig) {\n          throw new Error(T('TIPS_UPLOADER_CONFIG_NOT_FOUND'))\n        }\n        oldConfigName = existConfig._configName\n      }\n\n      if (oldConfigName && oldConfigName !== configName) {\n        picgo.uploaderConfig.rename(type, oldConfigName, configName)\n      }\n      picgo.uploaderConfig.createOrUpdate(type, configName, config)\n      notifyAppConfigUpdated()\n      return ok(true)\n    } catch (e) {\n      return fail(e)\n    }\n  })\n\nexport {\n  configRouter\n}\n"
  },
  {
    "path": "src/main/events/rpc/routes/galleryToolbox/builtIn/changeURL.ts",
    "content": "import { logger } from '@picgo/i18n'\nimport { PicGoUtils, type IPicGo } from 'picgo'\nimport { T } from '~/main/i18n'\n\ninterface IUrlRewriteRule {\n  match: string\n  replace: string\n  enable?: boolean\n  global?: boolean\n  ignoreCase?: boolean\n}\n\ninterface IUrlRewriteDialogResult {\n  applyGlobalRules?: boolean\n  match?: string\n  replace?: string\n  global?: boolean\n  ignoreCase?: boolean\n}\n\nfunction normalizeRules (value: unknown): IUrlRewriteRule[] {\n  if (!Array.isArray(value)) return []\n  return value.map(item => {\n    const raw = (item ?? {}) as Partial<Record<keyof IUrlRewriteRule, unknown>>\n    return {\n      match: String(raw.match ?? ''),\n      replace: String(raw.replace ?? ''),\n      enable: raw.enable === false ? false : true,\n      global: raw.global === true,\n      ignoreCase: raw.ignoreCase === true\n    }\n  }).filter(rule => rule.match.length > 0)\n}\n\nfunction buildFlags (rule: Pick<IUrlRewriteRule, 'global' | 'ignoreCase'>): string {\n  return `${rule.global ? 'g' : ''}${rule.ignoreCase ? 'i' : ''}`\n}\n\nfunction validateRuleOrThrow (rule: IUrlRewriteRule) {\n  if (!rule.match.trim() || !rule.replace.trim()) {\n    throw new Error(T('GALLERY_URL_REWRITE_TEMP_RULE_REQUIRED'))\n  }\n  try {\n    new RegExp(rule.match, buildFlags(rule))\n  } catch (error) {\n    const message = `Invalid URL rewrite regex pattern \"${rule.match}\": ${error instanceof Error ? error.message : String(error)}`\n    logger.error(message)\n    throw new Error(message)\n  }\n}\n\nfunction applyFirstMatchRewrite (ctx: IPicGo, imgItem: ImgInfo, rules: IUrlRewriteRule[]): ImgInfo {\n  const imgInfo = {\n    imgUrl: imgItem.imgUrl,\n    originImgUrl: imgItem.originImgUrl\n  }\n  PicGoUtils.applyUrlRewriteToImgInfo(imgInfo, rules, {\n    log: {\n      error: (...args: Parameters<IPicGo['log']['error']>) => ctx.log.error(...args),\n      warn: () => ctx.log.warn(T('GALLERY_URL_REWRITE_EMPTY_RESULT_WARN'))\n    }\n  })\n  if (imgInfo.imgUrl === '') return imgItem\n  return imgInfo\n}\n\nexport const galleryMenu = () => {\n  return [{\n    label: T('GALLERY_URL_REWRITE_TITLE'),\n    async handle (ctx: IPicGo, guiApi: IGuiApi, selectedList: ImgInfo[] = []) {\n      if (!selectedList.length) {\n        guiApi.showNotification({\n          title: T('GALLERY_URL_REWRITE_TITLE'),\n          body: T('GALLERY_URL_REWRITE_WARN_NO_SELECTION')\n        })\n        logger.warn(T('GALLERY_URL_REWRITE_WARN_NO_SELECTION'))\n        return\n      }\n\n      const globalRules = normalizeRules(ctx.getConfig('settings.urlRewrite.rules'))\n\n      const config: IPicGoPluginConfig[] = [\n        {\n          alias: T('GALLERY_URL_REWRITE_APPLY_GLOBAL_RULES'),\n          name: 'applyGlobalRules',\n          type: 'confirm',\n          default: globalRules.length > 0,\n          required: false,\n          confirmText: T('SETTINGS_OPEN'),\n          cancelText: T('SETTINGS_CLOSE'),\n          tips: `${T('GALLERY_URL_REWRITE_GLOBAL_RULES_COUNT')}: ${globalRules.length}`\n        },\n        {\n          alias: T('URL_REWRITE_MATCH'),\n          name: 'match',\n          type: 'input',\n          message: T('URL_REWRITE_MATCH_PLACEHOLDER'),\n          default: '',\n          required: false,\n          tips: `${T('GALLERY_URL_REWRITE_TEMP_RULE_TIPS')}\\n\\n${T('URL_REWRITE_MATCH_TIPS')}`\n        },\n        {\n          alias: T('URL_REWRITE_REPLACE'),\n          name: 'replace',\n          type: 'input',\n          message: T('URL_REWRITE_REPLACE_PLACEHOLDER'),\n          default: '',\n          required: false,\n          tips: T('URL_REWRITE_REPLACE_TIPS')\n        },\n        {\n          alias: T('URL_REWRITE_FLAG_GLOBAL_LABEL'),\n          name: 'global',\n          type: 'confirm',\n          default: false,\n          required: false,\n          confirmText: 'g',\n          cancelText: '-',\n          tips: T('URL_REWRITE_FLAG_GLOBAL_DESC')\n        },\n        {\n          alias: T('URL_REWRITE_FLAG_IGNORE_CASE_LABEL'),\n          name: 'ignoreCase',\n          type: 'confirm',\n          default: false,\n          required: false,\n          confirmText: 'i',\n          cancelText: '-',\n          tips: T('URL_REWRITE_FLAG_IGNORE_CASE_DESC')\n        }\n      ]\n      const options: IPicGoPluginShowConfigDialogOption = {\n        title: T('GALLERY_URL_REWRITE_TITLE'),\n        config\n      }\n      const res = await guiApi.showConfigDialog<IUrlRewriteDialogResult>(options)\n      if (!res) return\n\n      const applyGlobalRules = res.applyGlobalRules === true\n      const tempMatch = String(res.match ?? '').trim()\n      const tempReplace = String(res.replace ?? '').trim()\n\n      const hasTempRuleInput = tempMatch.length > 0 || tempReplace.length > 0\n      let tempRule: IUrlRewriteRule | null = null\n      if (hasTempRuleInput) {\n        tempRule = {\n          match: tempMatch,\n          replace: tempReplace,\n          enable: true,\n          global: res.global === true,\n          ignoreCase: res.ignoreCase === true\n        }\n      }\n\n      if (tempRule) {\n        try {\n          validateRuleOrThrow(tempRule)\n        } catch (e: any) {\n          guiApi.showNotification({\n            title: T('GALLERY_URL_REWRITE_TITLE'),\n            body: e.message\n          })\n          return\n        }\n      }\n\n      if (!applyGlobalRules && !tempRule) {\n        guiApi.showNotification({\n          title: T('GALLERY_URL_REWRITE_TITLE'),\n          body: T('GALLERY_URL_REWRITE_NO_RULES_TO_APPLY')\n        })\n        return\n      }\n\n      let shouldSaveTempRule = false\n      if (tempRule) {\n        const saveRes = await guiApi.showMessageBox({\n          title: T('GALLERY_URL_REWRITE_TITLE'),\n          message: T('GALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT'),\n          type: 'info',\n          buttons: [\n            T('GALLERY_URL_REWRITE_APPLY_AND_SAVE'),\n            T('GALLERY_URL_REWRITE_APPLY_ONLY'),\n            T('CANCEL')\n          ]\n        })\n        if (saveRes.result === 2) return\n        shouldSaveTempRule = saveRes.result === 0\n      }\n\n      const rulesToApply: IUrlRewriteRule[] = [\n        ...(tempRule ? [tempRule] : []),\n        ...(applyGlobalRules ? globalRules : [])\n      ]\n\n      const changedList = selectedList.map((item) => {\n        const current = item.imgUrl || ''\n        if (!current) return false\n        const next = applyFirstMatchRewrite(ctx, item, rulesToApply)\n        if (next.imgUrl === current) return false\n        return {\n          id: item.id,\n          imgUrl: next.imgUrl,\n          originImgUrl: next.originImgUrl\n        } as ImgInfo\n      }).filter(Boolean) as ImgInfo[]\n\n      if (changedList.length === 0) {\n        guiApi.showNotification({\n          title: T('GALLERY_URL_REWRITE_RESULT_TITLE'),\n          body: T('GALLERY_URL_REWRITE_NO_CHANGES')\n        })\n        return\n      }\n\n      if (shouldSaveTempRule && tempRule) {\n        const nextGlobalRules = [...globalRules, tempRule]\n        ctx.saveConfig({\n          'settings.urlRewrite.rules': nextGlobalRules\n        })\n      }\n\n      const updateRes = await guiApi.galleryDB.updateMany(changedList)\n      guiApi.showNotification({\n        title: T('GALLERY_URL_REWRITE_RESULT_TITLE'),\n        body: `${T('SUCCESS')}: ${updateRes.success} ${T('FAILED')}: ${updateRes.total - updateRes.success}`\n      })\n    }\n  }]\n}\n"
  },
  {
    "path": "src/main/events/rpc/routes/galleryToolbox/builtIn/index.ts",
    "content": "import { galleryMenu as changeURLGalleryMenu } from './changeURL'\nexport const builtInGalleryToolboxMenu = () => {\n  const menuList = [...changeURLGalleryMenu()]\n\n  return menuList\n}\n"
  },
  {
    "path": "src/main/events/rpc/routes/galleryToolbox/index.ts",
    "content": "import { IRPCActionType, IWindowList } from '~/universal/types/enum'\nimport { RPCRouter } from '../../router'\nimport windowManager from '~/main/apis/app/window/windowManager'\nimport { galleryMenuListManager } from './menuListManager'\n\nconst galleryToolboxRouter = new RPCRouter()\n\ngalleryToolboxRouter.add(IRPCActionType.GET_GALLERY_MENU_LIST, async (args) => {\n  const [selectedList] = args as IGetGalleryMenuListArgs\n  const win = windowManager.get(IWindowList.SETTING_WINDOW)!\n  const menu = galleryMenuListManager.getMenu(selectedList)\n\n  menu.popup({\n    window: win\n  })\n})\n\nexport {\n  galleryToolboxRouter\n}\n"
  },
  {
    "path": "src/main/events/rpc/routes/galleryToolbox/menuListManager.ts",
    "content": "import { Menu, MenuItemConstructorOptions } from 'electron'\nimport { builtInGalleryToolboxMenu } from './builtIn'\nimport picgo from '@core/picgo'\nimport GuiApi from 'apis/gui'\nimport windowManager from '~/main/apis/app/window/windowManager'\nimport { IRPCActionType, IWindowList } from '~/universal/types/enum'\nimport logger from '~/main/apis/core/picgo/logger'\n\nclass GalleryMenuListManager {\n  private menuList: MenuItemConstructorOptions[] = []\n  private menu: Menu | null = null\n\n  private getBuiltInMenuList (selectedList: IGalleryItem[]): MenuItemConstructorOptions[] {\n    const builtInMenu = builtInGalleryToolboxMenu().map(item => {\n      return {\n        label: item.label,\n        async click () {\n          try {\n            await item.handle(picgo, GuiApi.getInstance(), selectedList)\n          } catch (e: any) {\n            logger.error(e)\n          } finally {\n            windowManager.get(IWindowList.SETTING_WINDOW)?.webContents.send(IRPCActionType.UPDATE_GALLERY)\n          }\n        }\n      }\n    }) as MenuItemConstructorOptions[]\n    this.menuList = [...builtInMenu]\n\n    return this.menuList\n  }\n\n  private getMenuItemList (selectedList: IGalleryItem[]) {\n    // current only support built-in menu\n    return this.getBuiltInMenuList(selectedList)\n  }\n\n  public getMenu (selectedList: IGalleryItem[]): Menu {\n    this.menu = Menu.buildFromTemplate(this.getMenuItemList(selectedList))\n    return this.menu\n  }\n}\n\nconst galleryMenuListManager = new GalleryMenuListManager()\n\nexport {\n  galleryMenuListManager\n}\n"
  },
  {
    "path": "src/main/events/rpc/routes/system.ts",
    "content": "import { IRPCActionType, IWindowList } from '~/universal/types/enum'\nimport { RPCRouter } from '../router'\nimport { app, clipboard, shell } from 'electron'\nimport windowManager from '~/main/apis/app/window/windowManager'\nimport { handleMenubarIcon } from '~/main/apis/app/system'\nimport { PICGO_NOTIFICATION_CLICKED } from '~/universal/events/constants'\nimport { showNotification } from '~/main/utils/common'\n\nconst systemRouter = new RPCRouter()\n\nsystemRouter\n  .add(IRPCActionType.RELOAD_APP, async () => {\n    app.relaunch()\n    app.exit(0)\n  })\n  .add(IRPCActionType.OPEN_FILE, async (args) => {\n    const [filePath] = args as IOpenFileArgs\n    shell.openPath(filePath)\n  })\n  .add(IRPCActionType.COPY_TEXT, async (args) => {\n    const [text] = args as ICopyTextArgs\n    return clipboard.writeText(text)\n  })\n  .add(IRPCActionType.SHOW_DOCK_ICON, async (args) => {\n    const [visible] = args as IShowDockIconArgs\n    app.dock?.[visible ? 'show' : 'hide']()\n    const win = windowManager.get(IWindowList.SETTING_WINDOW)\n    if (!visible) {\n      win?.show()\n      win?.focus()\n      win?.setSkipTaskbar(true)\n    } else {\n      win?.setSkipTaskbar(false)\n    }\n  })\n  .add(IRPCActionType.SHOW_MENUBAR_ICON, async (args) => {\n    const [visible] = args as IShowMenubarIconArgs\n    handleMenubarIcon(visible)\n  })\n  .add(IRPCActionType.SHOW_NOTIFICATION, async (args, event) => {\n    const [title, body, id] = args as IShowNotificationArgs\n\n    const options: IPrivateShowNotificationOption = {\n      title,\n      body\n    }\n\n    if (id) {\n      options.callback = () => {\n        if (!event.sender.isDestroyed()) {\n          event.sender.send(PICGO_NOTIFICATION_CLICKED, id)\n        }\n      }\n    }\n    \n    showNotification(options)\n  })\n\nexport {\n  systemRouter\n}\n"
  },
  {
    "path": "src/main/events/rpc/routes/toolbox/checkClipboardUpload.ts",
    "content": "import fs from 'fs-extra'\nimport path from 'path'\nimport { dbPathChecker, defaultConfigPath } from '~/main/apis/core/datastore/dbChecker'\nimport { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'\nimport { CLIPBOARD_IMAGE_FOLDER } from '~/universal/utils/static'\nimport { sendToolboxResWithType } from './utils'\nimport { T } from '~/main/i18n'\n\nconst sendToolboxRes = sendToolboxResWithType(IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD)\n\nconst defaultClipboardImagePath = path.join(defaultConfigPath, CLIPBOARD_IMAGE_FOLDER)\n\nexport const checkClipboardUploadMap: IToolboxCheckerMap<\n  IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD\n> = {\n  [IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: async (event) => {\n    sendToolboxRes(event, {\n      status: IToolboxItemCheckStatus.LOADING\n    })\n    const configFilePath = dbPathChecker()\n    if (fs.existsSync(configFilePath)) {\n      const dirPath = path.dirname(configFilePath)\n      const clipboardImagePath = path.join(dirPath, CLIPBOARD_IMAGE_FOLDER)\n      if (fs.existsSync(clipboardImagePath)) {\n        sendToolboxRes(event, {\n          status: IToolboxItemCheckStatus.SUCCESS,\n          msg: T('TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_TIPS', {\n            path: clipboardImagePath\n          }),\n          value: clipboardImagePath\n        })\n      } else {\n        sendToolboxRes(event, {\n          status: IToolboxItemCheckStatus.ERROR,\n          msg: T('TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS', {\n            path: clipboardImagePath\n          }),\n          value: path.dirname(clipboardImagePath)\n        })\n      }\n    } else {\n      sendToolboxRes(event, {\n        status: IToolboxItemCheckStatus.ERROR,\n        msg: T('TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS', {\n          path: defaultClipboardImagePath\n        }),\n        value: path.dirname(defaultClipboardImagePath)\n      })\n    }\n  }\n}\n\nexport const fixClipboardUploadMap: IToolboxFixMap<\n  IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD\n> = {\n  [IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: async () => {\n    const configFilePath = dbPathChecker()\n    const dirPath = path.dirname(configFilePath)\n    const clipboardImagePath = path.join(dirPath, CLIPBOARD_IMAGE_FOLDER)\n    try {\n      fs.mkdirsSync(clipboardImagePath)\n      return {\n        type: IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD,\n        status: IToolboxItemCheckStatus.SUCCESS\n      }\n    } catch (e) {\n      return {\n        type: IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD,\n        status: IToolboxItemCheckStatus.ERROR,\n        msg: T('TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS', {\n          path: clipboardImagePath\n        }),\n        value: path.dirname(clipboardImagePath)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/main/events/rpc/routes/toolbox/checkFile.ts",
    "content": "import fs from 'fs-extra'\nimport { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'\nimport { sendToolboxResWithType } from './utils'\nimport { dbPathChecker, getGalleryDBPath } from '~/main/apis/core/datastore/dbChecker'\nimport { GalleryDB } from '~/main/apis/core/datastore'\nimport path from 'path'\nimport { T } from '~/main/i18n'\n\nexport const checkFileMap: IToolboxCheckerMap<\nIToolboxItemType.IS_CONFIG_FILE_BROKEN | IToolboxItemType.IS_GALLERY_FILE_BROKEN\n> = {\n  [IToolboxItemType.IS_CONFIG_FILE_BROKEN]: async (event) => {\n    const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.IS_CONFIG_FILE_BROKEN)\n    sendToolboxRes(event, {\n      status: IToolboxItemCheckStatus.LOADING\n    })\n    const configFilePath = dbPathChecker()\n    try {\n      if (fs.existsSync(configFilePath)) {\n        await fs.readJSON(configFilePath)\n        sendToolboxRes(event, {\n          status: IToolboxItemCheckStatus.SUCCESS,\n          msg: T('TOOLBOX_CHECK_CONFIG_FILE_PATH_TIPS', {\n            path: configFilePath\n          }),\n          value: configFilePath\n        })\n      }\n    } catch (e) {\n      sendToolboxRes(event, {\n        status: IToolboxItemCheckStatus.ERROR,\n        msg: T('TOOLBOX_CHECK_CONFIG_FILE_BROKEN_TIPS'),\n        value: path.dirname(configFilePath)\n      })\n    }\n  },\n  [IToolboxItemType.IS_GALLERY_FILE_BROKEN]: async (event) => {\n    const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.IS_GALLERY_FILE_BROKEN)\n    sendToolboxRes(event, {\n      status: IToolboxItemCheckStatus.LOADING\n    })\n    const { dbPath } = getGalleryDBPath()\n    const galleryDB = GalleryDB.getInstance()\n    if (galleryDB.errorList.length === 0) {\n      sendToolboxRes(event, {\n        status: IToolboxItemCheckStatus.SUCCESS,\n        msg: T('TOOLBOX_CHECK_GALLERY_FILE_PATH_TIPS', {\n          path: dbPath\n        }),\n        value: path.dirname(dbPath)\n      })\n    } else {\n      sendToolboxRes(event, {\n        status: IToolboxItemCheckStatus.ERROR,\n        msg: T('TOOLBOX_CHECK_GALLERY_FILE_BROKEN_TIPS'),\n        value: path.dirname(dbPath)\n      })\n    }\n  }\n}\n\nexport const fixFileMap: IToolboxFixMap<\nIToolboxItemType.IS_CONFIG_FILE_BROKEN | IToolboxItemType.IS_GALLERY_FILE_BROKEN\n> = {\n  [IToolboxItemType.IS_CONFIG_FILE_BROKEN]: async () => {\n    try {\n      fs.unlinkSync(dbPathChecker())\n    } catch (e) {\n      // do nothing\n    }\n    return {\n      type: IToolboxItemType.IS_CONFIG_FILE_BROKEN,\n      status: IToolboxItemCheckStatus.SUCCESS\n    }\n  },\n  [IToolboxItemType.IS_GALLERY_FILE_BROKEN]: async () => {\n    try {\n      fs.unlinkSync(getGalleryDBPath().dbPath)\n    } catch (e) {\n      // do nothing\n    }\n    return {\n      type: IToolboxItemType.IS_GALLERY_FILE_BROKEN,\n      status: IToolboxItemCheckStatus.SUCCESS\n    }\n  }\n}\n"
  },
  {
    "path": "src/main/events/rpc/routes/toolbox/checkProxy.ts",
    "content": "import fs from 'fs-extra'\nimport { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'\nimport { sendToolboxResWithType } from './utils'\nimport tunnel from 'tunnel'\nimport { dbPathChecker } from '~/main/apis/core/datastore/dbChecker'\nimport { IConfig } from 'picgo'\nimport axios, { AxiosRequestConfig } from 'axios'\nimport { T } from '~/main/i18n'\n\nconst getProxy = (proxyStr: string): AxiosRequestConfig['proxy'] | false => {\n  if (proxyStr) {\n    try {\n      const proxyOptions = new URL(proxyStr)\n      return {\n        host: proxyOptions.hostname,\n        port: parseInt(proxyOptions.port || '0', 10),\n        protocol: proxyOptions.protocol\n      }\n    } catch (e) {\n    }\n  }\n  return false\n}\n\nconst sendToolboxRes = sendToolboxResWithType(IToolboxItemType.HAS_PROBLEM_WITH_PROXY)\n\nexport const checkProxyMap: IToolboxCheckerMap<\n  IToolboxItemType.HAS_PROBLEM_WITH_PROXY\n> = {\n  [IToolboxItemType.HAS_PROBLEM_WITH_PROXY]: async (event) => {\n    sendToolboxRes(event, {\n      status: IToolboxItemCheckStatus.LOADING\n    })\n    const configFilePath = dbPathChecker()\n    if (fs.existsSync(configFilePath)) {\n      let config: IConfig | undefined\n      try {\n        config = await fs.readJSON(configFilePath) as IConfig\n      } catch (e) {\n      }\n      if (!config) {\n        return sendToolboxRes(event, {\n          status: IToolboxItemCheckStatus.SUCCESS,\n          msg: T('TOOLBOX_CHECK_PROXY_NO_PROXY_TIPS')\n        })\n      }\n\n      const proxy = config.picBed?.proxy\n      if (!proxy) {\n        return sendToolboxRes(event, {\n          status: IToolboxItemCheckStatus.SUCCESS,\n          msg: T('TOOLBOX_CHECK_PROXY_NO_PROXY_TIPS')\n        })\n      } else {\n        const proxyOptions = getProxy(proxy)\n        if (!proxyOptions) {\n          return sendToolboxRes(event, {\n            status: IToolboxItemCheckStatus.ERROR,\n            msg: T('TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_CORRECT')\n          })\n        } else {\n          const httpsAgent = tunnel.httpsOverHttp({\n            proxy: {\n              host: proxyOptions.host,\n              port: proxyOptions.port\n            }\n          })\n          try {\n            await axios.get('https://www.google.com', {\n              httpsAgent\n            })\n            return sendToolboxRes(event, {\n              status: IToolboxItemCheckStatus.SUCCESS,\n              msg: T('TOOLBOX_CHECK_PROXY_SUCCESS_TIPS')\n            })\n          } catch (e) {\n            console.log(e)\n            return sendToolboxRes(event, {\n              status: IToolboxItemCheckStatus.ERROR,\n              msg: T('TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_WORKING')\n            })\n          }\n        }\n      }\n    }\n\n    sendToolboxRes(event, {\n      status: IToolboxItemCheckStatus.SUCCESS,\n      msg: T('TOOLBOX_CHECK_PROXY_NO_PROXY_TIPS')\n    })\n  }\n}\n"
  },
  {
    "path": "src/main/events/rpc/routes/toolbox/index.ts",
    "content": "import { IRPCActionType, IToolboxItemType } from '~/universal/types/enum'\nimport { RPCRouter } from '../../router'\nimport { checkFileMap, fixFileMap } from './checkFile'\nimport { checkClipboardUploadMap, fixClipboardUploadMap } from './checkClipboardUpload'\nimport { checkProxyMap } from './checkProxy'\n\nconst toolboxRouter = new RPCRouter()\n\nconst toolboxCheckMap: Partial<IToolboxCheckerMap<IToolboxItemType>> = {\n  ...checkFileMap,\n  ...checkClipboardUploadMap,\n  ...checkProxyMap\n}\n\nconst toolboxFixMap: Partial<IToolboxFixMap<IToolboxItemType>> = {\n  ...fixFileMap,\n  ...fixClipboardUploadMap\n}\n\ntoolboxRouter\n  .add(IRPCActionType.TOOLBOX_CHECK, async (args, event) => {\n    const [type] = args as IToolboxCheckArgs\n    if (type) {\n      const handler = toolboxCheckMap[type]\n      if (handler) {\n        handler(event)\n      }\n    } else {\n      // do check all\n      for (const key in toolboxCheckMap) {\n        const handler = toolboxCheckMap[key as IToolboxItemType]\n        if (handler) {\n          handler(event)\n        }\n      }\n    }\n  })\n  .add(IRPCActionType.TOOLBOX_CHECK_FIX, async (args, event) => {\n    const [type] = args as IToolboxCheckArgs\n    const handler = toolboxFixMap[type]\n    if (handler) {\n      return await handler(event)\n    }\n  })\n\nexport {\n  toolboxRouter\n}\n"
  },
  {
    "path": "src/main/events/rpc/routes/toolbox/utils.ts",
    "content": "import type { IpcMainEvent, IpcMainInvokeEvent } from 'electron'\nimport { IRPCActionType, IToolboxItemType } from '~/universal/types/enum'\n\nexport const sendToolboxResWithType = (type: IToolboxItemType) => (event: IpcMainEvent | IpcMainInvokeEvent, res?: Omit<IToolboxCheckRes, 'type'>) => {\n  return event.sender.send(IRPCActionType.TOOLBOX_CHECK_RES, {\n    ...res,\n    type\n  })\n}\n"
  },
  {
    "path": "src/main/events/rpc/routes/version.ts",
    "content": "import { IRPCActionType } from '~/universal/types/enum'\nimport { RPCRouter } from '../router'\nimport { getLatestVersion } from '~/main/utils/getLatestVersion'\n\nconst versionRouter = new RPCRouter()\n\nversionRouter\n  .add(IRPCActionType.GET_LATEST_VERSION, async (args) => {\n    const [type] = args as IGetLatestVersionArgs\n    const config = await getLatestVersion(type)\n    return config\n  })\n\nexport {\n  versionRouter\n}\n"
  },
  {
    "path": "src/main/events/rpc/utils.ts",
    "content": "const errorToMessage = (e: unknown): string => {\n  if (e instanceof Error) return e.message\n  return String(e)\n}\n\nexport const ok = <T>(data: T): IRPCResult<T> => ({\n  success: true,\n  data\n})\n\nexport const fail = <T>(e: unknown): IRPCResult<T> => ({\n  success: false,\n  error: errorToMessage(e)\n})\n\nexport const isIRPCResult = (value: unknown): value is IRPCResult<any> => {\n  if (!value || typeof value !== 'object') return false\n  const maybe = value as { success?: unknown, data?: unknown, error?: unknown }\n  if (typeof maybe.success !== 'boolean') return false\n\n  // success=true MUST have data; success=false MUST have error.\n  if (maybe.success) return 'data' in maybe\n  return typeof maybe.error === 'string'\n}\n\n"
  },
  {
    "path": "src/main/i18n/index.ts",
    "content": "import yaml from 'js-yaml'\nimport { ObjectAdapter, I18n } from '@picgo/i18n'\nimport path from 'path'\nimport fs from 'fs-extra'\nimport { getStaticPath } from '#/utils/staticPath'\nimport { builtinI18nList } from '#/i18n'\n\nclass I18nManager {\n  private i18n: I18n | null = null\n  private builtinI18nFolder = getStaticPath('i18n')\n  private outerI18nFolder = ''\n  private localesMap: Map<string, ILocales> = new Map()\n  private currentLanguage: string = 'en'\n  readonly defaultLanguage: string = 'en'\n  private i18nFileList: II18nItem[] = builtinI18nList\n\n  setOuterI18nFolder (folder: string) {\n    this.outerI18nFolder = folder\n  }\n\n  addI18nFile (file: string, label: string) {\n    this.i18nFileList.push({\n      label,\n      value: file\n    })\n  }\n\n  private getLocales (lang: string): ILocales {\n    if (this.localesMap.has(lang)) {\n      return this.localesMap.get(lang)!\n    }\n    let localesPath = path.join(this.builtinI18nFolder, `${lang}.yml`)\n    if (!fs.existsSync(localesPath)) {\n      localesPath = path.join(this.outerI18nFolder, `${lang}.yml`)\n      if (!fs.existsSync(localesPath)) {\n        localesPath = path.join(this.builtinI18nFolder, `${this.defaultLanguage}.yml`)\n      }\n    }\n    try {\n      const localesString = fs.readFileSync(localesPath, 'utf8')\n      const locales = yaml.load(localesString) as unknown as ILocales\n      this.localesMap.set(lang, locales)\n      return locales\n    } catch (e) {\n      console.error(e)\n      // if error, use default language\n      localesPath = path.join(this.builtinI18nFolder, `${this.defaultLanguage}.yml`)\n      const localesString = fs.readFileSync(localesPath, 'utf8')\n      const locales = yaml.load(localesString) as unknown as ILocales\n      this.localesMap.set(lang, locales)\n      return locales\n    }\n  }\n\n  setCurrentLanguage (lang: string) {\n    const locales = this.getLocales(lang)\n    this.currentLanguage = lang\n    this.initI18n(lang, locales)\n  }\n\n  private initI18n (lang: string = this.defaultLanguage, locales: ILocales) {\n    const objectAdapter = new ObjectAdapter({\n      [lang]: locales\n    })\n    this.i18n = new I18n({\n      adapter: objectAdapter,\n      defaultLanguage: lang\n    })\n  }\n\n  T (key: ILocalesKey, args: IStringKeyMap = {}): string {\n    return this.i18n?.translate(key, args) || key\n  }\n\n  get languageList () {\n    return this.i18nFileList\n  }\n\n  getCurrentLocales () {\n    return {\n      lang: this.currentLanguage,\n      locales: this.getLocales(this.currentLanguage)\n    }\n  }\n}\n\nexport const T = (key: ILocalesKey, args: IStringKeyMap = {}): string => {\n  return i18nManager.T(key, args)\n}\n\nexport const i18nManager = new I18nManager()\n"
  },
  {
    "path": "src/main/lifeCycle/errorHandler.ts",
    "content": "import path from 'path'\nimport { app } from 'electron'\nimport { getLogger } from 'apis/core/utils/localLogger'\nconst STORE_PATH = app.getPath('userData')\nconst LOG_PATH = path.join(STORE_PATH, 'picgo-gui-local.log')\n\nconst logger = getLogger(LOG_PATH)\n\n// since the error may occur in picgo-core\n// so we can't use the log from picgo\n\nconst handleProcessError = (error: Error | string) => {\n  logger('error', error)\n}\n\nprocess.on('uncaughtException', error => {\n  handleProcessError(error)\n})\n\nprocess.on('unhandledRejection', (error: any) => {\n  handleProcessError(error)\n})\n\n// thanks to https://github.com/camunda/camunda-modeler/pull/3314\nfunction bootstrapEPIPESuppression () {\n  let suppressing = false\n  function logEPIPEErrorOnce () {\n    if (suppressing) {\n      return\n    }\n\n    suppressing = true\n    handleProcessError('Detected EPIPE error; suppressing further EPIPE errors')\n  }\n\n  require('epipebomb')(process.stdout, logEPIPEErrorOnce)\n  require('epipebomb')(process.stderr, logEPIPEErrorOnce)\n}\n\nbootstrapEPIPESuppression()\n"
  },
  {
    "path": "src/main/lifeCycle/fixPath.ts",
    "content": "// TODO: so how to import pure esm module in electron main process????? help wanted\n\n// just copy the fix-path because I can't import pure ESM module in electron main process\n\nconst shellPath = require('shell-path')\n\nexport default function fixPath () {\n  if (process.platform === 'win32') {\n    return\n  }\n\n  process.env.PATH = shellPath.sync() || [\n    './node_modules/.bin',\n    '/.nodebrew/current/bin',\n    '/usr/local/bin',\n    process.env.PATH\n  ].join(':')\n}\n"
  },
  {
    "path": "src/main/lifeCycle/index.ts",
    "content": "import './errorHandler'\nimport {\n  app,\n  globalShortcut,\n  protocol\n} from 'electron'\nimport installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'\nimport beforeOpen from '~/main/utils/beforeOpen'\nimport ipcList from '~/main/events/ipcList'\nimport busEventList from '~/main/events/busEventList'\nimport { IRemoteNoticeTriggerHook, IWindowList } from '#/types/enum'\nimport windowManager from 'apis/app/window/windowManager'\nimport {\n  updateShortKeyFromVersion212,\n  migrateGalleryFromVersion230\n} from '~/main/migrate'\nimport {\n  uploadSelectedFiles,\n  uploadClipboardFiles\n} from 'apis/app/uploader/apis'\nimport {\n  handleDockIcon, handleMenubarIcon\n} from 'apis/app/system'\nimport server from '~/main/server/index'\nimport updateChecker from '~/main/utils/updateChecker'\nimport shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'\nimport { getUploadFiles } from '~/main/utils/handleArgv'\nimport { GalleryDB } from '~/main/apis/core/datastore'\nimport bus from '@core/bus'\nimport logger from 'apis/core/picgo/logger'\nimport picgo from 'apis/core/picgo'\nimport fixPath from './fixPath'\nimport { initI18n } from '~/main/utils/handleI18n'\nimport { remoteNoticeHandler } from 'apis/app/remoteNotice'\nimport { isMacOS } from '../utils/getMacOSVersion'\nimport { isWindowShouldShowOnStartup } from '../apis/app/window/windowList'\nimport { showNotification } from '../utils/common'\nimport { initStaticPath, isDev } from '../utils/env'\n\nconst isDevelopment = isDev\n\nconst handleStartUpFiles = (argv: string[], cwd: string) => {\n  const files = getUploadFiles(argv, cwd, logger)\n  if (files === null || files.length > 0) { // 如果有文件列表作为参数，说明是命令行启动\n    if (files === null) {\n      logger.info('cli -> uploading file from clipboard')\n      uploadClipboardFiles()\n    } else {\n      logger.info('cli -> uploading files from cli', ...files.map(item => item.path))\n      const win = windowManager.getAvailableWindow()\n      uploadSelectedFiles(win.webContents, files)\n    }\n    return true\n  } else {\n    return false\n  }\n}\n\nclass LifeCycle {\n  private async beforeReady () {\n    protocol.registerSchemesAsPrivileged([{ scheme: 'picgo', privileges: { secure: true, standard: true } }])\n    // fix the $PATH in macOS & linux\n    fixPath()\n    beforeOpen()\n    initI18n()\n    ipcList.listen()\n    busEventList.listen()\n    updateShortKeyFromVersion212(picgo)\n    await migrateGalleryFromVersion230(GalleryDB.getInstance(), picgo)\n  }\n\n  private onReady () {\n    const readyFunction = async () => {\n      console.log('on ready')\n      if (isDevelopment && !process.env.IS_TEST) {\n        // Install Vue Devtools\n        try {\n          await installExtension(VUEJS_DEVTOOLS)\n        } catch (e: any) {\n          console.error('Vue Devtools failed to install:', e?.toString())\n        }\n      }\n      windowManager.create(IWindowList.TRAY_WINDOW)\n      const settingWindow = windowManager.create(IWindowList.SETTING_WINDOW)\n      settingWindow?.once('show', () => {\n        remoteNoticeHandler.triggerHook(IRemoteNoticeTriggerHook.SETTING_WINDOW_OPEN)\n      })\n      if (isWindowShouldShowOnStartup(IWindowList.SETTING_WINDOW)) {\n        settingWindow?.show()\n        settingWindow?.focus()\n      }\n      if (!isMacOS) {\n        if (isWindowShouldShowOnStartup(IWindowList.MINI_WINDOW)) {\n          const miniWindow = windowManager.create(IWindowList.MINI_WINDOW)\n          miniWindow?.show()\n          miniWindow?.focus()\n        }\n      }\n      handleMenubarIcon()\n      handleDockIcon()\n      picgo.saveConfig({ needReload: false })\n      updateChecker()\n      // 不需要阻塞\n      process.nextTick(() => {\n        shortKeyHandler.init()\n      })\n      server.startup()\n      if (process.env.NODE_ENV !== 'development') {\n        handleStartUpFiles(process.argv, process.cwd())\n      }\n\n      if (global.notificationList && global.notificationList?.length > 0) {\n        while (global.notificationList?.length) {\n          const option = global.notificationList.pop()\n          if (option) {\n            showNotification(option)\n          }\n        }\n      }\n      await remoteNoticeHandler.init()\n      remoteNoticeHandler.triggerHook(IRemoteNoticeTriggerHook.APP_START)\n    }\n    app.whenReady().then(readyFunction)\n  }\n\n  private onRunning () {\n    app.on('second-instance', (event, commandLine, workingDirectory) => {\n      logger.info('detect second instance')\n      const result = handleStartUpFiles(commandLine, workingDirectory)\n      if (!result) {\n        if (windowManager.has(IWindowList.SETTING_WINDOW)) {\n          const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!\n          if (settingWindow.isMinimized()) {\n            settingWindow.restore()\n          }\n          settingWindow.focus()\n        }\n      }\n    })\n    app.on('activate', () => {\n      if (!windowManager.has(IWindowList.TRAY_WINDOW)) {\n        windowManager.create(IWindowList.TRAY_WINDOW)\n      }\n      if (!windowManager.has(IWindowList.SETTING_WINDOW)) {\n        windowManager.create(IWindowList.SETTING_WINDOW)\n      }\n      // click dock to open setting window\n      if (isMacOS) {\n        handleDockIcon()\n        if (picgo.getConfig<boolean>('settings.showDockIcon') !== false) {\n          windowManager.get(IWindowList.SETTING_WINDOW)?.show()\n        }\n      }\n    })\n    app.setLoginItemSettings({\n      openAtLogin: picgo.getConfig<boolean>('settings.autoStart') || false\n    })\n    if (process.platform === 'win32') {\n      app.setAppUserModelId('com.molunerfinn.picgo')\n    }\n\n    if (process.env.XDG_CURRENT_DESKTOP && process.env.XDG_CURRENT_DESKTOP.includes('Unity')) {\n      process.env.XDG_CURRENT_DESKTOP = 'Unity'\n    }\n  }\n\n  private onQuit () {\n    app.on('window-all-closed', () => {\n      if (process.platform !== 'darwin') {\n        app.quit()\n      }\n    })\n\n    app.on('will-quit', () => {\n      globalShortcut.unregisterAll()\n      bus.removeAllListeners()\n      server.shutdown()\n    })\n    // Exit cleanly on request from parent process in development mode.\n    if (isDevelopment) {\n      if (process.platform === 'win32') {\n        process.on('message', data => {\n          if (data === 'graceful-exit') {\n            app.quit()\n          }\n        })\n      } else {\n        process.on('SIGTERM', () => {\n          app.quit()\n        })\n      }\n    }\n  }\n\n  async launchApp () {\n    const gotTheLock = app.requestSingleInstanceLock()\n    if (!gotTheLock) {\n      app.quit()\n    } else {\n      initStaticPath()\n      await this.beforeReady()\n      this.onReady()\n      this.onRunning()\n      this.onQuit()\n    }\n  }\n}\n\nconst bootstrap = new LifeCycle()\n\nexport {\n  bootstrap\n}\n"
  },
  {
    "path": "src/main/migrate/index.ts",
    "content": "import { DBStore } from '@picgo/store'\nimport path from 'path'\nimport fse from 'fs-extra'\nimport { PicGo as PicGoCore } from 'picgo'\nimport { T } from '~/main/i18n'\nimport { SHORTKEY_COMMAND_UPLOAD } from 'apis/core/bus/constants'\n// from v2.1.2\nconst updateShortKeyFromVersion212 = (picgo: PicGoCore) => {\n  const shortKeyConfig = picgo.getConfig<IShortKeyConfigs | IOldShortKeyConfigs | undefined>('settings.shortKey')\n  // #557 极端情况可能会出现配置不存在，需要重新写入\n  if (shortKeyConfig === undefined) {\n    const defaultShortKeyConfig = {\n      enable: true,\n      key: 'CommandOrControl+Shift+U',\n      name: 'upload',\n      label: T('QUICK_UPLOAD')\n    }\n    picgo.saveConfig({\n      [`settings.shortKey[${SHORTKEY_COMMAND_UPLOAD}]`]: defaultShortKeyConfig\n    })\n    return true\n  }\n  if (typeof (shortKeyConfig as IOldShortKeyConfigs).upload === 'string') {\n    const oldKey = (shortKeyConfig as IOldShortKeyConfigs).upload\n    const nextConfig = Object.fromEntries(\n      Object.entries(shortKeyConfig).filter(([key]) => key !== 'upload')\n    ) as IShortKeyConfigs\n    nextConfig[SHORTKEY_COMMAND_UPLOAD] = {\n      enable: true,\n      key: oldKey,\n      name: 'upload',\n      label: T('QUICK_UPLOAD')\n    }\n    picgo.saveConfig({\n      'settings.shortKey': nextConfig\n    })\n    return true\n  }\n  return false\n}\n\nconst migrateGalleryFromVersion230 = async (galleryDB: DBStore, picgo: PicGoCore) => {\n  const originGallery = picgo.getConfig<ImgInfo[] | undefined>('uploaded')\n  // if hasMigrate, we don't need to migrate\n  const hasMigrate = picgo.getConfig<boolean | undefined>('__migrateUploaded') === true\n  if (hasMigrate) {\n    return\n  }\n  const configPath = picgo.configPath\n  const configBakPath = path.join(path.dirname(configPath), 'config.bak.json')\n  // migrate gallery from config to gallery db\n  if (originGallery && Array.isArray(originGallery) && originGallery?.length > 0) {\n    if (fse.existsSync(configBakPath)) {\n      fse.copyFileSync(configPath, configBakPath)\n    }\n    await galleryDB.insertMany(originGallery)\n    picgo.saveConfig({\n      uploaded: [],\n      __migrateUploaded: true\n    })\n  }\n}\n\nexport {\n  updateShortKeyFromVersion212,\n  migrateGalleryFromVersion230\n}\n"
  },
  {
    "path": "src/main/server/handler.ts",
    "content": "import logger from '@core/picgo/logger'\nimport windowManager from 'apis/app/window/windowManager'\nimport { uploadClipboardFiles, uploadSelectedFiles } from 'apis/app/uploader/apis'\nimport path from 'path'\nimport { dbPathDir, getFormImageFolderPath } from 'apis/core/datastore/dbChecker'\nimport fs from 'fs-extra'\nimport { cleanupFormUploaderFiles } from '~/main/utils/cleanupFormUploaderFiles'\nimport {\n  buildError,\n  buildSuccess,\n  getFormDataFileName,\n  isFileLike,\n  isRecord,\n  type HonoContextLike\n} from './utils'\n\nconst STORE_PATH = dbPathDir()\nconst LOG_PATH = path.join(STORE_PATH, 'picgo.log')\n\nconst errorMessage = `upload error. see ${LOG_PATH} for more detail.`\n\nconst handleClipboardUpload = async (c: HonoContextLike): Promise<Response> => {\n  try {\n    logger.info('[PicGo Server] upload clipboard file')\n    const res = await uploadClipboardFiles()\n    if (res) {\n      return c.json(buildSuccess([res]))\n    }\n    return c.json(buildError(errorMessage), 500)\n  } catch (e: unknown) {\n    logger.error('[PicGo Server] upload clipboard error', e)\n    return c.json(buildError(errorMessage), 500)\n  }\n}\n\nconst handleListUpload = async (c: HonoContextLike, list: string[]): Promise<Response> => {\n  try {\n    logger.info('[PicGo Server] upload files in list')\n    const win = windowManager.getAvailableWindow()\n    if (!win) {\n      return c.json(buildError(errorMessage), 500)\n    }\n    const pathList = list.map(item => ({ path: item }))\n    const res = await uploadSelectedFiles(win.webContents, pathList)\n    if (res.length) {\n      return c.json(buildSuccess(res))\n    }\n    return c.json(buildError(errorMessage), 500)\n  } catch (e: unknown) {\n    logger.error('[PicGo Server] upload list error', e)\n    return c.json(buildError(errorMessage), 500)\n  }\n}\n\nexport const uploadHandler = async (c: HonoContextLike): Promise<Response> => {\n  const contentType = c.req.raw.headers.get('content-type') || ''\n\n  if (contentType.includes('multipart/form-data')) {\n    const tempFiles: string[] = []\n    try {\n      const formData = await c.req.formData()\n      const files = formData.getAll('files') as unknown[]\n      if (files.length === 0) {\n        return c.json(buildError('No files found in form-data: files'), 400)\n      }\n\n      // Pre-validate to avoid leaking already-written temp files.\n      const fileLikes: Array<Parameters<typeof getFormDataFileName>[0]> = []\n      for (const file of files) {\n        if (!isFileLike(file)) {\n          return c.json(buildError('Invalid form-data: files must be file(s)'), 400)\n        }\n        fileLikes.push(file)\n      }\n\n      const formImagesPath = getFormImageFolderPath()\n      await fs.ensureDir(formImagesPath)\n\n      for (const file of fileLikes) {\n        const fileName = getFormDataFileName(file)\n        const safeName = path.basename(fileName)\n        const filePath = path.join(formImagesPath, safeName)\n        const buffer = Buffer.from(await file.arrayBuffer())\n        await fs.writeFile(filePath, buffer)\n        tempFiles.push(filePath)\n      }\n\n      return await handleListUpload(c, tempFiles)\n    } catch (e: unknown) {\n      logger.error('[PicGo Server] process form upload error', e)\n      return c.json(buildError(errorMessage), 500)\n    } finally {\n      cleanupFormUploaderFiles(tempFiles)\n    }\n  }\n\n  const bodyText = await c.req.raw.text().catch(() => '')\n\n  // No request body -> upload from clipboard.\n  if (bodyText.trim() === '') {\n    return await handleClipboardUpload(c)\n  }\n\n  let body: unknown\n  try {\n    body = JSON.parse(bodyText)\n  } catch {\n    return c.json(buildError('Invalid JSON body'), 400)\n  }\n\n  // GUI compatibility: JSON without list (or empty list) -> upload from clipboard.\n  if (!isRecord(body) || !('list' in body)) {\n    return await handleClipboardUpload(c)\n  }\n\n  const listValue = body.list\n  if (listValue === undefined) {\n    return await handleClipboardUpload(c)\n  }\n\n  if (!Array.isArray(listValue)) {\n    return c.json(buildError('Invalid request body: { list: string[] } required'), 400)\n  }\n\n  if (listValue.length === 0) {\n    return await handleClipboardUpload(c)\n  }\n\n  if (!listValue.every((item) => typeof item === 'string')) {\n    return c.json(buildError('Invalid request body: { list: string[] } required'), 400)\n  }\n\n  return await handleListUpload(c, listValue)\n}\n"
  },
  {
    "path": "src/main/server/index.ts",
    "content": "import picgo from '@core/picgo'\nimport logger from '@core/picgo/logger'\nimport { uploadHandler } from './handler'\n\nclass Server {\n  private config: IServerConfig\n  private hasRegisteredUploadOverride = false\n  constructor () {\n    this.config = this.ensureConfig()\n  }\n\n  private checkIfConfigIsValid (config: IObj | undefined) {\n    if (config && config.port && config.host && (config.enable !== undefined)) {\n      return true\n    } else {\n      return false\n    }\n  }\n\n  private ensureConfig (): IServerConfig {\n    let config = picgo.getConfig<IServerConfig>('settings.server')\n    if (this.checkIfConfigIsValid(config)) {\n      return config\n    }\n\n    config = {\n      port: 36677,\n      host: '127.0.0.1',\n      enable: true\n    }\n    picgo.saveConfig({\n      'settings.server': config\n    })\n    return config\n  }\n\n  private ensureUploadOverrideRegistered () {\n    if (this.hasRegisteredUploadOverride) return\n    // @ts-expect-error override internal handler\n    picgo.server.registerPost('/upload', uploadHandler, true)\n    this.hasRegisteredUploadOverride = true\n  }\n\n  startup () {\n    this.config = this.ensureConfig()\n    if (this.config.enable) {\n      this.ensureUploadOverrideRegistered()\n      // let core resolve config defaults when possible, but preserve GUI config semantics.\n      const port = typeof this.config.port === 'string' ? parseInt(this.config.port, 10) : this.config.port\n      picgo.server.listen(port, this.config.host)\n    }\n  }\n\n  shutdown () {\n    picgo.server.shutdown()\n    logger.info('[PicGo Server] shutdown')\n  }\n\n  restart () {\n    this.shutdown()\n    this.startup()\n  }\n}\n\nexport default new Server()\n"
  },
  {
    "path": "src/main/server/utils.ts",
    "content": "import { randomUUID } from 'node:crypto'\n\nexport type UploadResponseBody = {\n  success: boolean\n  result: string[]\n  message?: string\n}\n\nexport type HonoContextLike = {\n  req: {\n    raw: Request\n    formData: () => Promise<FormData>\n  }\n  json: (data: unknown, status?: number) => Response\n}\n\ntype FormDataFileLike = {\n  name?: string\n  arrayBuffer: () => Promise<ArrayBuffer>\n}\n\nexport const isFileLike = (value: unknown): value is FormDataFileLike => {\n  if (typeof value !== 'object' || value === null) return false\n  if (!('arrayBuffer' in value)) return false\n  return typeof (value as { arrayBuffer?: unknown }).arrayBuffer === 'function'\n}\n\nexport const isRecord = (value: unknown): value is Record<string, unknown> => {\n  return typeof value === 'object' && value !== null\n}\n\nexport const buildSuccess = (result: string[]): UploadResponseBody => ({\n  success: true,\n  result\n})\n\nexport const buildError = (message: string): UploadResponseBody => ({\n  success: false,\n  result: [],\n  message\n})\n\nexport const getFormDataFileName = (value: FormDataFileLike): string => {\n  const name = value.name\n  if (typeof name === 'string' && name.trim() !== '') return name\n  return `${randomUUID()}.png`\n}\n\n"
  },
  {
    "path": "src/main/utils/appConfigNotifier.ts",
    "content": "import { APP_CONFIG_UPDATED } from '#/events/constants'\nimport { IWindowList } from '#/types/enum'\nimport windowManager from 'apis/app/window/windowManager'\n\nconst TARGET_WINDOWS: IWindowList[] = [\n  IWindowList.SETTING_WINDOW,\n  IWindowList.TRAY_WINDOW,\n  IWindowList.MINI_WINDOW\n]\n\nexport const notifyAppConfigUpdated = (): void => {\n  TARGET_WINDOWS.forEach((windowType) => {\n    if (!windowManager.has(windowType)) return\n    windowManager.get(windowType)?.webContents.send(APP_CONFIG_UPDATED)\n  })\n}\n"
  },
  {
    "path": "src/main/utils/beforeOpen.ts",
    "content": "import fs from 'fs-extra'\nimport path from 'path'\nimport os from 'os'\nimport { dbPathChecker } from 'apis/core/datastore/dbChecker'\nimport yaml from 'js-yaml'\nimport { i18nManager } from '~/main/i18n'\n// import { ILocales } from '~/universal/types/i18n'\nimport { getStaticPath } from '#/utils/staticPath'\n\nconst configPath = dbPathChecker()\nconst CONFIG_DIR = path.dirname(configPath)\n\nfunction beforeOpen () {\n  if (process.platform === 'darwin') {\n    resolveMacWorkFlow()\n  }\n  resolveClipboardImageGenerator()\n  resolveOtherI18nFiles()\n}\nfunction copyFileOutsideOfElectronAsar (\n  sourceInAsarArchive: string,\n  destOutsideAsarArchive: string\n) {\n  if (fs.existsSync(sourceInAsarArchive)) {\n    // file will be copied\n    if (fs.statSync(sourceInAsarArchive).isFile()) {\n      const file = destOutsideAsarArchive\n      const dir = path.dirname(file)\n      if (!fs.existsSync(dir)) {\n        fs.mkdirSync(dir, { recursive: true })\n      }\n      fs.writeFileSync(file, fs.readFileSync(sourceInAsarArchive))\n    } else if (fs.statSync(sourceInAsarArchive).isDirectory()) {\n      fs.readdirSync(sourceInAsarArchive).forEach(function (fileOrFolderName) {\n        copyFileOutsideOfElectronAsar(\n          `${sourceInAsarArchive}/${fileOrFolderName}`,\n          `${destOutsideAsarArchive}/${fileOrFolderName}`\n        )\n      })\n    }\n  }\n}\n\n/**\n * macOS 右键菜单\n */\nfunction resolveMacWorkFlow () {\n  const dest = `${os.homedir()}/Library/Services/Upload pictures with PicGo.workflow`\n  if (fs.existsSync(dest)) {\n    return true\n  } else {\n    try {\n      copyFileOutsideOfElectronAsar(getStaticPath('Upload pictures with PicGo.workflow'), dest)\n    } catch (e) {\n      console.log(e)\n    }\n  }\n}\n\nfunction diffFilesAndUpdate (filePath1: string, filePath2: string) {\n  try {\n    const file1 = fs.existsSync(filePath1) && fs.readFileSync(filePath1)\n    const file2 = fs.existsSync(filePath1) && fs.readFileSync(filePath2)\n\n    if (!file1 || !file2 || !file1.equals(file2)) {\n      fs.copyFileSync(filePath1, filePath2)\n    }\n  } catch (e) {\n    console.error(e)\n    fs.copyFileSync(filePath1, filePath2)\n  }\n}\n\n/**\n * 初始化剪贴板生成图片的脚本\n */\nfunction resolveClipboardImageGenerator () {\n  const clipboardFiles = getClipboardFiles()\n  if (!fs.pathExistsSync(path.join(CONFIG_DIR, 'windows10.ps1'))) {\n    clipboardFiles.forEach(item => {\n      fs.copyFileSync(item.origin, item.dest)\n    })\n  } else {\n    clipboardFiles.forEach(item => {\n      diffFilesAndUpdate(item.origin, item.dest)\n    })\n  }\n\n  function getClipboardFiles () {\n    const files = [\n      'linux.sh',\n      'mac.applescript',\n      'windows.ps1',\n      'windows10.ps1',\n      'wsl.sh'\n    ]\n\n    return files.map(item => {\n      return {\n        origin: getStaticPath(item),\n        dest: path.join(CONFIG_DIR, item)\n      }\n    })\n  }\n}\n\n/**\n * 初始化其他语言文件\n */\nfunction resolveOtherI18nFiles () {\n  const i18nFolder = path.join(CONFIG_DIR, 'i18n')\n  if (!fs.pathExistsSync(i18nFolder)) {\n    fs.mkdirSync(i18nFolder)\n  }\n  i18nManager.setOuterI18nFolder(i18nFolder)\n  const i18nFiles = fs.readdirSync(path.join(CONFIG_DIR, 'i18n'), {\n    withFileTypes: true\n  })\n  i18nFiles.forEach(item => {\n    if (item.isFile() && item.name?.endsWith('.yml')) {\n      const i18nFilePath = path.join(i18nFolder, item.name)\n      const i18nFile = fs.readFileSync(i18nFilePath, 'utf8')\n      try {\n        const i18nFileObj = yaml.load(i18nFile) as unknown as ILocales\n        if (i18nFileObj?.LANG_DISPLAY_LABEL) {\n          i18nManager.addI18nFile(item.name.replace('.yml', ''), i18nFileObj.LANG_DISPLAY_LABEL)\n        }\n      } catch (e) {\n        console.error(e)\n      }\n    }\n  })\n}\n\nexport default beforeOpen\n"
  },
  {
    "path": "src/main/utils/cleanupFormUploaderFiles.ts",
    "content": "import path from 'path'\nimport fs from 'fs-extra'\nimport { getFormImageFolderPath } from '@core/datastore/dbChecker'\nimport logger from '@core/picgo/logger'\n\nexport const cleanupFormUploaderFiles = (fileInfoList?: string[] | ImgInfo[]): void => {\n  const formImageFolderPath = getFormImageFolderPath()\n  if (Array.isArray(fileInfoList)) {\n    fileInfoList.forEach(async fileInfo => {\n      const filePath = typeof fileInfo === 'string' ? fileInfo : fileInfo?.imgPath\n      if (!filePath) {\n        return\n      }\n      try {\n        // 检查文件路径是否在 formImageFolderPath 目录下\n        const relativePath = path.relative(formImageFolderPath, filePath)\n        const isWithinFolder = !relativePath.startsWith('..') && !path.isAbsolute(relativePath)\n\n        if (isWithinFolder && await fs.pathExists(filePath)) {\n          await fs.remove(filePath)\n          logger.info(`[PicGo] Deleted temp file: ${filePath}`)\n        }\n      } catch (error: any) {\n        logger.error('[PicGo] Failed to delete temp file', filePath, error)\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "src/main/utils/common.ts",
    "content": "import fs from 'fs-extra'\nimport logger from '@core/picgo/logger'\nimport { clipboard, Notification, dialog } from 'electron'\nimport { handleUrlEncode } from '~/universal/utils/common'\nimport { readClipboardFilePaths } from 'clip-filepaths'\nimport crypto from 'node:crypto'\nimport picgo from '@core/picgo'\n\nexport const handleCopyUrl = (str: string): void => {\n  if (picgo.getConfig<boolean>('settings.autoCopyUrl') !== false) {\n    clipboard.writeText(str)\n  }\n}\n\n/**\n * show notification\n * @param options\n */\nexport const showNotification = (options: IPrivateShowNotificationOption = {\n  title: '',\n  body: '',\n  text: '',\n  clickToCopy: false,\n  copyContent: '',\n  callback: () => {}\n}) => {\n  if (options.text) {\n    logger.info('[PicGo Notification]', options.text)\n    clipboard.writeText(options.text)\n  }\n\n  const title = options.title || ''\n  const body = options.body || options.text || ''\n  const silent = picgo.getConfig('settings.notificationSound') === false\n  const notification = new Notification({\n    title,\n    body,\n    silent\n    // icon: options.icon || undefined\n  })\n  const handleClick = () => {\n    if (options.clickToCopy) {\n      clipboard.writeText(options.copyContent || body)\n    }\n    if (options.callback) {\n      options.callback()\n    }\n  }\n  notification.once('click', handleClick)\n  notification.once('close', () => {\n    notification.removeListener('click', handleClick)\n  })\n  notification.show()\n}\n\nexport const showMessageBox = (options: any) => {\n  return new Promise<IShowMessageBoxResult>(async (resolve) => {\n    dialog.showMessageBox(\n      options\n    ).then((res) => {\n      resolve({\n        result: res.response,\n        checkboxChecked: res.checkboxChecked\n      })\n    })\n  })\n}\n\nexport const calcUploadProcessDurationRange = (duration: number) => {\n  if (duration < 1000) {\n    return 500\n  } else if (duration < 1500) {\n    return 1000\n  } else if (duration < 3000) {\n    return 2000\n  } else if (duration < 5000) {\n    return 3000\n  } else if (duration < 7000) {\n    return 5000\n  } else if (duration < 10000) {\n    return 8000\n  } else if (duration < 12000) {\n    return 10000\n  } else if (duration < 20000) {\n    return 15000\n  } else if (duration < 30000) {\n    return 20000\n  }\n  // max range\n  return 100000\n}\n\n// 1 2 3 4 5 6 7 8 9 10 20 30 40 50 60 70 80 90 100 200 300 ...\nexport const calcUploadBigFileSizeRange = (fileSizeMB: number) => {\n  if (fileSizeMB < 10) {\n    // 3.2 -> 3, 3.6 -> 4\n    const result = Math.round(fileSizeMB)\n    return result === 0 && fileSizeMB > 0 ? 1 : result \n  } \n  else if (fileSizeMB < 100) {\n    // 13 -> 1.3 -> 1 -> 10\n    // 17 -> 1.7 -> 2 -> 20\n    return Math.round(fileSizeMB / 10) * 10\n  } \n  else {\n    // 135 -> 1.35 -> 1 -> 100\n    // 160 -> 1.60 -> 2 -> 200\n    return Math.round(fileSizeMB / 100) * 100\n  }\n}\n\n// 1 2 3 4 5 6 7 8 9 10 20 30 40 50 60 70 80 90 100 200 300 ...\nexport const calcVideoDurationRange = (durationSec: number) => {\n  if (durationSec < 10) {\n    // 3.2 -> 3, 3.6 -> 4\n    const result = Math.round(durationSec)\n    return result === 0 && durationSec > 0 ? 1 : result \n  } \n  else if (durationSec < 100) {\n    // 13 -> 1.3 -> 1 -> 10\n    // 17 -> 1.7 -> 2 -> 20\n    return Math.round(durationSec / 10) * 10\n  } \n  else {\n    // 135 -> 1.35 -> 1 -> 100\n    // 160 -> 1.60 -> 2 -> 200\n    return Math.round(durationSec / 100) * 100\n  }\n}\n\n/**\n * macOS public.file-url will get encoded file path,\n * so we need to decode it\n */\nexport const ensureFilePath = (filePath: string, prefix = 'file://'): string => {\n  filePath = filePath.replace(prefix, '')\n  if (fs.existsSync(filePath)) {\n    return `${prefix}${filePath}`\n  }\n  filePath = decodeURIComponent(filePath)\n  if (fs.existsSync(filePath)) {\n    return `${prefix}${filePath}`\n  }\n  return ''\n}\n\n/**\n * for builtin clipboard to get image path from clipboard\n * @returns\n */\nexport const getClipboardFilePathList = (): string[] => {\n  const { filePaths } = readClipboardFilePaths()\n  return filePaths\n}\n\nexport const handleUrlEncodeWithSetting = (url: string) => {\n  if (picgo.getConfig<boolean>('settings.encodeOutputURL') === true) {\n    url = handleUrlEncode(url)\n  }\n  return url\n}\n\nexport const replaceHost = (url: string, oldHost: string, newHost: string) => {\n  try {\n    const parsedUrl = new URL(url)\n    if (parsedUrl.host === oldHost) {\n      parsedUrl.host = newHost\n    }\n    return parsedUrl.toString()\n  } catch {\n    return url\n  }\n}\n\nexport const getHost = (url: string = '') => {\n  try {\n    const parsedUrl = new URL(url)\n    return parsedUrl.host\n  } catch {\n    return ''\n  }\n}\n\n/**\n * remove protocol and suffix\n */\nexport const removeProtocolAndSuffix = (url: string = '') => {\n  return url.replace(/^(https?:\\/\\/)?/, '').replace(/\\/$/, '')\n}\n\nexport const md5 = (str: string): string => {\n  return crypto.createHash('md5').update(str).digest('hex')\n}\n"
  },
  {
    "path": "src/main/utils/constants.ts",
    "content": "export const MB = 1024 * 1024\nexport const SECOND = 1000"
  },
  {
    "path": "src/main/utils/dataReport.ts",
    "content": "import { ipcMain, IpcMainEvent, type WebContents } from \"electron\"\nimport { deviceIdManager } from \"./deviceId\"\nimport { REGISTER_DEVICE_ID, TALKING_DATA_EVENT } from \"~/universal/events/constants\"\nimport type { IImgInfo } from \"picgo\"\nimport { calcUploadBigFileSizeRange, calcUploadProcessDurationRange, calcVideoDurationRange } from \"./common\"\nimport { app } from \"electron/main\"\nimport picgo from \"@core/picgo\"\nimport { getVideoDuration } from \"@picgo/video-duration\"\nimport { MB, SECOND } from \"./constants\"\n\nexport interface IReportUploadDataOptions {\n  fromClipboard: boolean;\n  duration: number;\n  outputList: IImgInfo[];\n}\n\nclass DataReportManager {\n  private deviceId: string | null = null\n  private hasRegisterDeviceID: boolean = false\n  constructor () {\n    this.init()\n    this.handleRegisterDeviceID()\n  }\n  private handleRegisterDeviceID() {\n    ipcMain.once(REGISTER_DEVICE_ID, (_evt: IpcMainEvent) => {\n      this.hasRegisterDeviceID = true\n      console.log('Device ID registered')\n    })\n  }\n  private async init () {\n    if (this.deviceId) return\n    this.deviceId = await deviceIdManager.getId()\n  }\n  public async reportUploadData(webContents: WebContents, options: IReportUploadDataOptions) {\n    await this.init()\n    await this.registerDeviceID(webContents)\n    const { fromClipboard, duration, outputList } = options\n    const fileList = outputList.map(item => {\n      return {\n        fileName: item.fileName,\n        filePath: item.filePath,\n        mimeType: item.mimeType,\n        size: item.size || 0\n      }\n    })\n    const uploadEventData: ITalkingDataOptions = {\n      EventId: 'upload',\n      Label: '',\n      MapKv: {\n        by: fromClipboard ? 'clipboard' : 'files', // 上传剪贴板图片还是选择的文文件\n        count: fileList.length, // 上传的数量\n        duration: calcUploadProcessDurationRange(duration || 0), // 上传耗时\n        type: picgo.getConfig<string>('picBed.uploader') || picgo.getConfig<string>('picBed.current') || 'smms',\n      }\n    }\n    this.reportDataToWebContents(webContents, uploadEventData)\n    fileList.forEach(async file => {\n      if (file?.mimeType?.startsWith('video/') && file.filePath) {\n        const metadata = await getVideoDuration(file.filePath)\n        const sizeMB = metadata.size / MB\n        const durationSec = (metadata.duration / SECOND) || 0\n        const videoEventData: ITalkingDataOptions = {\n          EventId: 'upload_video',\n          Label: '',\n          MapKv: {\n            type: picgo.getConfig<string>('picBed.uploader') || picgo.getConfig<string>('picBed.current') || 'smms',\n            mimeType: file.mimeType,\n            sizeRange: calcUploadBigFileSizeRange(sizeMB),\n            durationRange: calcVideoDurationRange(durationSec),\n          }\n        }\n        this.reportDataToWebContents(webContents, videoEventData)\n      } else if (file?.mimeType?.startsWith('image/') || fromClipboard) {\n        const imageEventData: ITalkingDataOptions = {\n          EventId: 'upload_image',\n          Label: '',\n          MapKv: {\n            type: picgo.getConfig<string>('picBed.uploader') || picgo.getConfig<string>('picBed.current') || 'smms',\n            mimeType: file.mimeType || 'image/png',\n            sizeRange: calcUploadBigFileSizeRange(file.size / MB)\n          }\n        }\n        this.reportDataToWebContents(webContents, imageEventData)\n      } else {\n        const otherEventData: ITalkingDataOptions = {\n          EventId: 'upload_file',\n          Label: '',\n          MapKv: {\n            type: picgo.getConfig<string>('picBed.uploader') || picgo.getConfig<string>('picBed.current') || 'smms',\n            mimeType: file.mimeType || 'UNKNOWN',\n            sizeRange: calcUploadBigFileSizeRange(file.size / MB)\n          }\n        }\n        this.reportDataToWebContents(webContents, otherEventData)\n      }\n    })\n  }\n\n  public async registerDeviceID(webContents: WebContents) {\n    if (this.hasRegisterDeviceID) return\n    await this.init()\n    webContents.send(REGISTER_DEVICE_ID, this.deviceId)\n  }\n\n  private reportDataToWebContents(webContents: WebContents, data: ITalkingDataOptions) {\n    webContents.send(TALKING_DATA_EVENT, {\n      ...data,\n      MapKv: {\n        ...data.MapKv,\n        deviceId: this.deviceId,\n        version: app.getVersion(),\n        area: this.getAreaFromTimezone()\n      }\n    })\n  }\n\n  private getAreaFromTimezone() {\n    try {\n      const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone\n      if (!timeZone) return 'UNKNOWN'\n    \n      if (timeZone === 'Asia/Shanghai') return 'CN'\n    \n      const region = timeZone.split('/')[0] // Asia, America, Europe\n      return region \n    } catch (e) {\n      return 'UNKNOWN'\n    }\n  }\n}\n\nexport const dataReportManager = new DataReportManager()\n"
  },
  {
    "path": "src/main/utils/deviceId.ts",
    "content": "import { networkInterfaceDefault } from 'systeminformation'\nimport { md5 } from './common'\nimport writeFile from 'write-file-atomic'\nimport { DEVICE_ID_PATH } from './env'\nimport fs from 'fs-extra'\n\nclass DeviceIdManager {\n  private deviceId: string | null = null\n\n  constructor() {\n    this.init()\n  }\n\n  private async init() {\n    await this.loadDeviceId()\n  }\n\n  private async getDeviceIdWithFallback(): Promise<string> {\n    try {\n      if (fs.existsSync(DEVICE_ID_PATH)) {\n        const deviceId = await fs.readFile(DEVICE_ID_PATH, 'utf-8')\n        if (deviceId && deviceId?.trim().length > 0) {\n          return deviceId.trim()\n        }\n      }\n      const netInterfaceMac = await networkInterfaceDefault()\n      if (netInterfaceMac) {\n        return md5(netInterfaceMac)\n      } else {\n        // random fallback\n        return md5(`${new Date().getTime()}-${Math.random().toString(36).substring(2, 15)}`)\n      }\n    } catch (error) {\n      // random fallback\n      return md5(`${new Date().getTime()}-${Math.random().toString(36).substring(2, 15)}`)\n    }\n  }\n\n  private async saveDeviceId(id: string): Promise<void> {\n    try {\n      await writeFile(DEVICE_ID_PATH, id, { encoding: 'utf-8' })\n    } catch (error) {\n      console.error('Failed to save device ID:', error)\n    }\n  }\n\n  private async loadDeviceId(): Promise<string> {\n    this.deviceId = await this.getDeviceIdWithFallback()\n    await this.saveDeviceId(this.deviceId)\n    return this.deviceId\n  }\n\n  public async getId(): Promise<string> {\n    if (this.deviceId) {\n      return this.deviceId\n    }\n    this.deviceId = await this.loadDeviceId()\n    return this.deviceId\n  }\n}\n\nexport const deviceIdManager = new DeviceIdManager()\n"
  },
  {
    "path": "src/main/utils/env.ts",
    "content": "import path from 'path'\nimport { app } from 'electron'\nimport { pathToFileURL } from 'url'\n\nexport const isDev = !app.isPackaged\n\nconst defaultStaticPath = process.env.STATIC_PATH || (isDev\n  ? path.join(process.cwd(), 'public')\n  : path.join(process.resourcesPath, 'public'))\n\nprocess.env.STATIC_PATH = defaultStaticPath\n\nconst rendererHtmlPath = path.join(__dirname, '../renderer/index.html')\n\nexport const initStaticPath = () => defaultStaticPath\n\nexport const getRendererBaseUrl = () => {\n  if (isDev && process.env.ELECTRON_RENDERER_URL) {\n    return process.env.ELECTRON_RENDERER_URL\n  }\n  return pathToFileURL(rendererHtmlPath).toString()\n}\n\nexport const buildRendererUrl = (hash?: string) => {\n  const base = getRendererBaseUrl()\n  if (!hash) return base\n  const cleanedHash = hash.startsWith('#') ? hash : `#${hash}`\n  return `${base}${cleanedHash}`\n}\n\nexport const getStaticPath = () => process.env.STATIC_PATH || defaultStaticPath\n\n// paths\nexport const STORE_PATH = app.getPath('userData')\nexport const DEVICE_ID_PATH = path.join(STORE_PATH, 'picgo-device-id')\n"
  },
  {
    "path": "src/main/utils/getLatestVersion.ts",
    "content": "// for referer policy, we can't use it in renderer\nimport axios from 'axios'\nimport { RELEASE_URL, RELEASE_URL_BACKUP } from '../../universal/utils/static'\nimport semver from 'semver'\nimport yaml from 'js-yaml'\n\ninterface IGithubRelease {\n  tag_name?: string\n  name?: string\n  prerelease?: boolean\n  draft?: boolean\n}\n\ninterface IReleaseYAML {\n  version?: unknown\n}\n\nconst REQUEST_HEADERS = {\n  Referer: 'https://github.com'\n}\n\nfunction normalizeVersion (version: unknown): string {\n  if (typeof version !== 'string') {\n    return ''\n  }\n  const normalized = version.trim().replace(/^v/i, '')\n  return semver.valid(normalized) ?? ''\n}\n\nfunction pickLatestVersion (versions: string[]): string {\n  return versions.reduce((latest, current) => {\n    if (latest === '' || semver.gt(current, latest)) {\n      return current\n    }\n    return latest\n  }, '')\n}\n\nasync function fetchLatestVersionFromGitHub (isCheckBetaUpdate: boolean): Promise<string> {\n  const response = await axios.get(RELEASE_URL, {\n    headers: REQUEST_HEADERS\n  })\n  const releaseList: IGithubRelease[] = Array.isArray(response.data) ? response.data : []\n\n  const versions = releaseList.flatMap((release) => {\n    if (release.draft) {\n      return []\n    }\n    if (!isCheckBetaUpdate && release.prerelease) {\n      return []\n    }\n\n    const version = normalizeVersion(release.tag_name ?? release.name)\n    return version ? [version] : []\n  })\n\n  return pickLatestVersion(versions)\n}\n\nasync function fetchVersionFromBackupYAML (fileName: 'latest.yml' | 'latest.beta.yml'): Promise<string> {\n  const response = await axios.get(`${RELEASE_URL_BACKUP}/${fileName}`, {\n    headers: REQUEST_HEADERS\n  })\n  const releaseInfo = yaml.load(response.data)\n\n  if (typeof releaseInfo !== 'object' || releaseInfo === null) {\n    return ''\n  }\n\n  return normalizeVersion((releaseInfo as IReleaseYAML).version)\n}\n\nasync function fetchLatestVersionFromBackup (isCheckBetaUpdate: boolean): Promise<string> {\n  if (!isCheckBetaUpdate) {\n    return fetchVersionFromBackupYAML('latest.yml')\n  }\n\n  const settled = await Promise.allSettled([\n    fetchVersionFromBackupYAML('latest.yml'),\n    fetchVersionFromBackupYAML('latest.beta.yml')\n  ])\n\n  const versions: string[] = []\n  settled.forEach((item) => {\n    if (item.status === 'fulfilled' && item.value) {\n      versions.push(item.value)\n    }\n  })\n\n  return pickLatestVersion(versions)\n}\n\nexport const getLatestVersion = async (isCheckBetaUpdate: boolean = false) => {\n  try {\n    const version = await fetchLatestVersionFromGitHub(isCheckBetaUpdate)\n    if (version) {\n      return version\n    }\n  } catch (err) {\n    console.log(err)\n  }\n\n  try {\n    return await fetchLatestVersionFromBackup(isCheckBetaUpdate)\n  } catch (err) {\n    console.log(err)\n    return ''\n  }\n}\n"
  },
  {
    "path": "src/main/utils/getMacOSVersion.ts",
    "content": "// fork from https://github.com/sindresorhus/macos-version\n// cause I can't change it to common-js module\nimport process from 'process'\nimport fs from 'fs'\nimport semver from 'semver'\n\nexport const isMacOS = process.platform === 'darwin'\n\nlet version: string | undefined\n\nconst clean = (version: string) => {\n  const { length } = version.split('.')\n\n  if (length === 1) {\n    return `${version}.0.0`\n  }\n\n  if (length === 2) {\n    return `${version}.0`\n  }\n\n  return version\n}\n\nconst parseVersion = (plist: string) => {\n  const matches = /<key>ProductVersion<\\/key>\\s*<string>([\\d.]+)<\\/string>/.exec(plist)\n  if (!matches) {\n    return\n  }\n\n  return matches[1].replace('10.16', '11')\n}\n\nexport function macOSVersion (): string {\n  if (!isMacOS) {\n    return ''\n  }\n\n  if (!version) {\n    const file = fs.readFileSync('/System/Library/CoreServices/SystemVersion.plist', 'utf8')\n    const matches = parseVersion(file)\n\n    if (!matches) {\n      return ''\n    }\n\n    version = clean(matches)\n  }\n\n  return version\n}\n\nif (process.env.NODE_ENV === 'test') {\n  macOSVersion._parseVersion = parseVersion\n}\n\nexport function isMacOSVersion (semverRange: string) {\n  if (!isMacOS) {\n    return false\n  }\n\n  semverRange = semverRange.replace('10.16', '11')\n\n  return semver.satisfies(macOSVersion(), clean(semverRange))\n}\n\nexport function isMacOSVersionGreaterThanOrEqualTo (version: string) {\n  if (!isMacOS) {\n    return false\n  }\n\n  version = version.replace('10.16', '11')\n\n  return semver.gte(macOSVersion(), clean(version))\n}\n\nexport function assertMacOSVersion (semverRange: string) {\n  semverRange = semverRange.replace('10.16', '11')\n\n  if (!isMacOSVersion(semverRange)) {\n    throw new Error(`Requires macOS ${semverRange}`)\n  }\n}\n\nexport function assertMacOSVersionGreaterThanOrEqualTo (version: string) {\n  version = version.replace('10.16', '11')\n\n  if (!isMacOSVersionGreaterThanOrEqualTo(version)) {\n    throw new Error(`Requires macOS ${version} or later`)\n  }\n}\n\nexport function assertMacOS () {\n  if (!isMacOS) {\n    throw new Error('Requires macOS')\n  }\n}\n"
  },
  {
    "path": "src/main/utils/getPicBeds.ts",
    "content": "import picgo from '@core/picgo'\n\nconst getPicBeds = () => {\n  const picBedTypes = picgo.helper.uploader.getIdList()\n  const picBedFromDB = picgo.getConfig<IPicBedType[]>('picBed.list') || []\n  const picBeds = picBedTypes.map((item: string) => {\n    const visible = picBedFromDB.find((i: IPicBedType) => i.type === item) // object or undefined\n    return {\n      type: item,\n      name: picgo.helper.uploader.get(item)!.name || item,\n      visible: visible ? visible.visible : true\n    }\n  }).sort((a) => {\n    if (a.type === 'tcyun') {\n      return -1\n    }\n    return 0\n  }) as IPicBedType[]\n  return picBeds\n}\n\nexport default getPicBeds\n"
  },
  {
    "path": "src/main/utils/handleArgv.ts",
    "content": "import path from 'path'\nimport fs from 'fs-extra'\nimport { Logger } from 'picgo'\nimport { isUrl } from '~/universal/utils/common'\ninterface IResultFileObject {\n  path: string\n}\ntype Result = IResultFileObject[]\n\ninterface IHandleArgvResult {\n  success: boolean\n  fileList?: string[]\n}\n\nconst handleArgv = (argv: string[]): IHandleArgvResult => {\n  const uploadIndex = argv.indexOf('upload')\n  if (uploadIndex !== -1) {\n    const fileList = argv.slice(1 + uploadIndex)\n    return {\n      success: true,\n      fileList\n    }\n  }\n  return {\n    success: false\n  }\n}\n\nconst getUploadFiles = (argv = process.argv, cwd = process.cwd(), logger: Logger) => {\n  const { success, fileList } = handleArgv(argv)\n  if (!success) {\n    return []\n  } else {\n    if (fileList?.length === 0) {\n      return null // for uploading images in clipboard\n    } else if ((fileList?.length || 0) > 0) {\n      const result = fileList!.map(item => {\n        if (isUrl(item)) {\n          return {\n            path: item\n          }\n        }\n        if (path.isAbsolute(item)) {\n          return {\n            path: item\n          }\n        } else {\n          const tempPath = path.join(cwd, item)\n          if (fs.existsSync(tempPath)) {\n            return {\n              path: tempPath\n            }\n          } else {\n            logger.warn(`cli -> can't get file: ${tempPath}, invalid path`)\n            return null\n          }\n        }\n      }).filter(item => item !== null) as Result\n      return result\n    }\n  }\n  return []\n}\n\nexport {\n  getUploadFiles\n}\n"
  },
  {
    "path": "src/main/utils/handleI18n.ts",
    "content": "import picgo from '@core/picgo'\nimport { i18nManager } from '~/main/i18n'\nexport const initI18n = () => {\n  const currentLanguage = picgo.getConfig<string>('settings.language') || 'en'\n  i18nManager.setCurrentLanguage(currentLanguage)\n}\n"
  },
  {
    "path": "src/main/utils/handleUploaderConfig.ts",
    "content": "import { simpleClone, trimValues } from '#/utils/common'\nimport picgo from '@core/picgo'\nimport { v4 as uuid } from 'uuid'\n\nexport const handleConfigWithFunction = (config: IPicGoPluginOriginConfig[]): IPicGoPluginConfig[] => {\n  for (const i in config) {\n    if (typeof config[i].default === 'function') {\n      config[i].default = config[i].default()\n    }\n    if (typeof config[i].choices === 'function') {\n      config[i].choices = (config[i].choices as Function)()\n    }\n  }\n  return config as IPicGoPluginConfig[]\n}\n\nexport const completeUploaderMetaConfig = (originData: IStringKeyMap): IUploaderConfigListItem => {\n  return Object.assign({\n    _configName: 'Default'\n  }, trimValues(originData), {\n    _id: uuid(),\n    _createdAt: Date.now(),\n    _updatedAt: Date.now()\n  })\n}\n\n/**\n * get picbed config by type\n * it will trigger the uploader config function & get the uploader config result\n * & not just read from\n */\nexport const getPicBedConfig = (type: string) => {\n  const name = picgo.helper.uploader.get(type)?.name || type\n  if (picgo.helper.uploader.get(type)?.config) {\n    const _config = picgo.helper.uploader.get(type)!.config!(picgo)\n    const config = handleConfigWithFunction(_config)\n    return {\n      config,\n      name\n    }\n  } else {\n    return {\n      config: [],\n      name\n    }\n  }\n}\n\nexport const changeCurrentUploader = (type: string, config?: IStringKeyMap, id?: string) => {\n  if (!type) {\n    return\n  }\n  if (id) {\n    picgo.saveConfig({\n      [`uploader.${type}.defaultId`]: id\n    })\n  }\n  if (config) {\n    picgo.saveConfig({\n      [`picBed.${type}`]: config\n    })\n  }\n  picgo.saveConfig({\n    'picBed.current': type,\n    'picBed.uploader': type\n  })\n}\n\nexport const selectUploaderConfig = (type: string, id: string) => {\n  const { configList } = getUploaderConfigList(type)\n  const config = configList.find((item: IStringKeyMap) => item._id === id)\n  if (config) {\n    picgo.saveConfig({\n      [`uploader.${type}.defaultId`]: id,\n      [`picBed.${type}`]: config\n    })\n  }\n}\n\nexport const getUploaderConfigList = (type: string): IUploaderConfigItem => {\n  if (!type) {\n    return {\n      configList: [],\n      defaultId: ''\n    }\n  }\n  const currentUploaderConfig = picgo.getConfig<IStringKeyMap>(`uploader.${type}`) ?? {}\n  let configList = currentUploaderConfig.configList\n  let defaultId = currentUploaderConfig.defaultId || ''\n  if (!configList) {\n    const res = upgradeUploaderConfig(type)\n    configList = res.configList\n    defaultId = res.defaultId\n  }\n  return {\n    configList,\n    defaultId\n  }\n}\n\n/**\n * delete uploader config by type & id\n */\nexport const deleteUploaderConfig = (type: string, id: string): IUploaderConfigItem | void => {\n  const { configList, defaultId } = getUploaderConfigList(type)\n  if (configList.length <= 1) {\n    return\n  }\n  let newDefaultId = defaultId\n  const updatedConfigList = configList.filter((item: IStringKeyMap) => item._id !== id)\n  if (id === defaultId) {\n    newDefaultId = updatedConfigList[0]._id\n    changeCurrentUploader(type, updatedConfigList[0], updatedConfigList[0]._id)\n  }\n  picgo.saveConfig({\n    [`uploader.${type}.configList`]: updatedConfigList\n  })\n  return {\n    configList: updatedConfigList,\n    defaultId: newDefaultId\n  }\n}\n\n/**\n * copy uploader config by type & id\n */\nexport const copyUploaderConfig = (type: string, id: string): IUploaderConfigItem | void => {\n  const { configList, defaultId } = getUploaderConfigList(type)\n  const existConfig = configList.find((item: IStringKeyMap) => item._id === id)\n  if (!existConfig) {\n    return\n  }\n  const copiedConfig = completeUploaderMetaConfig({\n    ...simpleClone(existConfig),\n    _configName: `${existConfig._configName || 'Default'} - Copy`\n  })\n  configList.push(copiedConfig)\n  picgo.saveConfig({\n    [`uploader.${type}.configList`]: configList\n  })\n  return {\n    configList,\n    defaultId\n  }\n}\n\n/**\n * upgrade old uploader config to new format\n */\nexport const upgradeUploaderConfig = (type: string): {\n  configList: IStringKeyMap[]\n  defaultId: string\n} => {\n  const uploaderConfig = picgo.getConfig<IStringKeyMap>(`picBed.${type}`) ?? {}\n  if (!uploaderConfig._id) {\n    Object.assign(uploaderConfig, completeUploaderMetaConfig(uploaderConfig))\n  }\n\n  const uploaderConfigList = [uploaderConfig]\n  picgo.saveConfig({\n    [`uploader.${type}`]: {\n      configList: uploaderConfigList,\n      defaultId: uploaderConfig._id\n    },\n    [`picBed.${type}`]: uploaderConfig\n  })\n  return {\n    configList: uploaderConfigList,\n    defaultId: uploaderConfig._id\n  }\n}\n\nexport const updateUploaderConfig = (type: string, id: string, config: IStringKeyMap) => {\n  const { configList, defaultId } = getUploaderConfigList(type)\n  const existConfig = configList.find((item: IStringKeyMap) => item._id === id)\n  let updatedConfig: IUploaderConfigListItem\n  let updatedDefaultId = defaultId\n  if (existConfig) {\n    updatedConfig = Object.assign(existConfig, trimValues(config), {\n      _updatedAt: Date.now()\n    })\n  } else {\n    updatedConfig = completeUploaderMetaConfig(config)\n    updatedDefaultId = updatedConfig._id\n    configList.push(updatedConfig)\n  }\n  picgo.saveConfig({\n    [`uploader.${type}.configList`]: configList,\n    [`uploader.${type}.defaultId`]: updatedDefaultId,\n    [`picBed.${type}`]: updatedConfig\n  })\n}\n"
  },
  {
    "path": "src/main/utils/pasteTemplate.ts",
    "content": "import { IPasteStyle } from '#/types/enum'\nimport { handleUrlEncodeWithSetting } from './common'\n\nconst formatCustomLink = (customLink: string, item: ImgInfo) => {\n  const fileName = item.fileName!.replace(new RegExp(`\\\\${item.extname}$`), '')\n  const url = item.url || item.imgUrl\n  const extName = item.extname\n  const formatObj = {\n    url,\n    fileName,\n    extName\n  }\n  const keys = Object.keys(formatObj) as ['url', 'fileName', 'extName']\n  keys.forEach(item => {\n    if (customLink.indexOf(`$${item}`) !== -1) {\n      const reg = new RegExp(`\\\\$${item}`, 'g')\n      customLink = customLink.replace(reg, formatObj[item])\n    }\n  })\n  return customLink\n}\n\nexport default (style: IPasteStyle, item: ImgInfo, customLink: string | undefined) => {\n  const url = handleUrlEncodeWithSetting(item.url || item.imgUrl)\n  const _customLink = customLink || '$url'\n  const tpl = {\n    markdown: `![](${url})`,\n    HTML: `<img src=\"${url}\"/>`,\n    URL: url,\n    UBB: `[IMG]${url}[/IMG]`,\n    Custom: formatCustomLink(_customLink, {\n      ...item,\n      url\n    })\n  }\n  return tpl[style]\n}\n"
  },
  {
    "path": "src/main/utils/privacyManager.ts",
    "content": "import picgo from '@core/picgo'\nimport { showMessageBox } from '~/main/utils/common'\nimport { T } from '~/main/i18n'\n\nclass PrivacyManager {\n  async check () {\n    if (picgo.getConfig<string | boolean>('settings.privacyEnsure') !== '20260127') {\n      const res = await this.show(true)\n      // cancel\n      if (res.result === 1) {\n        return false\n      } else {\n        picgo.saveConfig({ 'settings.privacyEnsure': '20260127' })\n        return true\n      }\n    }\n    return true\n  }\n\n  async show (showCancel = true) {\n    const privacyUrl = 'https://picgo.app/privacy/'\n    const termsUrl = 'https://picgo.app/terms/'\n    const res = await showMessageBox({\n      type: 'info',\n      buttons: showCancel ? ['Yes', 'No'] : ['Yes'],\n      title: T('PRIVACY_TERMS_AGREEMENT'),\n      message: T('PRIVACY', {\n        privacyUrl,\n        termsUrl\n      })\n    })\n    return res\n  }\n}\n\nconst privacyManager = new PrivacyManager()\n\nexport {\n  privacyManager\n}\n"
  },
  {
    "path": "src/main/utils/updateChecker.ts",
    "content": "import { dialog, shell } from 'electron'\nimport picgo from '@core/picgo'\nimport pkg from 'root/package.json'\nimport { lt } from 'semver'\nimport { T } from '~/main/i18n'\nimport { getLatestVersion } from '~/main/utils/getLatestVersion'\nconst version = pkg.version\n// const releaseUrl = 'https://api.github.com/repos/Molunerfinn/PicGo/releases'\n// const releaseUrlBackup = 'https://picgo-1251750343.cos.ap-chengdu.myqcloud.com'\nconst downloadUrl = 'https://github.com/Molunerfinn/PicGo/releases/latest'\n\nconst checkVersion = async () => {\n  let showTip = picgo.getConfig<boolean | undefined>('settings.showUpdateTip')\n  if (showTip === undefined) {\n    picgo.saveConfig({ 'settings.showUpdateTip': true })\n    showTip = true\n  }\n  if (showTip) {\n    const isCheckBetaUpdate = picgo.getConfig<boolean>('settings.checkBetaUpdate') !== false\n    const res: string = await getLatestVersion(isCheckBetaUpdate)\n    if (res !== '') {\n      const latest = res\n      const result = compareVersion2Update(version, latest)\n      if (result) {\n        dialog.showMessageBox({\n          type: 'info',\n          title: T('FIND_NEW_VERSION'),\n          buttons: ['Yes', 'No'],\n          message: T('TIPS_FIND_NEW_VERSION', {\n            v: latest\n          }),\n          checkboxLabel: T('NO_MORE_NOTICE'),\n          checkboxChecked: false\n        }).then(res => {\n          if (res.response === 0) { // if selected yes\n            shell.openExternal(downloadUrl)\n          }\n          picgo.saveConfig({ 'settings.showUpdateTip': !res.checkboxChecked })\n        })\n      }\n    } else {\n      return false\n    }\n  } else {\n    return false\n  }\n}\n\n// if true -> update else return false\nconst compareVersion2Update = (current: string, latest: string) => {\n  try {\n    if (latest.includes('beta')) {\n      const isCheckBetaUpdate = picgo.getConfig<boolean>('settings.checkBetaUpdate') !== false\n      if (!isCheckBetaUpdate) {\n        return false\n      }\n    }\n    return lt(current, latest)\n  } catch (e) {\n    return false\n  }\n}\n\nexport default checkVersion\n"
  },
  {
    "path": "src/preload/index.ts",
    "content": "// temp no used\n// will be refactor in future\nimport { contextBridge, webUtils } from 'electron'\n\nconst getFilePath = (file: File): string => webUtils.getPathForFile(file)\n\nconst electronApi = {\n  getFilePath\n}\n\ncontextBridge.exposeInMainWorld('electronApi', electronApi)\n\nexport type ElectronApi = typeof electronApi\n"
  },
  {
    "path": "src/renderer/App.vue",
    "content": "<template>\n  <div id=\"app\">\n    <router-view />\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useStore } from '@/hooks/useStore'\nimport { onBeforeMount, onMounted, onUnmounted } from 'vue'\nimport bus from './utils/bus'\nimport { APP_CONFIG_UPDATED, FORCE_UPDATE } from '~/universal/events/constants'\nimport { useATagClick } from './hooks/useATagClick'\nimport { useIPCOn } from './hooks/useIPC'\n\nuseATagClick()\n\nconst store = useStore()\nconst handleAppConfigUpdated = () => {\n  store?.refreshAppConfig()\n  store?.refreshPicBeds()\n}\n\nonBeforeMount(async () => {\n  if (!store) return\n  await store.refreshAppConfig()\n  await store.refreshPicBeds()\n})\n\nuseIPCOn(APP_CONFIG_UPDATED, handleAppConfigUpdated)\n\nonMounted(() => {\n  bus.on(FORCE_UPDATE, () => {\n    store?.updateForceUpdateTime()\n  })\n})\n\nonUnmounted(() => {\n  bus.off(FORCE_UPDATE)\n})\n\n</script>\n\n<script lang=\"ts\">\nexport default {\n  name: 'PicGoApp'\n}\n</script>\n\n<style lang=\"stylus\">\n  body,\n  html\n    padding 0\n    margin 0\n    height 100%\n    font-family \"Helvetica Neue\",Helvetica,\"PingFang SC\",\"Hiragino Sans GB\",\"Microsoft YaHei\",\"微软雅黑\",Arial,sans-serif\n  #app\n    user-select none\n    overflow hidden\n  .el-button-group\n    width 100%\n    .el-button\n      width 50%\n</style>\n"
  },
  {
    "path": "src/renderer/assets/.gitkeep",
    "content": ""
  },
  {
    "path": "src/renderer/assets/css/tailwind.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;"
  },
  {
    "path": "src/renderer/assets/fonts/iconfont.css",
    "content": "\n@font-face {font-family: \"iconfont\";\n  src: url('iconfont.eot?t=1523001890286'); /* IE9*/\n  src: url('iconfont.eot?t=1523001890286#iefix') format('embedded-opentype'), /* IE6-IE8 */\n  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'),\n  url('iconfont.ttf?t=1523001890286') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/\n  url('iconfont.svg?t=1523001890286#iconfont') format('svg'); /* iOS 4.1- */\n}\n\n[class*=\" el-icon-ui\"], [class^=el-icon-ui] {\n  font-family:\"iconfont\" !important;\n  font-size:16px;\n  font-style:normal;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n.el-icon-ui-github:before { content: \"\\e7ab\"; }\n\n.el-icon-ui-weibo:before { content: \"\\e61c\"; }\n\n.el-icon-ui-tcyun:before { content: \"\\e64c\"; }\n\n.el-icon-ui-upload:before { content: \"\\e61b\"; }\n\n.el-icon-ui-qiniu:before { content: \"\\e601\"; }\n\n.el-icon-ui-upyun:before { content: \"\\e602\"; }\n\n"
  },
  {
    "path": "src/renderer/components/ConfigForm.vue",
    "content": "<template>\n  <div\n    id=\"config-form\"\n    :class=\"props.colorMode === 'white' ? 'white' : ''\"\n  >\n    <BaseConfigForm\n      ref=\"$form\"\n      :config=\"configList\"\n      :form-model=\"formModel\"\n    >\n      <template #extra-form>\n        <el-form-item\n          v-if=\"isUploader\"\n          :label=\"$T('UPLOADER_CONFIG_NAME')\"\n          required\n          prop=\"_configName\"\n        >\n          <el-input\n            v-model=\"formModel._configName\"\n            type=\"input\"\n            :placeholder=\"$T('UPLOADER_CONFIG_PLACEHOLDER')\"\n          />\n        </el-form-item>\n      </template>\n      <slot />\n    </BaseConfigForm>\n  </div>\n</template>\n<script lang=\"ts\" setup>\nimport { computed, reactive, ref, watch } from 'vue'\nimport { getConfig } from '@/utils/dataSender'\nimport { useRoute } from 'vue-router'\nimport BaseConfigForm from './form/BaseConfigForm.vue'\nimport { useConfigForm } from '@/hooks/useConfigForm'\nimport { useStore } from '@/hooks/useStore'\n\ninterface IProps {\n  config: IPicGoPluginConfig[]\n  type: 'uploader' | 'transformer' | 'plugin'\n  id: string\n  colorMode?: 'white' | 'dark'\n}\n\nconst props = defineProps<IProps>()\nconst $route = useRoute()\nconst $form = ref<IFormInstance>()\nconst configList = ref<IPicGoPluginConfig[]>([])\nconst formModel = reactive<IStringKeyMap>({})\nconst isUploader = computed(() => props.type === 'uploader')\nconst store = useStore()\n\nasync function validate (): Promise<IStringKeyMap | false> {\n  const res = await $form.value?.validate() || false\n  return res\n}\n\nfunction getConfigType () {\n  switch (props.type) {\n    case 'plugin': {\n      return props.id\n    }\n    case 'uploader': {\n      return `picBed.${props.id}`\n    }\n    case 'transformer': {\n      return `transformer.${props.id}`\n    }\n    default:\n      return 'unknown'\n  }\n}\n\nconst handleConfigForm = useConfigForm()\n\nfunction resetFormModel () {\n  Object.keys(formModel).forEach((key) => {\n    delete formModel[key]\n  })\n}\n\nasync function handleConfig (inputConfig: IPicGoPluginConfig[]) {\n  const currentConfig = await getCurConfigFormData()\n  const resetConfig = isUploader.value && !$route.params.configId\n  resetFormModel()\n  Object.assign(formModel, currentConfig)\n  configList.value = handleConfigForm(inputConfig, formModel, currentConfig, resetConfig)\n}\n\nasync function getCurConfigFormData () {\n  const configId = $route.params.configId\n  const configType = getConfigType()\n  if (isUploader.value) {\n    const cachedList = store?.state.appConfig?.uploader?.[props.id]?.configList\n    const curTypeConfigList = Array.isArray(cachedList)\n      ? cachedList\n      : await getConfig<IStringKeyMap[]>(`uploader.${props.id}.configList`) || []\n    return curTypeConfigList.find(i => i._id === configId) || {}\n  } else {\n    const config = await getConfig<IStringKeyMap>(configType)\n    return config || {}\n  }\n}\n\nasync function resetConfig () {\n  const config = await getCurConfigFormData()\n  Object.assign(formModel, config)\n}\n\nwatch(() => props.config, async (val: IPicGoPluginConfig[]) => {\n  await handleConfig(val)\n}, {\n  deep: true,\n  immediate: true\n})\n\ndefineExpose({\n  validate,\n  getConfigType,\n  resetConfig\n})\n</script>\n<style lang='stylus'>\n.config-form-common-tips\n  a\n    color #409EFF\n    text-decoration none\n#config-form\n  .el-form\n    label\n      line-height 22px\n      padding-bottom 0\n    &-item\n      display: flex\n      justify-content space-between\n      border-bottom 1px solid darken(#eee, 50%)\n      padding-bottom 16px\n      &:last-child\n        border-bottom none\n      &__content\n        justify-content flex-end\n    .el-button-group\n      width 100%\n      .el-button\n        width 50%\n    .el-radio-group\n      margin-left 25px\n    .el-switch__label\n      &.is-active\n        color #409EFF\n  &.white\n    .el-form-item\n      border-bottom 1px solid #ddd\n</style>\n"
  },
  {
    "path": "src/renderer/components/ToolboxHandler.vue",
    "content": "<template>\n  <div class=\"toolbox-handler\">\n    <ElButton\n      type=\"text\"\n      @click=\"() => props.handler(value)\"\n    >\n      {{ props.handlerText }}\n    </ElButton>\n  </div>\n</template>\n<script lang=\"ts\" setup>\nimport { IToolboxItemCheckStatus } from '~/universal/types/enum'\ninterface IProps {\n  status: IToolboxItemCheckStatus\n  value: any\n  handlerText: string\n  handler: (value: any) => void | Promise<void>\n}\n\nconst props = defineProps<IProps>()\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'ToolboxHandler'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/components/ToolboxStatusIcon.vue",
    "content": "<template>\n  <el-icon\n    :color=\"color\"\n    class=\"toolbox-status-icon\"\n  >\n    <template v-if=\"props.status === IToolboxItemCheckStatus.SUCCESS\">\n      <SuccessFilled />\n    </template>\n    <template v-if=\"props.status === IToolboxItemCheckStatus.ERROR\">\n      <CircleCloseFilled />\n    </template>\n    <template v-if=\"props.status === IToolboxItemCheckStatus.LOADING\">\n      <Loading />\n    </template>\n  </el-icon>\n</template>\n<script lang=\"ts\" setup>\nimport { CircleCloseFilled, Loading, SuccessFilled } from '@element-plus/icons-vue'\nimport { computed } from 'vue'\nimport { IToolboxItemCheckStatus } from '~/universal/types/enum'\ninterface IProps {\n  status: IToolboxItemCheckStatus\n}\n\nconst props = defineProps<IProps>()\n\nconst color = computed(() => {\n  switch (props.status) {\n    case IToolboxItemCheckStatus.SUCCESS:\n      return '#67C23A'\n    case IToolboxItemCheckStatus.ERROR:\n      return '#F56C6C'\n    default:\n      return '#909399'\n  }\n})\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'ToolboxStatusIcon'\n}\n</script>\n<style lang='stylus'>\n.toolbox-status-icon {\n  margin-left: 8px;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/dialog/ConfigFormDialog.vue",
    "content": "<template>\n  <div class=\"\">\n    <ConfirmDialog\n      v-model=\"visible\"\n      :title=\"title\"\n      :width=\"width\"\n      @confirm=\"handleConfirm\"\n      @cancel=\"handleCancel\"\n      @close=\"handleCancel\"\n    >\n      <template #body>\n        <BaseConfigForm\n          ref=\"$form\"\n          :config=\"configList\"\n          :form-model=\"formModel\"\n          theme=\"light\"\n        />\n      </template>\n    </ConfirmDialog>\n  </div>\n</template>\n<script lang=\"ts\" setup>\nimport { reactive, ref } from 'vue'\nimport ConfirmDialog from '@/components/dialog/ConfirmDialog.vue'\nimport { useIPCOn } from '@/hooks/useIPC'\nimport { IRPCActionType } from '~/universal/types/enum'\nimport BaseConfigForm from '@/components/form/BaseConfigForm.vue'\nimport { useConfigForm } from '@/hooks/useConfigForm'\nimport { sendToMain } from '@/utils/dataSender'\n\nconst configList = ref<IPicGoPluginConfig[]>([])\nconst formModel = reactive<IStringKeyMap>({})\nconst $form = ref<IFormInstance>()\nconst title = ref('')\nconst width = ref(500)\n\nconst handleConfigForm = useConfigForm()\n\nconst visible = ref(false)\nuseIPCOn(IRPCActionType.OPEN_CONFIG_DIALOG, (event, options: IPicGoPluginShowConfigDialogOption) => {\n  visible.value = true\n  configList.value = handleConfigForm(options.config, formModel)\n  title.value = options.title\n  width.value = options.width || 500\n})\nconst handleConfirm = async () => {\n  const res = await $form.value?.validate() || false\n  if (!res) {\n    return\n  }\n  sendToMain(IRPCActionType.OPEN_CONFIG_DIALOG, res)\n  visible.value = false\n}\nconst handleCancel = () => {\n  visible.value = false\n  sendToMain(IRPCActionType.OPEN_CONFIG_DIALOG, false)\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'ConfigFormDialog'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/components/dialog/ConfirmDialog.vue",
    "content": "<template>\n  <ElDialog\n    v-model=\"visible\"\n    :title=\"title\"\n    :width=\"width + 'px'\"\n    :append-to-body=\"true\"\n    @close=\"handleClose\"\n  >\n    <slot name=\"body\" />\n    <template #footer>\n      <slot\n        v-if=\"footerSlots\"\n        name=\"footer\"\n      />\n      <template v-else>\n        <ElButton\n          type=\"default\"\n          round\n          @click=\"handleCancel\"\n        >\n          {{ cancelButtonText }}\n        </ElButton>\n        <ElButton\n          type=\"primary\"\n          round\n          @click=\"handleConfirm\"\n        >\n          {{ confirmButtonText }}\n        </ElButton>\n      </template>\n    </template>\n  </ElDialog>\n</template>\n<script lang=\"ts\" setup>\nimport { useVModel } from '@/hooks/useVModel'\nimport { T as $T } from '@/i18n/index'\nimport { useSlots, ref } from 'vue'\n\ninterface IProps {\n  modelValue: boolean\n  title: string\n  confirmButtonText?: string\n  cancelButtonText?: string\n  width?: number\n}\nconst props = withDefaults(defineProps<IProps>(), {\n  confirmButtonText: $T('CONFIRM'),\n  cancelButtonText: $T('CANCEL'),\n  width: 500\n})\nconst $emit = defineEmits(['confirm', 'cancel', 'close'])\n\nconst isCancel = ref(false)\nconst isConfirm = ref(false)\n\nconst visible = useVModel(props, 'modelValue')\n\nconst handleClose = () => {\n  if (isConfirm.value) {\n    return\n  }\n  if (isCancel.value) {\n    $emit('cancel')\n    isCancel.value = false\n  } else {\n    $emit('close')\n  }\n}\n\nconst handleCancel = () => {\n  isCancel.value = true\n  visible.value = false\n}\n\nconst handleConfirm = () => {\n  isConfirm.value = true\n  $emit('confirm')\n}\n\nconst footerSlots = !!useSlots().footer\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'ConfirmDialog'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/components/dialog/InputBoxDialog.vue",
    "content": "<template>\n  <el-dialog\n    v-model=\"showInputBoxVisible\"\n    :title=\"inputBoxOptions.title || $T('INPUT')\"\n    :append-to-body=\"true\"\n    :width=\"inputBoxOptions.width + 'px'\"\n  >\n    <el-input\n      v-model=\"inputBoxValue\"\n      :placeholder=\"inputBoxOptions.placeholder\"\n      :type=\"inputBoxOptions.inputType || 'text'\"\n      :show-password=\"inputBoxOptions.inputType === 'password'\"\n      :rows=\"inputBoxOptions.inputType === 'textarea' ? 6 : undefined\"\n      :class=\"{ 'input-box__textarea': inputBoxOptions.inputType === 'textarea' }\"\n    />\n    <el-input\n      v-if=\"inputBoxOptions.hasConfirm\"\n      v-model=\"inputBoxConfirmValue\"\n      class=\"mt-[12px]\"\n      :placeholder=\"inputBoxOptions.confirmPlaceholder\"\n      :type=\"inputBoxOptions.inputType || 'text'\"\n      :show-password=\"inputBoxOptions.inputType === 'password'\"\n    />\n    <div\n      v-if=\"confirmError\"\n      class=\"mt-[8px] text-[12px] text-[#f56c6c] leading-[18px]\"\n    >\n      {{ confirmError }}\n    </div>\n    <template #footer>\n      <el-button\n        round\n        @click=\"handleInputBoxCancel\"\n      >\n        {{ $T('CANCEL') }}\n      </el-button>\n      <el-button\n        type=\"primary\"\n        round\n        @click=\"handleInputBoxConfirm\"\n      >\n        {{ $T('CONFIRM') }}\n      </el-button>\n    </template>\n  </el-dialog>\n</template>\n<script lang=\"ts\" setup>\nimport { ref, reactive, onBeforeUnmount, onBeforeMount, watch } from 'vue'\nimport { ipcRenderer, IpcRendererEvent } from 'electron'\nimport {\n  SHOW_INPUT_BOX,\n  SHOW_INPUT_BOX_RESPONSE\n} from '~/universal/events/constants'\nimport $bus from '@/utils/bus'\nimport { sendToMain } from '@/utils/dataSender'\nimport { T as $T } from '@/i18n/index'\nconst inputBoxValue = ref('')\nconst inputBoxConfirmValue = ref('')\nconst confirmError = ref('')\nconst showInputBoxVisible = ref(false)\nconst inputBoxOptions = reactive({\n  title: '',\n  placeholder: '',\n  inputType: 'text' as 'text' | 'textarea' | 'password',\n  width: 500,\n  hasConfirm: false,\n  confirmPlaceholder: ''\n})\n\nonBeforeMount(() => {\n  ipcRenderer.on(SHOW_INPUT_BOX, ipcEventHandler)\n  $bus.on(SHOW_INPUT_BOX, initInputBoxValue)\n})\n\nfunction ipcEventHandler (evt: IpcRendererEvent, options: IShowInputBoxOption) {\n  initInputBoxValue(options)\n}\n\nfunction initInputBoxValue (options: IShowInputBoxOption) {\n  inputBoxValue.value = options.value || ''\n  inputBoxConfirmValue.value = options.confirm?.value || ''\n  inputBoxOptions.title = options.title || ''\n  inputBoxOptions.placeholder = options.placeholder || ''\n  inputBoxOptions.inputType = options.inputType || 'text'\n  inputBoxOptions.width = options.width || 400\n  inputBoxOptions.hasConfirm = !!options.confirm\n  inputBoxOptions.confirmPlaceholder = options.confirm?.placeholder || ''\n  confirmError.value = ''\n  showInputBoxVisible.value = true\n}\n\nfunction handleInputBoxCancel () {\n  // TODO: RPCServer\n  showInputBoxVisible.value = false\n  sendToMain(SHOW_INPUT_BOX, '')\n  $bus.emit(SHOW_INPUT_BOX_RESPONSE, '')\n}\n\nfunction handleInputBoxConfirm () {\n  if (inputBoxOptions.hasConfirm && inputBoxValue.value !== inputBoxConfirmValue.value) {\n    confirmError.value = $T('INPUT_BOX_CONFIRM_MISMATCH')\n    return\n  }\n  showInputBoxVisible.value = false\n  sendToMain(SHOW_INPUT_BOX, inputBoxValue.value)\n  $bus.emit(SHOW_INPUT_BOX_RESPONSE, inputBoxValue.value)\n}\n\nwatch([inputBoxValue, inputBoxConfirmValue], () => {\n  confirmError.value = ''\n})\n\nonBeforeUnmount(() => {\n  ipcRenderer.removeListener(SHOW_INPUT_BOX, ipcEventHandler)\n  $bus.off(SHOW_INPUT_BOX)\n})\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'InputBoxDialog'\n}\n</script>\n<style lang='stylus'>\n.input-box__textarea\n  .el-textarea__inner\n    resize vertical\n    max-height 240px\n</style>\n"
  },
  {
    "path": "src/renderer/components/form/BaseConfigForm.vue",
    "content": "<template>\n  <div\n    id=\"base-config-form\"\n    :class=\"theme ?? 'dark'\"\n  >\n    <el-form\n      ref=\"$form\"\n      label-position=\"left\"\n      label-width=\"50%\"\n      :model=\"form\"\n      size=\"small\"\n    >\n      <slot name=\"extra-form\" />\n      <!-- dynamic config -->\n      <el-form-item\n        v-for=\"(item, index) in config\"\n        :key=\"item.name + index\"\n        :required=\"item.required\"\n        :prop=\"item.name\"\n        :class=\"index !== config.length - 1 ? 'has-border' : ''\"\n      >\n        <template #label>\n          <el-row align=\"middle\">\n            {{ item.alias || item.name }}\n            <template v-if=\"item.tips\">\n              <el-tooltip\n                class=\"item\"\n                effect=\"dark\"\n                placement=\"right\"\n              >\n                <template #content>\n                  <span\n                    class=\"config-form-common-tips\"\n                    v-html=\"transformMarkdownToHTML(item.tips)\"\n                  />\n                </template>\n                <el-icon class=\"ml-[4px] cursor-pointer hover:text-blue\">\n                  <QuestionFilled />\n                </el-icon>\n              </el-tooltip>\n            </template>\n          </el-row>\n        </template>\n        <el-input\n          v-if=\"item.type === 'input' || item.type === 'password'\"\n          v-model=\"form[item.name]\"\n          :type=\"item.type === 'password' ? 'password' : 'input'\"\n          :placeholder=\"item.message || item.name\"\n        />\n        <el-select\n          v-else-if=\"item.type === 'list' && item.choices\"\n          v-model=\"form[item.name]\"\n          :placeholder=\"item.message || item.name\"\n        >\n          <el-option\n            v-for=\"choice in item.choices\"\n            :key=\"choice.name || choice.value || choice\"\n            :label=\"choice.name || choice.value || choice\"\n            :value=\"choice.value || choice\"\n          />\n        </el-select>\n        <el-select\n          v-else-if=\"item.type === 'checkbox' && item.choices\"\n          v-model=\"form[item.name]\"\n          :placeholder=\"item.message || item.name\"\n          multiple\n          collapse-tags\n        >\n          <el-option\n            v-for=\"choice in item.choices\"\n            :key=\"choice.value || choice\"\n            :label=\"choice.name || choice.value || choice\"\n            :value=\"choice.value || choice\"\n          />\n        </el-select>\n        <el-switch\n          v-else-if=\"item.type === 'confirm'\"\n          v-model=\"form[item.name]\"\n          :active-text=\"item.confirmText || 'yes'\"\n          :inactive-text=\"item.cancelText || 'no'\"\n        />\n      </el-form-item>\n      <slot />\n    </el-form>\n  </div>\n</template>\n<script lang=\"ts\" setup>\nimport { ref } from 'vue'\nimport { marked } from 'marked'\nimport type { FormInstance } from 'element-plus'\nimport { useVModel } from '@/hooks/useVModel'\nimport { QuestionFilled } from '@element-plus/icons-vue'\n\nconst $form = ref<FormInstance>()\n\ninterface IProps {\n  config: IPicGoPluginConfig[]\n  formModel: IStringKeyMap\n  theme?: 'light' | 'dark'\n}\n\nconst props = defineProps<IProps>()\n\nconst form = useVModel(props, 'formModel')\n\nfunction transformMarkdownToHTML (markdown: string) {\n  try {\n    return marked.parse(markdown)\n  } catch (e) {\n    return markdown\n  }\n}\n\nasync function validate (): Promise<IStringKeyMap | false> {\n  return new Promise((resolve) => {\n    $form.value?.validate((valid: boolean) => {\n      if (valid) {\n        resolve(form.value)\n      } else {\n        resolve(false)\n      }\n    })\n  })\n}\n\ndefineExpose({\n  validate\n})\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'BaseConfigForm'\n}\n</script>\n<style lang='stylus'>\n#base-config-form\n  .el-form\n    label\n      line-height 22px\n      padding-bottom 0\n    &-item\n      display: flex\n      justify-content space-between\n      border-bottom 1px solid darken(#eee, 50%)\n      padding-bottom 16px\n      &:last-child\n        border-bottom none\n      &__content\n        justify-content flex-end\n    .el-button-group\n      width 100%\n      .el-button\n        width 50%\n    .el-radio-group\n      margin-left 25px\n    .el-switch__label\n      &.is-active\n        color #409EFF\n  &.light\n    .el-form-item.has-border\n      border-bottom 1px solid #ddd\n</style>\n"
  },
  {
    "path": "src/renderer/components/picgoCloud/ConfigSyncConflictDialog.vue",
    "content": "<template>\n  <el-dialog\n    v-model=\"visible\"\n    :close-on-click-modal=\"false\"\n    :close-on-press-escape=\"false\"\n    :show-close=\"false\"\n    class=\"picgo-cloud-config-sync-conflict-dialog\"\n    width=\"80%\"\n    top=\"6vh\"\n    append-to-body\n    lock-scroll\n    header-class=\"!px-[20px] !pt-[18px] !pb-[14px] !m-0 border-b border-slate-100\"\n    body-class=\"!p-0 overflow-hidden\"\n    footer-class=\"!px-[20px] !py-[16px] border-t border-slate-100\"\n  >\n    <template #header>\n      <div class=\"flex items-start justify-between gap-[16px]\">\n        <div class=\"min-w-0\">\n          <div class=\"flex items-center gap-[8px]\">\n            <el-icon>\n              <WarningFilled class=\"text-[20px] text-orange-500\" />\n            </el-icon>\n            <div class=\"text-[20px] font-semibold text-slate-800 leading-[22px]\">\n              {{ $T('PICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE', { count: conflicts.length }) }}\n            </div>\n          </div>\n        </div>\n        <div class=\"flex items-center gap-[10px] flex-shrink-0\">\n          <el-button\n            size=\"small\"\n            round\n            @click=\"handleResetAll\"\n          >\n            <el-icon class=\"mr-1\">\n              <RefreshLeft />\n            </el-icon>\n            {{ $T('PICGO_CLOUD_CONFIG_SYNC_RESET_ALL') }}\n          </el-button>\n          <el-button-group>\n            <el-button\n              size=\"small\"\n              round\n              @click=\"handleChooseAllLocal\"\n            >\n              {{ $T('PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL') }}\n            </el-button>\n            <el-button\n              size=\"small\"\n              round\n              @click=\"handleChooseAllCloud\"\n            >\n              {{ $T('PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD') }}\n            </el-button>\n          </el-button-group>\n        </div>\n      </div>\n      <div class=\"mt-[4px] text-[12px] text-slate-500 leading-[18px]\">\n        {{ $T('PICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED') }}\n      </div>\n    </template>\n\n    <!-- Conflict List -->\n    <div>\n      <div class=\"h-full overflow-y-auto px-[20px] pt-[16px] pb-[24px] picgo-cloud-conflict-list\">\n        <div\n          v-for=\"item in conflicts\"\n          :key=\"item.path\"\n          class=\"mb-[16px] last:mb-0 rounded-[12px] border-2 bg-white shadow-[0_1px_6px_rgba(15,23,42,0.10)] transition-colors duration-200 overflow-hidden\"\n          :class=\"getCardBorderClass(item.path)\"\n        >\n          <!-- Card Header -->\n          <div class=\"bg-slate-100/80 px-[16px] py-[10px] border-b border-slate-200 flex justify-between items-center gap-[12px]\">\n            <div class=\"min-w-0 flex items-center gap-2\">\n              <el-icon class=\"text-slate-500\">\n                <Operation />\n              </el-icon>\n              <span class=\"font-mono font-medium text-[12px] text-slate-700 leading-[16px] break-all\">\n                {{ item.path }}\n              </span>\n            </div>\n            <div\n              v-if=\"selections[item.path]\"\n              class=\"px-[10px] py-[4px] rounded-full text-[12px] font-medium flex items-center gap-1 shrink-0 border\"\n              :class=\"selections[item.path] === IPicGoCloudConfigSyncConflictChoice.LOCAL\n                ? 'bg-blue-100 text-blue-700 border-blue-200'\n                : 'bg-purple-100 text-purple-700 border-purple-200'\"\n            >\n              <el-icon class=\"text-[14px]\">\n                <Check />\n              </el-icon>\n              {{ selections[item.path] === IPicGoCloudConfigSyncConflictChoice.LOCAL\n                ? $T('PICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION')\n                : $T('PICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION') }}\n            </div>\n          </div>\n\n          <!-- Card Content -->\n          <div>\n            <div class=\"grid grid-cols-[1fr_auto_1fr] gap-[12px] items-stretch overflow-x-auto p-[16px]\">\n              <!-- Local Option -->\n              <div\n                class=\"relative cursor-pointer rounded-[8px] p-[16px] border-2 transition-all duration-200 group flex flex-col\"\n                :class=\"localBoxClass(item.path)\"\n                @click=\"setChoice(item.path, IPicGoCloudConfigSyncConflictChoice.LOCAL)\"\n              >\n                <div\n                  class=\"flex items-center gap-[8px] mb-[8px] font-semibold text-[14px]\"\n                  :class=\"selections[item.path] === IPicGoCloudConfigSyncConflictChoice.LOCAL ? 'text-blue-600' : 'text-gray-600'\"\n                >\n                  <el-icon class=\"text-[16px]\">\n                    <Monitor />\n                  </el-icon>\n                  <span>{{ $T('PICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION') }}</span>\n                </div>\n                <div class=\"font-mono text-[13px] text-slate-800 break-words leading-relaxed whitespace-pre-wrap\">\n                  {{ formatValue(item.localValue) }}\n                </div>\n                <div\n                  v-if=\"selections[item.path] === IPicGoCloudConfigSyncConflictChoice.LOCAL\"\n                  class=\"absolute top-[12px] right-[12px] text-blue-500\"\n                >\n                  <el-icon class=\"text-[20px]\">\n                    <Check />\n                  </el-icon>\n                </div>\n              </div>\n\n              <!-- Middle Indicator -->\n              <div class=\"flex flex-col items-center justify-center w-[40px]\">\n                <el-icon\n                  v-if=\"selections[item.path] === IPicGoCloudConfigSyncConflictChoice.LOCAL\"\n                  class=\"text-[24px] text-blue-500\"\n                >\n                  <ArrowLeft />\n                </el-icon>\n                <el-icon\n                  v-else-if=\"selections[item.path] === IPicGoCloudConfigSyncConflictChoice.CLOUD\"\n                  class=\"text-[24px] text-purple-500\"\n                >\n                  <ArrowRight />\n                </el-icon>\n                <div\n                  v-else\n                  class=\"w-[2px] h-full bg-gray-100 rounded-full my-2\"\n                />\n              </div>\n\n              <!-- Cloud Option -->\n              <div\n                class=\"relative cursor-pointer rounded-[8px] p-[16px] border-2 transition-all duration-200 group flex flex-col\"\n                :class=\"cloudBoxClass(item.path)\"\n                @click=\"setChoice(item.path, IPicGoCloudConfigSyncConflictChoice.CLOUD)\"\n              >\n                <div\n                  class=\"flex items-center gap-[8px] mb-[8px] font-semibold text-[14px]\"\n                  :class=\"selections[item.path] === IPicGoCloudConfigSyncConflictChoice.CLOUD ? 'text-purple-600' : 'text-gray-600'\"\n                >\n                  <el-icon class=\"text-[16px]\">\n                    <Cloudy />\n                  </el-icon>\n                  <span>{{ $T('PICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION') }}</span>\n                </div>\n                <div class=\"font-mono text-[13px] text-slate-800 break-words leading-relaxed whitespace-pre-wrap\">\n                  {{ formatValue(item.remoteValue) }}\n                </div>\n                <div\n                  v-if=\"selections[item.path] === IPicGoCloudConfigSyncConflictChoice.CLOUD\"\n                  class=\"absolute top-[12px] right-[12px] text-purple-500\"\n                >\n                  <el-icon class=\"text-[20px]\">\n                    <Check />\n                  </el-icon>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <template #footer>\n      <div class=\"flex items-center justify-between\">\n        <div class=\"text-[13px] text-gray-500 mr-[20px] text-left\">\n          <span\n            v-if=\"remainingCount > 0\"\n          >\n            {{ $T('PICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING') }}\n            <span class=\"font-bold text-orange-500 ml-1\">{{ remainingCount }}</span>\n          </span>\n          <span\n            v-else\n            class=\"text-green-600 flex items-center gap-1\"\n          >\n            <el-icon class=\"text-[14px]\">\n              <SuccessFilled />\n            </el-icon>\n            {{ $T('PICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED') }}\n          </span>\n        </div>\n        <div class=\"flex items-center gap-[12px]\">\n          <el-button\n            round\n            plain\n            :disabled=\"confirmLoading\"\n            @click=\"handleAbort\"\n          >\n            {{ $T('PICGO_CLOUD_CONFIG_SYNC_ABORT') }}\n          </el-button>\n          <el-button\n            type=\"primary\"\n            round\n            :loading=\"confirmLoading\"\n            :disabled=\"isConfirmDisabled || confirmLoading\"\n            @click=\"handleConfirm\"\n          >\n            {{ $T('PICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC') }}\n          </el-button>\n        </div>\n      </div>\n    </template>\n  </el-dialog>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, onBeforeUnmount, reactive, watch } from 'vue'\nimport { T as $T } from '@/i18n/index'\nimport {\n  IPicGoCloudConfigSyncConflictChoice,\n  type IPicGoCloudConfigSyncConflictItem,\n  type IPicGoCloudConfigSyncResolution\n} from '#/types/cloudConfigSync'\nimport {\n  ArrowLeft,\n  ArrowRight,\n  Check,\n  Cloudy,\n  Monitor,\n  Operation,\n  RefreshLeft,\n  SuccessFilled,\n  WarningFilled\n} from '@element-plus/icons-vue'\n\nconst props = defineProps<{\n  modelValue: boolean\n  conflicts: IPicGoCloudConfigSyncConflictItem[]\n  confirmLoading: boolean\n}>()\n\nconst emit = defineEmits<{\n  (e: 'update:modelValue', value: boolean): void\n  (e: 'abort'): void\n  (e: 'confirm', resolution: IPicGoCloudConfigSyncResolution): void\n}>()\n\nconst visible = computed({\n  get: () => props.modelValue,\n  set: (value: boolean) => emit('update:modelValue', value)\n})\n\ntype IScrollLockRecord = {\n  el: HTMLElement\n  previousOverflow: string\n}\n\nconst scrollLockRecords: IScrollLockRecord[] = []\n\nconst lockPageScroll = () => {\n  if (typeof document === 'undefined') return\n  if (scrollLockRecords.length > 0) return\n\n  const targets: Array<HTMLElement | null> = [\n    document.documentElement,\n    document.body,\n    document.getElementById('main-page'),\n    document.querySelector<HTMLElement>('.main-wrapper')\n  ]\n\n  targets.forEach((el) => {\n    if (!el) return\n    scrollLockRecords.push({\n      el,\n      previousOverflow: el.style.overflow\n    })\n    el.style.overflow = 'hidden'\n  })\n}\n\nconst unlockPageScroll = () => {\n  if (scrollLockRecords.length === 0) return\n  scrollLockRecords.forEach(({ el, previousOverflow }) => {\n    el.style.overflow = previousOverflow\n  })\n  scrollLockRecords.length = 0\n}\n\nwatch(visible, (nextVisible) => {\n  if (nextVisible) {\n    lockPageScroll()\n    return\n  }\n  unlockPageScroll()\n}, { immediate: true })\n\nonBeforeUnmount(() => {\n  unlockPageScroll()\n})\n\nconst selections = reactive<Record<string, IPicGoCloudConfigSyncConflictChoice | undefined>>({})\n\nwatch(() => props.conflicts, (next) => {\n  // New conflict set: reset selections.\n  Object.keys(selections).forEach((key) => {\n    delete selections[key]\n  })\n  next.forEach((item) => {\n    selections[item.path] = undefined\n  })\n}, { immediate: true })\n\nconst isConfirmDisabled = computed(() => {\n  if (!props.conflicts.length) return true\n  return props.conflicts.some(item => !selections[item.path])\n})\n\nconst remainingCount = computed(() => {\n  return props.conflicts.filter(item => !selections[item.path]).length\n})\n\nconst setChoice = (path: string, choice: IPicGoCloudConfigSyncConflictChoice) => {\n  selections[path] = choice\n}\n\nconst getCardBorderClass = (path: string) => {\n  const choice = selections[path]\n  if (!choice) return 'border-slate-300'\n  return choice === IPicGoCloudConfigSyncConflictChoice.LOCAL ? 'border-blue-300' : 'border-purple-300'\n}\n\nconst localBoxClass = (path: string) => {\n  const selected = selections[path] === IPicGoCloudConfigSyncConflictChoice.LOCAL\n  return selected\n    ? 'bg-blue-50 border-blue-500 ring-1 ring-blue-500'\n    : 'bg-white border-gray-200 hover:border-blue-300 hover:bg-gray-100'\n}\n\nconst cloudBoxClass = (path: string) => {\n  const selected = selections[path] === IPicGoCloudConfigSyncConflictChoice.CLOUD\n  return selected\n    ? 'bg-purple-50 border-purple-500 ring-1 ring-purple-500'\n    : 'bg-white border-gray-200 hover:border-purple-300 hover:bg-gray-100'\n}\n\nconst handleChooseAllLocal = () => {\n  props.conflicts.forEach(item => {\n    selections[item.path] = IPicGoCloudConfigSyncConflictChoice.LOCAL\n  })\n}\n\nconst handleChooseAllCloud = () => {\n  props.conflicts.forEach(item => {\n    selections[item.path] = IPicGoCloudConfigSyncConflictChoice.CLOUD\n  })\n}\n\nconst handleResetAll = () => {\n  props.conflicts.forEach(item => {\n    selections[item.path] = undefined\n  })\n}\n\nconst handleAbort = () => {\n  emit('abort')\n  visible.value = false\n}\n\nconst handleConfirm = () => {\n  const resolution: IPicGoCloudConfigSyncResolution = {}\n  props.conflicts.forEach(item => {\n    const choice = selections[item.path]\n    if (choice) {\n      resolution[item.path] = choice\n    }\n  })\n  emit('confirm', resolution)\n}\n\nconst formatValue = (value: unknown): string => {\n  if (value === undefined) return $T('PICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED')\n  try {\n    return JSON.stringify(value, null, 2) ?? String(value)\n  } catch (e) {\n    return String(value)\n  }\n}\n</script>\n\n<style scoped>\n.picgo-cloud-conflict-list::-webkit-scrollbar {\n  width: 6px;\n}\n.picgo-cloud-conflict-list::-webkit-scrollbar-track {\n  background: transparent;\n}\n.picgo-cloud-conflict-list::-webkit-scrollbar-thumb {\n  background: rgba(0, 0, 0, 0.15);\n  border-radius: 3px;\n}\n.picgo-cloud-conflict-list::-webkit-scrollbar-thumb:hover {\n  background: rgba(0, 0, 0, 0.25);\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/settings/ButtonFormItem.vue",
    "content": "<template>\n  <el-form-item\n    :label=\"label\"\n  >\n    <el-button\n      type=\"primary\"\n      round\n      size=\"small\"\n      @click=\"handleClick\"\n    >\n      {{ buttonLabel }}\n    </el-button>\n  </el-form-item>\n</template>\n<script lang=\"ts\" setup>\n\ninterface IProps {\n  label: string\n  buttonLabel: string\n}\n\ndefineProps<IProps>()\n\nconst emit = defineEmits(['click'])\n\nconst handleClick = () => {\n  emit('click')\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'ButtonFormItem'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/components/settings/SelectFormItem.vue",
    "content": "<template>\n  <el-form\n    label-position=\"left\"\n    label-width=\"50%\"\n    size=\"small\"\n  >\n    <el-form-item\n      :label=\"label\"\n    >\n      <el-select\n        v-model=\"currentValue\"\n        size=\"small\"\n        style=\"width: 100%\"\n        :placeholder=\"placeholder\"\n        @change=\"handleChange\"\n      >\n        <el-option\n          v-for=\"item in list\"\n          :key=\"item.value\"\n          :label=\"item.label\"\n          :value=\"item.value\"\n        />\n      </el-select>\n    </el-form-item>\n  </el-form>\n</template>\n<script lang=\"ts\" setup generic=\"T extends string | number\">\nimport { useVModel } from '@/hooks/useVModel'\n\ninterface IProps {\n  modelValue: T\n  label: string\n  placeholder?: string\n  list: {\n    label: string\n    value: T\n  }[]\n}\n\nconst props = defineProps<IProps>()\nconst emit = defineEmits(['change'])\n\nconst currentValue = useVModel(props, 'modelValue')\n\nconst handleChange = (val: T) => {\n  emit('change', val)\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'SelectFormItem'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/components/settings/SwitchFormItem.vue",
    "content": "<template>\n  <el-form-item>\n    <template #label>\n      <el-row align=\"middle\">\n        {{ props.label }}\n        <template v-if=\"props.tooltips\">\n          <el-tooltip\n            class=\"item\"\n            effect=\"dark\"\n            placement=\"right\"\n          >\n            <template #content>\n              <div class=\"picgo-tooltip-content\">\n                {{ props.tooltips }}\n              </div>\n            </template>\n            <el-icon class=\"ml-[4px] cursor-pointer hover:text-blue\">\n              <QuestionFilled />\n            </el-icon>\n          </el-tooltip>\n        </template>\n      </el-row>\n    </template>\n    <el-switch\n      v-model=\"value\"\n      :active-text=\"$T('SETTINGS_OPEN')\"\n      :inactive-text=\"$T('SETTINGS_CLOSE')\"\n      @change=\"handleChange\"\n    />\n  </el-form-item>\n</template>\n<script lang=\"ts\" setup>\nimport { T as $T } from '@/i18n'\nimport { saveConfig } from '@/utils/dataSender'\nimport { QuestionFilled } from '@element-plus/icons-vue'\nimport { showNotification } from '@/utils/notification'\nimport { useVModel } from '@/hooks/useVModel'\n\ninterface IProps {\n  tooltips?: string\n  settingProps: keyof ISettingForm\n  label: string\n  modelValue: boolean\n}\n\nconst props = defineProps<IProps>()\n\nconst emit = defineEmits(['update:modelValue', 'change'])\n\nconst value = useVModel(props, 'modelValue')\n\nconst handleChange = async (value: ISwitchValueType) => {\n  await saveConfig(`settings.${props.settingProps}`, value)\n  emit('update:modelValue', value)\n  emit('change', value)\n  showNotification({\n    title: props.label,\n    body: $T('TIPS_SET_SUCCEED')\n  })\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'SwitchFormItem'\n}\n</script>\n<style lang='stylus'>\n.picgo-tooltip-content\n  max-width: 360px\n  max-height: 200px\n  overflow: auto\n  white-space: normal\n  word-break: break-word\n</style>\n"
  },
  {
    "path": "src/renderer/hooks/useATagClick.ts",
    "content": "import { openURL } from '@/utils/common'\nimport { onMounted, onUnmounted } from 'vue'\n\nexport function useATagClick () {\n  const handleATagClick = (e: MouseEvent) => {\n    if (e.target instanceof HTMLAnchorElement) {\n      if (e.target.href) {\n        e.preventDefault()\n        openURL(e.target.href)\n      }\n    }\n  }\n  onMounted(() => {\n    document.addEventListener('click', handleATagClick)\n  })\n  onUnmounted(() => {\n    document.removeEventListener('click', handleATagClick)\n  })\n}\n"
  },
  {
    "path": "src/renderer/hooks/useConfigForm.ts",
    "content": "import { cloneDeep, union } from 'lodash'\n/**\n *\n * @param configList origin config list\n * @param formModel el-form form model for default value\n * @param currentConfig current config\n * @param resetConfigForm reset form model -> clear default value\n * @returns transformed config list\n */\nexport const useConfigForm = () => {\n  return (configList: IPicGoPluginConfig[], formModel: IStringKeyMap, currentConfig?: IStringKeyMap, resetConfigForm?: boolean) => {\n    if (configList.length > 0) {\n      return cloneDeep(configList).map((item) => {\n        // if (!configId) return item\n        if (resetConfigForm) return item\n        let defaultValue = item.default !== undefined\n          ? item.default\n          : item.type === 'checkbox'\n            ? []\n            : null\n        if (item.type === 'checkbox') {\n          const defaults = item.choices?.filter((i: any) => {\n            return i.checked\n          }).map((i: any) => i.value) || []\n          defaultValue = union(defaultValue, defaults)\n        }\n        if (currentConfig && currentConfig[item.name] !== undefined) {\n          defaultValue = currentConfig[item.name]\n        }\n        formModel[item.name] = defaultValue\n        return item\n      })\n    }\n    return []\n  }\n}\n"
  },
  {
    "path": "src/renderer/hooks/useIPC.ts",
    "content": "import { ipcRenderer } from 'electron'\nimport { onUnmounted } from 'vue'\nimport { IRPCActionType } from '~/universal/types/enum'\n\nexport const useIPCOn = (channel: string, listener: IpcRendererListener) => {\n  ipcRenderer.on(channel, listener)\n\n  onUnmounted(() => {\n    ipcRenderer.removeListener(channel, listener)\n  })\n}\n\nexport const useIPCOnce = (channel: string, listener: IpcRendererListener) => {\n  ipcRenderer.once(channel, listener)\n\n  onUnmounted(() => {\n    ipcRenderer.removeListener(channel, listener)\n  })\n}\n\n/**\n * will auto removeListener when component unmounted\n */\nexport const useIPC = () => {\n  return {\n    on: (channel: IRPCActionType, listener: IpcRendererListener) => useIPCOn(channel, listener),\n    once: (channel: IRPCActionType, listener: IpcRendererListener) => useIPCOnce(channel, listener)\n  }\n}\n"
  },
  {
    "path": "src/renderer/hooks/useOS.ts",
    "content": "import { onBeforeMount, ref } from 'vue'\n\nexport const useOS = () => {\n  const os = ref<string>('')\n\n  onBeforeMount(() => {\n    os.value = process.platform\n  })\n  return os\n}\n"
  },
  {
    "path": "src/renderer/hooks/useStore.ts",
    "content": "import { inject } from 'vue'\nimport { storeKey } from '@/store'\n\nexport const useStore = () => {\n  return inject(storeKey) ?? null\n}\n"
  },
  {
    "path": "src/renderer/hooks/useVModel.ts",
    "content": "import { computed, getCurrentInstance } from 'vue'\n\nexport type VModelObject = object\n\n/**\n * v-model for single prop\n */\nexport function useVModel<T extends VModelObject, K extends keyof T> (props: T, key: K) {\n  const vm = getCurrentInstance()\n  return computed({\n    get: () => props[key],\n    set: (value) => {\n      vm?.emit(`update:${key as string}`, value)\n    }\n  })\n}\n"
  },
  {
    "path": "src/renderer/hooks/useVModelValues.ts",
    "content": "import { getCurrentInstance, reactive, UnwrapNestedRefs, watch } from 'vue'\n\n/**\n * v-model for multiple props\n */\nexport function useVModelValues<T extends object> (props: T, keys: Array<keyof T>) {\n  const vm = getCurrentInstance()\n  const obj = {} as T\n  keys.forEach(key => {\n    obj[key] = props[key]\n  })\n  const mutableValue = reactive(obj) as T\n\n  watch(() => props, (val) => {\n    keys.forEach(key => {\n      mutableValue[key] = val[key]\n    })\n  }, {\n    deep: true\n  })\n\n  function updateProps () {\n    for (const key of keys) {\n      vm?.emit(`update:${key as string}`, mutableValue[key])\n    }\n  }\n\n  return [mutableValue as UnwrapNestedRefs<T>, updateProps] as const\n}\n"
  },
  {
    "path": "src/renderer/i18n/index.ts",
    "content": "import { ipcRenderer } from 'electron'\nimport { ObjectAdapter, I18n } from '@picgo/i18n'\nimport { GET_CURRENT_LANGUAGE, SET_CURRENT_LANGUAGE, FORCE_UPDATE, GET_LANGUAGE_LIST } from '#/events/constants'\nimport bus from '@/utils/bus'\nimport { builtinI18nList } from '#/i18n'\n\nexport class I18nManager {\n  private i18n: I18n | null = null\n  private i18nFileList: II18nItem[] = builtinI18nList\n\n  private getLanguageList () {\n    ipcRenderer.send(GET_LANGUAGE_LIST)\n    ipcRenderer.once(GET_LANGUAGE_LIST, (event, list: II18nItem[]) => {\n      this.i18nFileList = list\n    })\n  }\n\n  private getCurrentLanguage () {\n    ipcRenderer.send(GET_CURRENT_LANGUAGE)\n    ipcRenderer.once(GET_CURRENT_LANGUAGE, (event, lang: string, locales: ILocales) => {\n      this.setLocales(lang, locales)\n      bus.emit(FORCE_UPDATE)\n    })\n  }\n\n  private setLocales (lang: string, locales: ILocales) {\n    const objectAdapter = new ObjectAdapter({\n      [lang]: locales\n    })\n    this.i18n = new I18n({\n      adapter: objectAdapter,\n      defaultLanguage: lang\n    })\n  }\n\n  constructor () {\n    this.getCurrentLanguage()\n    this.getLanguageList()\n    ipcRenderer.on(SET_CURRENT_LANGUAGE, (event, lang: string, locales: ILocales) => {\n      this.setLocales(lang, locales)\n      bus.emit(FORCE_UPDATE)\n    })\n  }\n\n  T (key: ILocalesKey, args: IStringKeyMap = {}): string {\n    return this.i18n?.translate(key, args) || key\n  }\n\n  setCurrentLanguage (lang: string) {\n    ipcRenderer.send(SET_CURRENT_LANGUAGE, lang)\n  }\n\n  get languageList () {\n    return this.i18nFileList\n  }\n}\n\nconst i18nManager = new I18nManager()\n\nconst T = (key: ILocalesKey, args: IStringKeyMap = {}): string => {\n  return i18nManager.T(key, args)\n}\n\nexport {\n  i18nManager,\n  T\n}\n"
  },
  {
    "path": "src/renderer/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"referrer\" content=\"never\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <title>PicGo</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/renderer/layouts/Main.vue",
    "content": "<template>\n  <div id=\"main-page\">\n    <div\n      class=\"fake-title-bar\"\n    >\n      <div class=\"fake-title-bar__title\">\n        PicGo - {{ version }}\n      </div>\n      <div\n        v-if=\"os !== 'darwin'\"\n        class=\"handle-bar\"\n      >\n        <el-icon\n          class=\"minus\"\n          @click=\"minimizeWindow\"\n        >\n          <Minus />\n        </el-icon>\n        <el-icon\n          class=\"plus\"\n          @click=\"openMiniWindow\"\n        >\n          <CirclePlus />\n        </el-icon>\n        <el-icon\n          class=\"close\"\n          @click=\"closeWindow\"\n        >\n          <Close />\n        </el-icon>\n      </div>\n    </div>\n    <el-row\n      style=\"padding-top: 22px;\"\n      class=\"main-content\"\n    >\n      <el-col\n        class=\"side-bar-menu\"\n      >\n        <el-menu\n          class=\"picgo-sidebar\"\n          :default-active=\"defaultActive\"\n          :unique-opened=\"true\"\n          @select=\"handleSelect\"\n          @open=\"handleGetPicPeds\"\n        >\n          <el-menu-item :index=\"routerConfig.UPLOAD_PAGE\">\n            <el-icon>\n              <UploadFilled />\n            </el-icon>\n            <span>{{ $T('UPLOAD_AREA') }}</span>\n          </el-menu-item>\n          <el-menu-item :index=\"routerConfig.GALLERY_PAGE\">\n            <el-icon>\n              <PictureFilled />\n            </el-icon>\n            <span>{{ $T('GALLERY') }}</span>\n          </el-menu-item>\n          <el-sub-menu\n            index=\"sub-menu\"\n          >\n            <template #title>\n              <el-icon>\n                <Menu />\n              </el-icon>\n              <span>{{ $T('PICBEDS_SETTINGS') }}</span>\n            </template>\n            <template\n              v-for=\"item in picBed\"\n            >\n              <el-menu-item\n                v-if=\"item.visible\"\n                :key=\"item.type\"\n                :index=\"`${routerConfig.UPLOADER_CONFIG_PAGE}-${item.type}`\"\n              >\n                <span>{{ item.name }}</span>\n              </el-menu-item>\n            </template>\n          </el-sub-menu>\n          <el-menu-item :index=\"routerConfig.PICGO_CLOUD_PAGE\">\n            <el-icon>\n              <Cloudy />\n            </el-icon>\n            <span>PicGo Cloud</span>\n          </el-menu-item>\n          <el-menu-item :index=\"routerConfig.SETTING_PAGE\">\n            <el-icon>\n              <Setting />\n            </el-icon>\n            <span>{{ $T('PICGO_SETTINGS') }}</span>\n          </el-menu-item>\n          <el-menu-item :index=\"routerConfig.PLUGIN_PAGE\">\n            <el-icon>\n              <Share />\n            </el-icon>\n            <span>{{ $T('PLUGIN_SETTINGS') }}</span>\n          </el-menu-item>\n        </el-menu>\n        <el-icon\n          class=\"info-window\"\n          @click=\"openMenu\"\n        >\n          <InfoFilled />\n        </el-icon>\n      </el-col>\n      <el-col\n        :span=\"19\"\n        :offset=\"5\"\n        style=\"height: 428px\"\n        class=\"main-wrapper\"\n      >\n        <router-view\n          v-slot=\"{ Component }\"\n        >\n          <transition\n            name=\"picgo-fade\"\n            mode=\"out-in\"\n          >\n            <keep-alive :include=\"keepAlivePages\">\n              <component\n                :is=\"Component\"\n              />\n            </keep-alive>\n          </transition>\n        </router-view>\n      </el-col>\n    </el-row>\n    <el-dialog\n      v-model=\"visible\"\n      :title=\"$T('SPONSOR_PICGO')\"\n      width=\"70%\"\n      top=\"10vh\"\n    >\n      {{ $T('PICGO_SPONSOR_TEXT') }}\n      <el-row class=\"support\">\n        <el-col :span=\"12\">\n          <img\n            src=\"https://user-images.githubusercontent.com/12621342/34188165-e7cdf372-e56f-11e7-8732-1338c88b9bb7.jpg\"\n            :alt=\"$T('ALIPAY')\"\n          >\n          <div class=\"support-title\">\n            {{ $T('ALIPAY') }}\n          </div>\n        </el-col>\n        <el-col :span=\"12\">\n          <img\n            src=\"https://user-images.githubusercontent.com/12621342/34188201-212cda84-e570-11e7-9b7a-abb298699d85.jpg\"\n            :alt=\"$T('WECHATPAY')\"\n          >\n          <div class=\"support-title\">\n            {{ $T('WECHATPAY') }}\n          </div>\n        </el-col>\n      </el-row>\n    </el-dialog>\n    <el-dialog\n      v-model=\"qrcodeVisible\"\n      class=\"qrcode-dialog\"\n      top=\"3vh\"\n      width=\"60%\"\n      :title=\"$T('PICBED_QRCODE')\"\n      :modal-append-to-body=\"false\"\n      lock-scroll\n    >\n      <el-form\n        label-position=\"left\"\n        label-width=\"70px\"\n        size=\"small\"\n      >\n        <el-form-item\n          :label=\"$T('CHOOSE_PICBED')\"\n        >\n          <el-select\n            v-model=\"choosedPicBedForQRCode\"\n            multiple\n            collapse-tags\n          >\n            <el-option\n              v-for=\"item in picBed\"\n              :key=\"item.type\"\n              :label=\"item.name\"\n              :value=\"item.type\"\n            />\n          </el-select>\n          <el-button\n            v-show=\"choosedPicBedForQRCode.length > 0\"\n            type=\"primary\"\n            round\n            class=\"copy-picbed-config\"\n            @click=\"handleCopyPicBedConfig\"\n          >\n            {{ $T('COPY_PICBED_CONFIG') }}\n          </el-button>\n        </el-form-item>\n      </el-form>\n      <div class=\"qrcode-container\">\n        <qrcode-vue\n          v-show=\"choosedPicBedForQRCode.length > 0\"\n          :size=\"280\"\n          :value=\"picBedConfigString\"\n        />\n      </div>\n    </el-dialog>\n    <input-box-dialog />\n  </div>\n</template>\n<script lang=\"ts\" setup>\n// import { Component, Vue, Watch } from 'vue-property-decorator'\nimport {\n  Setting,\n  UploadFilled,\n  PictureFilled,\n  Menu,\n  Cloudy,\n  Share,\n  InfoFilled,\n  Minus,\n  CirclePlus,\n  Close\n} from '@element-plus/icons-vue'\nimport { ElMessage as $message } from 'element-plus'\nimport { T as $T } from '@/i18n/index'\nimport { ref, onBeforeUnmount, Ref, onBeforeMount, watch, nextTick, reactive, computed } from 'vue'\nimport { onBeforeRouteUpdate, useRouter } from 'vue-router'\nimport QrcodeVue from 'qrcode.vue'\nimport pick from 'lodash/pick'\nimport pkg from 'root/package.json'\nimport * as config from '@/router/config'\nimport {\n  ipcRenderer,\n  clipboard\n} from 'electron'\nimport InputBoxDialog from '@/components/dialog/InputBoxDialog.vue'\nimport {\n  MINIMIZE_WINDOW,\n  CLOSE_WINDOW,\n  SHOW_MAIN_PAGE_MENU,\n  SHOW_MAIN_PAGE_QRCODE,\n  SHOW_MAIN_PAGE_DONATION\n} from '~/universal/events/constants'\nimport { getConfig, sendToMain } from '@/utils/dataSender'\nimport { useOS } from '@/hooks/useOS'\nimport { useStore } from '@/hooks/useStore'\nconst version = ref(process.env.NODE_ENV === 'production' ? pkg.version : 'Dev')\nconst routerConfig = reactive(config)\nconst defaultActive = ref(routerConfig.UPLOAD_PAGE)\nconst visible = ref(false)\nconst os = useOS()\nconst $router = useRouter()\nconst store = useStore()\nconst picBed = computed(() => store?.state.picBeds ?? [])\nconst qrcodeVisible = ref(false)\nconst picBedConfigString = ref('')\nconst choosedPicBedForQRCode: Ref<string[]> = ref([])\n\nconst keepAlivePages = $router.getRoutes().filter(item => item.meta.keepAlive).map(item => item.name as string)\n\nonBeforeMount(() => {\n  store?.refreshPicBeds()\n  handleGetPicPeds()\n  ipcRenderer.on(SHOW_MAIN_PAGE_QRCODE, () => {\n    qrcodeVisible.value = true\n  })\n  ipcRenderer.on(SHOW_MAIN_PAGE_DONATION, () => {\n    visible.value = true\n  })\n})\n\nwatch(() => choosedPicBedForQRCode, (val) => {\n  if (val.value.length > 0) {\n    nextTick(async () => {\n      const picBedConfig = store?.state.appConfig?.picBed ?? await getConfig('picBed')\n      const config = pick(picBedConfig, ...choosedPicBedForQRCode.value)\n      picBedConfigString.value = JSON.stringify(config)\n    })\n  }\n}, { deep: true })\n\nconst handleGetPicPeds = () => {\n  store?.refreshPicBeds()\n}\n\nconst handleSelect = (index: string) => {\n  defaultActive.value = index\n  const type = index.match(routerConfig.UPLOADER_CONFIG_PAGE)\n  if (type === null) {\n    $router.push({\n      name: index\n    })\n  } else {\n    const type = index.replace(`${routerConfig.UPLOADER_CONFIG_PAGE}-`, '')\n    $router.push({\n      name: routerConfig.UPLOADER_CONFIG_PAGE,\n      params: {\n        type\n      }\n    })\n    // if (this.$builtInPicBed.includes(picBed)) {\n    //   this.$router.push({\n    //     name: picBed\n    //   })\n    // } else {\n    //   this.$router.push({\n    //     name: 'others',\n    //     params: {\n    //       type: picBed\n    //     }\n    //   })\n    // }\n  }\n}\n\nfunction minimizeWindow () {\n  sendToMain(MINIMIZE_WINDOW)\n}\n\nfunction closeWindow () {\n  sendToMain(CLOSE_WINDOW)\n}\n\nfunction openMenu () {\n  sendToMain(SHOW_MAIN_PAGE_MENU)\n}\n\nfunction openMiniWindow () {\n  sendToMain('openMiniWindow')\n}\n\nfunction handleCopyPicBedConfig () {\n  clipboard.writeText(picBedConfigString.value)\n  $message.success($T('COPY_PICBED_CONFIG_SUCCEED'))\n}\n\nonBeforeRouteUpdate(async (to) => {\n  if (to.params.type) {\n    defaultActive.value = `${routerConfig.UPLOADER_CONFIG_PAGE}-${to.params.type}`\n  } else {\n    defaultActive.value = to.name as string\n  }\n})\n\nonBeforeUnmount(() => {\n  ipcRenderer.removeAllListeners(SHOW_MAIN_PAGE_QRCODE)\n  ipcRenderer.removeAllListeners(SHOW_MAIN_PAGE_DONATION)\n})\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'MainPage'\n}\n</script>\n<style lang='stylus'>\n$bg = transparentify(#172426, #000, 0.9)\n$sideBg = transparentify(#000, 0.7)\n.setting-list-scroll\n  height 425px\n  overflow-y auto\n  overflow-x hidden\n  margin-right 0!important\n.picgo-fade\n  &-enter,\n  &-leave,\n  &-leave-active\n    opacity 0\n  &-enter-active,\n  &-leave-active\n    transition all 150ms linear\n.view-title\n  color #eee\n  font-size 20px\n  text-align center\n  margin 10px auto\n#main-page\n  .qrcode-dialog\n    .qrcode-container\n      display flex\n      justify-content center\n    .el-dialog__body\n      padding-top 10px\n    .copy-picbed-config\n      margin-left 10px\n  .fake-title-bar\n    -webkit-app-region drag\n    height h = 22px\n    width 100%\n    text-align center\n    color #eee\n    font-size 12px\n    line-height h\n    position fixed\n    z-index 100\n    background transparent\n    background-image linear-gradient(\n      to right,\n      $sideBg 0%,\n      $sideBg 170px,\n      $bg 170px,\n      $bg 100%\n    )\n    .fake-title-bar__title\n      padding-left 167px\n    .handle-bar\n      position absolute\n      top 2px\n      right 4px\n      z-index 10000\n      -webkit-app-region no-drag\n      .el-icon\n        cursor pointer\n        font-size 16px\n        margin-left 5px\n      .el-icon.minus\n        &:hover\n          color #409EFF\n      .el-icon.close\n        &:hover\n          color #F15140\n      .el-icon.plus\n        &:hover\n          color #69C282\n  .main-wrapper\n    background $bg\n  .side-bar-menu\n    position fixed\n    height calc(100vh - 22px)\n    overflow-x hidden\n    overflow-y auto\n    width 170px\n    background $sideBg\n    .info-window\n      cursor pointer\n      position fixed\n      bottom 4px\n      left 4px\n      cursor poiter\n      color #878d99\n      transition .2s all ease-in-out\n      &:hover\n        color #409EFF\n  .el-menu\n    border-right none\n    background transparent\n    width 170px\n    &-item\n      color #eee\n      position relative\n      &:focus,\n      &:hover\n        color #fff\n        background transparent\n      &.is-active\n        color active-color = #409EFF\n        &:before\n          content ''\n          position absolute\n          width 3px\n          height 20px\n          right 0\n          top 18px\n          background active-color\n  .el-sub-menu__title\n    color #eee\n    &:hover\n      background transparent\n      span\n        color #fff\n  .el-sub-menu\n    .el-menu-item\n      min-width 166px\n      &.is-active\n        &:before\n          top 16px\n  .main-content\n    padding-top 22px\n    position relative\n    z-index 10\n  .el-dialog__body\n    padding 20px\n  .support\n    text-align center\n    &-title\n      text-align center\n      color #878d99\n  .align-center\n    input\n      text-align center\n  *::-webkit-scrollbar\n    width 8px\n    height 8px\n  *::-webkit-scrollbar-thumb\n    border-radius 4px\n    background #6f6f6f\n  *::-webkit-scrollbar-track\n    background-color transparent\n</style>\n"
  },
  {
    "path": "src/renderer/main.ts",
    "content": "import './assets/css/tailwind.css'\nimport { createApp } from 'vue'\nimport { webFrame } from 'electron'\nimport App from './App.vue'\nimport router from './router'\nimport ElementUI from 'element-plus'\nimport 'element-plus/dist/index.css'\nimport VueLazyLoad from 'vue3-lazyload'\nimport axios from 'axios'\nimport { mainMixin } from './utils/mainMixin'\nimport { dragMixin } from '@/utils/mixin'\nimport { initTalkingData } from './utils/analytics'\nimport db from './utils/db'\nimport { i18nManager, T } from './i18n/index'\nimport { getConfig, saveConfig, sendToMain } from '@/utils/dataSender'\nimport { store } from '@/store'\nimport vue3PhotoPreview from 'vue3-photo-preview'\nimport 'vue3-photo-preview/dist/index.css'\nimport { getRendererStaticFileUrl } from './utils/static'\n\nwebFrame.setVisualZoomLevelLimits(1, 1)\n\nconst app = createApp(App)\n\napp.config.globalProperties.$builtInPicBed = [\n  'smms',\n  'imgur',\n  'qiniu',\n  'tcyun',\n  'upyun',\n  'aliyun',\n  'github'\n]\n\napp.config.globalProperties.$$db = db\napp.config.globalProperties.$http = axios\napp.config.globalProperties.$T = T\napp.config.globalProperties.$i18n = i18nManager\napp.config.globalProperties.getConfig = getConfig\napp.config.globalProperties.saveConfig = saveConfig\napp.config.globalProperties.sendToMain = sendToMain\n\napp.mixin(mainMixin)\napp.mixin(dragMixin)\n\napp.use(VueLazyLoad, {\n  error: getRendererStaticFileUrl('unknown-file-type.svg')\n})\napp.use(ElementUI)\napp.use(router)\napp.use(store)\napp.use(vue3PhotoPreview)\n\napp.mount('#app')\n\ninitTalkingData()\n"
  },
  {
    "path": "src/renderer/pages/Gallery.vue",
    "content": "<template>\n  <div id=\"gallery-view\">\n    <div class=\"view-title\">\n      {{ $T('GALLERY') }} - {{ filterList.length }}\n      <el-icon\n        style=\"margin-left: 4px\"\n        class=\"cursor-pointer\"\n        @click=\"toggleHandleBar\"\n      >\n        <CaretBottom v-show=\"!handleBarActive\" />\n        <CaretTop v-show=\"handleBarActive\" />\n      </el-icon>\n    </div>\n    <transition name=\"el-zoom-in-top\">\n      <el-row v-show=\"handleBarActive\">\n        <el-col\n          :span=\"20\"\n          :offset=\"2\"\n        >\n          <el-row\n            class=\"handle-bar\"\n            :gutter=\"16\"\n          >\n            <el-col :span=\"12\">\n              <el-select\n                v-model=\"selectedPicBed\"\n                multiple\n                collapse-tags\n                size=\"small\"\n                style=\"width: 100%\"\n                :placeholder=\"$T('CHOOSE_SHOWED_PICBED')\"\n              >\n                <el-option\n                  v-for=\"item in visiblePicBedList\"\n                  :key=\"item.type\"\n                  :label=\"item.name\"\n                  :value=\"item.type\"\n                />\n              </el-select>\n            </el-col>\n            <el-col :span=\"12\">\n              <el-select\n                v-model=\"pasteStyle\"\n                size=\"small\"\n                style=\"width: 100%\"\n                :placeholder=\"$T('CHOOSE_PASTE_FORMAT')\"\n                @change=\"handlePasteStyleChange\"\n              >\n                <el-option\n                  v-for=\"(value, key) in pasteStyleMap\"\n                  :key=\"key\"\n                  :label=\"key\"\n                  :value=\"value\"\n                />\n              </el-select>\n            </el-col>\n          </el-row>\n          <el-row\n            class=\"handle-bar\"\n            :gutter=\"16\"\n          >\n            <el-col :span=\"12\">\n              <el-input\n                v-model=\"searchText\"\n                :placeholder=\"$T('SEARCH')\"\n                size=\"small\"\n              >\n                <template #suffix>\n                  <el-icon\n                    class=\"el-input__icon\"\n                    style=\"cursor: pointer;\"\n                    @click=\"cleanSearch\"\n                  >\n                    <close />\n                  </el-icon>\n                </template>\n              </el-input>\n            </el-col>\n            <GalleryToolbar\n              :selected-list=\"selectedList\"\n              :filter-list=\"filterList\"\n              :is-all-selected=\"isAllSelected\"\n              @multi-copy=\"multiCopy\"\n              @multi-remove=\"multiRemove\"\n              @toggle-select-all=\"toggleSelectAll\"\n              @select-more=\"selectMore\"\n            />\n          </el-row>\n        </el-col>\n      </el-row>\n    </transition>\n    <el-row\n      class=\"gallery-list\"\n      :class=\"{ small: handleBarActive }\"\n    >\n      <el-col\n        :span=\"20\"\n        :offset=\"2\"\n      >\n        <el-row :gutter=\"16\">\n          <photo-slider\n            :items=\"filterList\"\n            :visible=\"gallerySliderControl.visible\"\n            :index=\"gallerySliderControl.index\"\n            :should-transition=\"true\"\n            @change-index=\"zoomImage\"\n            @click-mask=\"handleClose\"\n            @close-modal=\"handleClose\"\n          />\n          <el-col\n            v-for=\"(item, index) in filterList\"\n            :key=\"item.id\"\n            :span=\"6\"\n            class=\"gallery-list__img\"\n          >\n            <div\n              class=\"gallery-list__item\"\n              @click=\"zoomImage(index)\"\n            >\n              <img\n                v-lazy=\"item.imgUrl\"\n                class=\"gallery-list__item-img\"\n              >\n            </div>\n            <div\n              class=\"gallery-list__file-name\"\n              :title=\"item.fileName\"\n            >\n              {{ item.fileName }}\n            </div>\n            <el-row\n              class=\"gallery-list__tool-panel\"\n              justify=\"space-between\"\n              align=\"middle\"\n            >\n              <el-row>\n                <el-icon\n                  class=\"cursor-pointer document\"\n                  @click=\"copy(item)\"\n                >\n                  <Document />\n                </el-icon>\n                <el-icon\n                  class=\"cursor-pointer edit\"\n                  @click=\"openDialog(item)\"\n                >\n                  <Edit />\n                </el-icon>\n                <el-icon\n                  class=\"cursor-pointer delete\"\n                  @click=\"remove(item.id)\"\n                >\n                  <Delete />\n                </el-icon>\n              </el-row>\n              <el-checkbox\n                v-model=\"selectedList[item.id ? item.id : '']\"\n                @change=\"(val) => handleChooseImage(val, index)\"\n              />\n            </el-row>\n          </el-col>\n        </el-row>\n      </el-col>\n    </el-row>\n    <el-dialog\n      v-model=\"dialogVisible\"\n      :title=\"$T('CHANGE_IMAGE_URL')\"\n      width=\"500px\"\n      :modal-append-to-body=\"false\"\n    >\n      <el-input v-model=\"imgInfo.imgUrl\" />\n      <template\n        #footer\n      >\n        <el-button @click=\"dialogVisible = false\">\n          {{ $T('CANCEL') }}\n        </el-button>\n        <el-button\n          type=\"primary\"\n          @click=\"confirmModify\"\n        >\n          {{ $T('CONFIRM') }}\n        </el-button>\n      </template>\n    </el-dialog>\n  </div>\n</template>\n<script lang=\"ts\" setup>\nimport type { IResult } from '@picgo/store/dist/types'\nimport { PASTE_TEXT } from '#/events/constants'\nimport { CheckboxValueType, ElMessageBox } from 'element-plus'\nimport { Close, CaretBottom, Document, Edit, Delete, CaretTop } from '@element-plus/icons-vue'\nimport {\n  ipcRenderer,\n  clipboard\n} from 'electron'\nimport { computed, nextTick, onActivated, onBeforeUnmount, onBeforeMount, reactive, ref, watch } from 'vue'\nimport { saveConfig, sendRPC, sendToMain } from '@/utils/dataSender'\nimport { onBeforeRouteUpdate } from 'vue-router'\nimport { T as $T } from '@/i18n/index'\nimport $$db from '@/utils/db'\nimport GalleryToolbar from './components/gallery/GalleryToolbar.vue'\nimport { IRPCActionType } from '~/universal/types/enum'\nimport { getRawData } from '@/utils/common'\nimport { showNotification } from '@/utils/notification'\nimport { useStore } from '@/hooks/useStore'\nconst images = ref<ImgInfo[]>([])\nconst dialogVisible = ref(false)\nconst imgInfo = reactive({\n  id: '',\n  imgUrl: ''\n})\nconst $confirm = ElMessageBox.confirm\nconst selectedList: IObjT<boolean> = reactive({})\nconst gallerySliderControl = reactive({\n  visible: false,\n  index: 0\n})\nconst selectedPicBed = ref<string[]>([])\nconst lastSelected = ref<number>(-1)\nconst isShiftKeyPress = ref<boolean>(false)\nconst searchText = ref<string>('')\nconst handleBarActive = ref<boolean>(false)\nconst pasteStyle = ref<string>('')\nconst pasteStyleMap = {\n  Markdown: 'markdown',\n  HTML: 'HTML',\n  URL: 'URL',\n  UBB: 'UBB',\n  Custom: 'Custom'\n}\nconst store = useStore()\nconst picBed = computed(() => store?.state.picBeds ?? [])\nconst visiblePicBedList = computed(() => picBed.value.filter(item => item.visible))\nonBeforeRouteUpdate((to, from) => {\n  if (from.name === 'gallery') {\n    clearSelectedList()\n  }\n  if (to.name === 'gallery') {\n    updateGallery()\n  }\n})\n\nonBeforeMount(async () => {\n  ipcRenderer.on(IRPCActionType.UPDATE_GALLERY, () => {\n    nextTick(async () => {\n      updateGallery()\n    })\n  })\n  store?.refreshPicBeds()\n  store?.refreshAppConfig()\n  updateGallery()\n\n  document.addEventListener('keydown', handleDetectShiftKey)\n  document.addEventListener('keyup', handleDetectShiftKey)\n})\n\nfunction handleDetectShiftKey (event: KeyboardEvent) {\n  if (event.key === 'Shift') {\n    if (event.type === 'keydown') {\n      isShiftKeyPress.value = true\n    } else if (event.type === 'keyup') {\n      isShiftKeyPress.value = false\n    }\n  }\n}\n\nconst filterList = computed(() => {\n  return getGallery()\n})\n\nconst isAllSelected = computed(() => {\n  const values = Object.values(selectedList)\n  if (values.length === 0) {\n    return false\n  } else {\n    return filterList.value.every(item => {\n      return selectedList[item.id!]\n    })\n  }\n})\n\nfunction getGallery (): IGalleryItem[] {\n  if (searchText.value || selectedPicBed.value.length > 0) {\n    return images.value\n      .filter(item => {\n        let isInSelectedPicBed = true\n        let isIncludesSearchText = true\n        if (selectedPicBed.value.length > 0) {\n          isInSelectedPicBed = selectedPicBed.value.some(type => type === item.type)\n        }\n        if (searchText.value) {\n          isIncludesSearchText = item.fileName?.includes(searchText.value) || false\n        }\n        return isIncludesSearchText && isInSelectedPicBed\n      }).map(item => {\n        return {\n          ...item,\n          src: item.imgUrl || '',\n          key: (item.id || `${Date.now()}`),\n          intro: item.fileName || ''\n        }\n      })\n  } else {\n    return images.value.map(item => {\n      return {\n        ...item,\n        src: item.imgUrl || '',\n        key: (item.id || `${Date.now()}`),\n        intro: item.fileName || ''\n      }\n    })\n  }\n}\n\nasync function updateGallery () {\n  images.value = (await $$db.get({ orderBy: 'desc' })).data\n}\n\nwatch(() => filterList, () => {\n  clearSelectedList()\n})\n\nwatch(picBed, (list) => {\n  if (selectedPicBed.value.length === 0) return\n  const visibleTypes = new Set(list.filter(item => item.visible).map(item => item.type))\n  selectedPicBed.value = selectedPicBed.value.filter(type => visibleTypes.has(type))\n})\n\nfunction handleChooseImage (val: CheckboxValueType, index: number) {\n  if (val === true) {\n    handleBarActive.value = true\n    if (lastSelected.value !== -1 && isShiftKeyPress.value) {\n      const min = Math.min(lastSelected.value, index)\n      const max = Math.max(lastSelected.value, index)\n      for (let i = min + 1; i < max; i++) {\n        const id = filterList.value[i].id!\n        selectedList[id] = true\n      }\n    }\n    lastSelected.value = index\n  }\n}\n\nfunction clearSelectedList () {\n  isShiftKeyPress.value = false\n  Object.keys(selectedList).forEach(key => {\n    selectedList[key] = false\n  })\n  lastSelected.value = -1\n}\n\nfunction zoomImage (index: number) {\n  gallerySliderControl.index = index\n  gallerySliderControl.visible = true\n  changeZIndexForGallery(true)\n}\n\nfunction changeZIndexForGallery (isOpen: boolean) {\n  if (isOpen) {\n    // @ts-ignore\n    document.querySelector('.main-content.el-row').style.zIndex = 101\n  } else {\n    // @ts-ignore\n    document.querySelector('.main-content.el-row').style.zIndex = 10\n  }\n}\n\nfunction handleClose () {\n  gallerySliderControl.index = 0\n  gallerySliderControl.visible = false\n  changeZIndexForGallery(false)\n}\n\nasync function copy (item: ImgInfo) {\n  const copyLink = await ipcRenderer.invoke(PASTE_TEXT, getRawData(item))\n  const obj = {\n    title: $T('COPY_LINK_SUCCEED'),\n    body: copyLink\n    // sometimes will cause lagging\n    // icon: item.url || item.imgUrl\n  }\n  showNotification({\n    title: obj.title,\n    body: obj.body\n  })\n}\n\nfunction remove (id?: string) {\n  if (id) {\n    $confirm($T('TIPS_REMOVE_LINK'), $T('TIPS_NOTICE'), {\n      confirmButtonText: $T('CONFIRM'),\n      cancelButtonText: $T('CANCEL'),\n      type: 'warning'\n    }).then(async () => {\n      const file = await $$db.getById(id)\n      await $$db.removeById(id)\n      sendToMain('removeFiles', [file])\n      const obj = {\n        title: $T('OPERATION_SUCCEED'),\n        body: ''\n      }\n      showNotification({\n        title: obj.title,\n        body: obj.body\n      })\n      updateGallery()\n    }).catch((e) => {\n      console.log(e)\n      return true\n    })\n  }\n}\n\nfunction openDialog (item: ImgInfo) {\n  imgInfo.id = item.id!\n  imgInfo.imgUrl = item.imgUrl as string\n  dialogVisible.value = true\n}\n\nasync function confirmModify () {\n  await $$db.updateById(imgInfo.id, {\n    imgUrl: imgInfo.imgUrl\n  })\n  const obj = {\n    title: $T('CHANGE_IMAGE_URL_SUCCEED'),\n    body: imgInfo.imgUrl\n    // icon: this.imgInfo.imgUrl\n  }\n  showNotification({\n    title: obj.title,\n    body: obj.body\n  })\n  dialogVisible.value = false\n  updateGallery()\n}\n\nfunction cleanSearch () {\n  searchText.value = ''\n}\n\nfunction toggleSelectAll () {\n  const result = !isAllSelected.value\n  filterList.value.forEach(item => {\n    selectedList[item.id!] = result\n  })\n}\n\nfunction selectMore () {\n  sendRPC(IRPCActionType.GET_GALLERY_MENU_LIST, filterList.value.filter(item => {\n    return selectedList[item.id!]\n  }))\n}\n\nfunction multiRemove () {\n  // selectedList -> { [id]: true or false }; true means selected. false means not selected.\n  const multiRemoveNumber = Object.values(selectedList).filter(item => item).length\n  if (multiRemoveNumber) {\n    $confirm($T('TIPS_WILL_REMOVE_CHOOSED_IMAGES', {\n      m: multiRemoveNumber\n    }), $T('TIPS_NOTICE'), {\n      confirmButtonText: $T('CONFIRM'),\n      cancelButtonText: $T('CANCEL'),\n      type: 'warning'\n    }).then(async () => {\n      const files: IResult<ImgInfo>[] = []\n      const imageIDList = Object.keys(selectedList)\n      for (let i = 0; i < imageIDList.length; i++) {\n        const key = imageIDList[i]\n        if (selectedList[key]) {\n          const file = await $$db.getById<ImgInfo>(key)\n          if (file) {\n            files.push(file)\n            await $$db.removeById(key)\n          }\n        }\n      }\n      clearSelectedList()\n      // TODO: check this\n      // selectedList = {} // 只有删除才能将这个置空\n      const obj = {\n        title: $T('OPERATION_SUCCEED'),\n        body: ''\n      }\n      sendToMain('removeFiles', files)\n      showNotification({\n        title: obj.title,\n        body: obj.body\n      })\n      updateGallery()\n    }).catch(() => {\n      return true\n    })\n  }\n}\n\nasync function multiCopy () {\n  if (Object.values(selectedList).some(item => item)) {\n    const copyString: string[] = []\n    // selectedList -> { [id]: true or false }; true means selected. false means not selected.\n    const imageIDList = Object.keys(selectedList)\n    for (let i = 0; i < imageIDList.length; i++) {\n      const key = imageIDList[i]\n      if (selectedList[key]) {\n        const item = await $$db.getById<ImgInfo>(key)\n        if (item) {\n          const txt = await ipcRenderer.invoke(PASTE_TEXT, getRawData(item))\n          copyString.push(txt)\n          selectedList[key] = false\n        }\n      }\n    }\n    const obj = {\n      title: $T('BATCH_COPY_LINK_SUCCEED'),\n      body: copyString.join('\\n')\n    }\n    clipboard.writeText(copyString.join('\\n'))\n    showNotification({\n      title: obj.title,\n      body: obj.body\n    })\n  }\n}\n\nfunction toggleHandleBar () {\n  handleBarActive.value = !handleBarActive.value\n}\n\nasync function handlePasteStyleChange (val: string) {\n  await saveConfig('settings.pasteStyle', val)\n  pasteStyle.value = val\n}\n\nonBeforeUnmount(() => {\n  ipcRenderer.removeAllListeners('updateGallery')\n})\n\nconst applyAppConfig = () => {\n  const settings = store?.state.appConfig?.settings ?? {}\n  pasteStyle.value = settings.pasteStyle || 'markdown'\n}\n\nwatch(() => store?.state.appConfig, () => {\n  applyAppConfig()\n}, { immediate: true })\n\nonActivated(() => {\n  applyAppConfig()\n})\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'GalleryPage'\n}\n</script>\n<style lang='stylus'>\n.PhotoSlider\n  &__BannerIcon\n    &:nth-child(1)\n      display none\n  &__Counter\n    margin-top 20px\n.view-title\n  color #eee\n  font-size 20px\n  text-align center\n  margin 10px auto\n  .sub-title\n    font-size 14px\n  .el-icon-caret-bottom\n    cursor: pointer\n    transition all .2s ease-in-out\n    &.active\n      transform: rotate(180deg)\n#gallery-view\n  height 100%\n  .cursor-pointer\n    cursor pointer\n#gallery-view\n  position relative\n  .round\n    border-radius 14px\n  .pull-right\n    float right\n  .gallery-list\n    height 360px\n    box-sizing border-box\n    padding 8px 0\n    overflow-y auto\n    overflow-x hidden\n    position absolute\n    top: 38px\n    transition all .2s ease-in-out .1s\n    width 100%\n    &.small\n      height: 287px\n      top: 113px\n    &__img\n      // height 150px\n      position relative\n      margin-bottom 8px\n    &__item\n      width 100%\n      height 120px\n      transition all .2s ease-in-out\n      cursor pointer\n      margin-bottom 4px\n      overflow hidden\n      display flex\n      margin-bottom 6px\n      &-fake\n        position absolute\n        top 0\n        left 0\n        opacity 0\n        width 100%\n        z-index -1\n      &:hover\n        transform scale(1.1)\n      &-img\n        width 100%\n        object-fit contain\n    &__tool-panel\n      color #ddd\n      margin-bottom 4px\n      display flex\n      .el-checkbox\n        height 16px\n      i\n        cursor pointer\n        transition all .2s ease-in-out\n        margin-right 4px\n        &.document\n          &:hover\n            color #49B1F5\n        &.edit\n          &:hover\n            color #69C282\n        &.delete\n          &:hover\n            color #F15140\n    &__file-name\n      overflow hidden\n      text-overflow ellipsis\n      white-space nowrap\n      color #ddd\n      font-size 14px\n      margin-bottom 4px\n  .handle-bar\n    color #ddd\n    margin-bottom 10px\n</style>\n"
  },
  {
    "path": "src/renderer/pages/MiniPage.vue",
    "content": "<template>\n  <div\n    id=\"mini-page\"\n    :style=\"{ backgroundImage: 'url(' + logo + ')' }\"\n    :class=\"{ linux: os === 'linux' }\"\n  >\n    <!-- <i class=\"el-icon-upload2\"></i> -->\n    <div\n      id=\"upload-area\"\n      :class=\"{ 'is-dragover': dragover, uploading: showProgress, linux: os === 'linux' }\"\n      :style=\"{ backgroundPosition: '0 ' + progress + '%'}\"\n      @drop.prevent=\"onDrop\"\n      @dragover.prevent=\"dragover = true\"\n      @dragleave.prevent=\"dragover = false\"\n    >\n      <div\n        id=\"upload-dragger\"\n        @dblclick=\"openUploadWindow\"\n      >\n        <input\n          id=\"file-uploader\"\n          type=\"file\"\n          multiple\n          @change=\"onChange\"\n        >\n      </div>\n    </div>\n  </div>\n</template>\n<script lang=\"ts\" setup>\n// import mixin from '@/utils/mixin'\n// import { Component, Vue, Watch } from 'vue-property-decorator'\nimport { T as $T } from '@/i18n/index'\nimport { showNotification } from '@/utils/notification'\nimport {\n  ipcRenderer,\n  IpcRendererEvent\n} from 'electron'\nimport { onBeforeUnmount, onBeforeMount, ref, watch } from 'vue'\nimport { LOG_INVALID_URL_LINES, SHOW_MINI_PAGE_MENU, SET_MINI_WINDOW_POS } from '~/universal/events/constants'\nimport {\n  isUrl,\n  parseNewlineSeparatedUrls\n} from '~/universal/utils/common'\nimport { sendToMain } from '@/utils/dataSender'\nimport { getFilePath } from '@/utils/common'\nimport { getRendererStaticFileUrl } from '@/utils/static'\nimport { useOS } from '@/hooks/useOS'\nconst logo = ref(getRendererStaticFileUrl('squareLogo.png'))\nconst dragover = ref(false)\nconst progress = ref(0)\nconst showProgress = ref(false)\nconst showError = ref(false)\nconst dragging = ref(false)\nconst wX = ref(-1)\nconst wY = ref(-1)\nconst screenX = ref(-1)\nconst screenY = ref(-1)\nconst os = useOS()\n\nonBeforeMount(() => {\n  ipcRenderer.on('uploadProgress', (event: IpcRendererEvent, _progress: number) => {\n    if (_progress !== -1) {\n      showProgress.value = true\n      progress.value = _progress\n    } else {\n      progress.value = 100\n      showError.value = true\n    }\n  })\n  window.addEventListener('mousedown', handleMouseDown, false)\n  window.addEventListener('mousemove', handleMouseMove, false)\n  window.addEventListener('mouseup', handleMouseUp, false)\n})\n\nwatch(progress, (val) => {\n  if (val === 100) {\n    setTimeout(() => {\n      showProgress.value = false\n      showError.value = false\n    }, 1000)\n    setTimeout(() => {\n      progress.value = 0\n    }, 1200)\n  }\n})\n\nasync function onDrop (e: DragEvent) {\n  dragover.value = false\n  const files = e.dataTransfer?.files!\n\n  // send files first\n  if (files?.length) {\n    ipcSendFiles(e.dataTransfer?.files!)\n    return\n  }\n\n  const dataTransfer = e.dataTransfer\n  if (!dataTransfer) return\n\n  const uriList = dataTransfer.getData('text/uri-list')\n  if (uriList) {\n    await handleUriListDrop(uriList, dataTransfer.getData('text/html'))\n    return\n  }\n\n  const plainText = dataTransfer.getData('text/plain')\n  if (plainText) {\n    await handlePlainTextDrop(plainText)\n    return\n  }\n\n  showNotification({\n    title: $T('TIPS_ERROR'),\n    body: $T('TIPS_DRAG_VALID_PICTURE_OR_URL')\n  })\n}\n\nasync function uploadUrls (urls: string[], invalidLines: string[]) {\n  if (invalidLines.length) {\n    sendToMain(LOG_INVALID_URL_LINES, invalidLines)\n    showNotification({\n      title: $T('TIPS_WARNING'),\n      body: $T('TIPS_SKIPPED_INVALID_URLS', { n: invalidLines.length })\n    })\n  }\n\n  sendToMain('uploadChoosedFiles', urls.map((url) => ({ path: url })))\n}\n\nasync function handlePlainTextDrop (plainText: string) {\n  const { urls, invalidLines } = parseNewlineSeparatedUrls(plainText, { source: 'plain' })\n  if (!urls.length) {\n    showNotification({\n      title: $T('TIPS_ERROR'),\n      body: $T('TIPS_DRAG_VALID_PICTURE_OR_URL')\n    })\n    return\n  }\n  await uploadUrls(urls, invalidLines)\n}\n\nasync function handleUriListDrop (uriListText: string, urlString: string) {\n  const { urls, invalidLines } = parseNewlineSeparatedUrls(uriListText, { source: 'uri-list' })\n  if (urls.length) {\n    await uploadUrls(urls, invalidLines)\n    return\n  }\n\n  const urlMatch = urlString.match(/<img.*src=\"(.*?)\"/)\n  if (urlMatch && isUrl(urlMatch[1])) {\n    await uploadUrls([urlMatch[1]], invalidLines)\n    return\n  }\n\n  showNotification({\n    title: $T('TIPS_ERROR'),\n    body: $T('TIPS_DRAG_VALID_PICTURE_OR_URL')\n  })\n}\n\nfunction openUploadWindow () {\n  // @ts-ignore\n  document.getElementById('file-uploader').click()\n}\n\nfunction onChange (e: any) {\n  ipcSendFiles(e.target.files)\n  // @ts-ignore\n  document.getElementById('file-uploader').value = ''\n}\n\nfunction ipcSendFiles (files: FileList) {\n  const sendFiles: IFileWithPath[] = []\n  Array.from(files).forEach((item) => {\n    const filePath = getFilePath(item)\n    if (!filePath) return\n    sendFiles.push({\n      name: item.name,\n      path: filePath\n    })\n  })\n  if (!sendFiles.length) return\n  sendToMain('uploadChoosedFiles', sendFiles)\n}\n\nfunction handleMouseDown (e: MouseEvent) {\n  dragging.value = true\n  wX.value = e.pageX\n  wY.value = e.pageY\n  screenX.value = e.screenX\n  screenY.value = e.screenY\n}\n\nfunction handleMouseMove (e: MouseEvent) {\n  e.preventDefault()\n  e.stopPropagation()\n  if (dragging.value) {\n    const xLoc = e.screenX - wX.value\n    const yLoc = e.screenY - wY.value\n    sendToMain(SET_MINI_WINDOW_POS, {\n      x: xLoc,\n      y: yLoc,\n      width: 64,\n      height: 64\n    })\n    // remote.BrowserWindow.getFocusedWindow()!.setBounds({\n    //   x: xLoc,\n    //   y: yLoc,\n    //   width: 64,\n    //   height: 64\n    // })\n  }\n}\n\nfunction handleMouseUp (e: MouseEvent) {\n  dragging.value = false\n  if (screenX.value === e.screenX && screenY.value === e.screenY) {\n    if (e.button === 0) { // left mouse\n      openUploadWindow()\n    } else {\n      openContextMenu()\n    }\n  }\n}\n\nfunction openContextMenu () {\n  sendToMain(SHOW_MINI_PAGE_MENU)\n}\n\nonBeforeUnmount(() => {\n  ipcRenderer.removeAllListeners('uploadProgress')\n  window.removeEventListener('mousedown', handleMouseDown, false)\n  window.removeEventListener('mousemove', handleMouseMove, false)\n  window.removeEventListener('mouseup', handleMouseUp, false)\n})\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'MiniPage'\n}\n</script>\n<style lang='stylus'>\n  #mini-page\n    background #409EFF\n    color #FFF\n    height 100vh\n    width 100vw\n    border-radius 50%\n    text-align center\n    line-height 100vh\n    font-size 40px\n    background-size 90vh 90vw\n    background-position center center\n    background-repeat no-repeat\n    position relative\n    border 4px solid #fff\n    box-sizing border-box\n    cursor pointer\n    &.linux\n      border-radius 0\n      background-size 100vh 100vw\n    #upload-area\n      height 100%\n      width 100%\n      border-radius 50%\n      transition all .2s ease-in-out\n      &.linux\n        border-radius 0\n      &.uploading\n        background: linear-gradient(to top, #409EFF 50%, #fff 51%)\n        background-size 200%\n      #upload-dragger\n        height 100%\n      &.is-dragover\n        background rgba(0,0,0,0.3)\n    #file-uploader\n      display none\n</style>\n"
  },
  {
    "path": "src/renderer/pages/PicGoCloud.vue",
    "content": "<template>\n  <div id=\"picgo-cloud-page\">\n    <div class=\"view-title\">\n      {{ $T('PICGO_CLOUD_TITLE') }}\n    </div>\n    <el-row>\n      <el-col\n        :span=\"20\"\n        :offset=\"2\"\n      >\n        <div\n          v-loading=\"isUserInfoLoading\"\n          element-loading-background=\"rgba(0, 0, 0, 0.6)\"\n          class=\"mt-[16px] rounded-[8px] border border-[rgba(255,255,255,0.06)] bg-[rgba(130,130,130,0.12)] p-[16px]\"\n        >\n          <el-alert\n            v-if=\"errorMessage\"\n            class=\"!mb-[12px]\"\n            type=\"error\"\n            show-icon\n            :title=\"$T('PICGO_CLOUD_ERROR_TITLE')\"\n            :description=\"errorMessage\"\n          />\n\n          <template v-if=\"isLoginInProgress\">\n            <div class=\"text-[12px] text-[#bbb] leading-[18px] mb-[12px]\">\n              {{ $T('PICGO_CLOUD_LOGIN_IN_PROGRESS') }}\n            </div>\n          </template>\n\n          <template v-if=\"userInfo\">\n            <div class=\"flex items-center gap-[12px] mb-[12px]\">\n              <div class=\"text-[16px] font-medium text-[#ddd] leading-[22px]\">\n                {{ $T('PICGO_CLOUD_LOGGED_IN_AS', { user: userInfo.user }) }}\n              </div>\n              <el-button\n                size=\"small\"\n                @click=\"handleOpenCloud\"\n              >\n                {{ $T('PICGO_CLOUD_OPEN') }}\n              </el-button>\n              <el-button\n                type=\"danger\"\n                size=\"small\"\n                @click=\"handleLogout\"\n              >\n                {{ $T('PICGO_CLOUD_LOGOUT') }}\n              </el-button>\n            </div>\n\n            <div class=\"flex items-center gap-[12px] flex-wrap\">\n              <el-button\n                type=\"primary\"\n                :loading=\"isConfigSyncRunning\"\n                :disabled=\"isConfigSyncBusy\"\n                @click=\"handleConfigSyncStart\"\n              >\n                {{ $T('PICGO_CLOUD_CONFIG_SYNC') }}\n              </el-button>\n\n              <div class=\"flex items-center gap-[8px] text-[#bbb]\">\n                <span class=\"text-[12px] text-[#bbb] leading-[18px] shrink-0 whitespace-nowrap\">\n                  {{ $T('PICGO_CLOUD_ENCRYPTION_MODE_LABEL') }}\n                </span>\n                <el-select\n                  v-model=\"encryptionMethodValue\"\n                  size=\"small\"\n                  class=\"w-[180px] shrink-0\"\n                  :disabled=\"isEncryptionModeDisabled\"\n                >\n                  <el-option\n                    :label=\"$T('PICGO_CLOUD_ENCRYPTION_MODE_AUTO')\"\n                    :value=\"IPicGoCloudEncryptionMethod.AUTO\"\n                  />\n                  <el-option\n                    :label=\"$T('PICGO_CLOUD_ENCRYPTION_MODE_SERVER')\"\n                    :value=\"IPicGoCloudEncryptionMethod.SSE\"\n                  />\n                  <el-option\n                    :label=\"$T('PICGO_CLOUD_ENCRYPTION_MODE_E2E')\"\n                    :value=\"IPicGoCloudEncryptionMethod.E2EE\"\n                  />\n                </el-select>\n                <el-tooltip\n                  effect=\"dark\"\n                  placement=\"top\"\n                  :enterable=\"true\"\n                >\n                  <template #content>\n                    <div class=\"text-[12px] leading-[18px] max-w-[320px]\">\n                      <div>{{ $T('PICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO') }}</div>\n                      <div>{{ $T('PICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER') }}</div>\n                      <div>{{ $T('PICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E') }}</div>\n                      <div class=\"mt-[6px]\">\n                        <el-link\n                          type=\"primary\"\n                          :underline=\"false\"\n                          @click.stop.prevent=\"handleOpenDocs\"\n                        >\n                          {{ $T('PICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC') }}\n                        </el-link>\n                      </div>\n                    </div>\n                  </template>\n                  <el-icon class=\"cursor-pointer text-[#999] hover:text-blue\">\n                    <QuestionFilled />\n                  </el-icon>\n                </el-tooltip>\n              </div>\n            </div>\n            <div class=\"mt-[8px] text-[12px] text-[#999] leading-[18px]\">\n              {{ $T('PICGO_CLOUD_LAST_SYNC_TIME', { time: lastSyncedAtText || $T('PICGO_CLOUD_LAST_SYNC_TIME_NONE') }) }}\n            </div>\n          </template>\n\n          <template v-else>\n            <div class=\"text-[12px] text-[#bbb] leading-[18px] mb-[12px]\">\n              {{ $T('PICGO_CLOUD_NOT_LOGGED_IN') }}\n            </div>\n            <div class=\"flex gap-[8px]\">\n              <el-button\n                type=\"primary\"\n                :loading=\"isLoginInProgress\"\n                :disabled=\"isLoginInProgress || !hasAgreedToTermsAndPrivacy\"\n                @click=\"handleLogin\"\n              >\n                {{ $T('PICGO_CLOUD_LOGIN') }}\n              </el-button>\n              <el-button\n                v-if=\"isLoginInProgress\"\n                @click=\"handleDisposeLoginFlow\"\n              >\n                {{ $T('PICGO_CLOUD_CANCEL_LOGIN') }}\n              </el-button>\n            </div>\n\n            <el-checkbox\n              v-model=\"hasAgreedToTermsAndPrivacy\"\n              :disabled=\"isLoginInProgress\"\n              class=\"mt-[8px]\"\n            >\n              <span class=\"text-[12px] text-[#bbb] leading-[18px]\">\n                {{ $T('PICGO_CLOUD_AGREE_PREFIX') }}\n                <el-link\n                  type=\"primary\"\n                  :underline=\"false\"\n                  @click.stop.prevent=\"handleOpenTerms\"\n                >\n                  {{ $T('PICGO_CLOUD_TERMS_OF_SERVICE') }}\n                </el-link>\n                {{ $T('PICGO_CLOUD_AGREE_AND') }}\n                <el-link\n                  type=\"primary\"\n                  :underline=\"false\"\n                  @click.stop.prevent=\"handleOpenPrivacy\"\n                >\n                  {{ $T('PICGO_CLOUD_PRIVACY_POLICY') }}\n                </el-link>\n              </span>\n            </el-checkbox>\n          </template>\n        </div>\n      </el-col>\n    </el-row>\n\n    <ConfigSyncConflictDialog\n      v-model=\"isConflictDialogVisible\"\n      :conflicts=\"configSyncConflicts\"\n      :confirm-loading=\"isApplyResolutionLoading\"\n      @abort=\"handleAbortConfigSync\"\n      @confirm=\"handleConfirmConfigSyncResolution\"\n    />\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'\nimport { T as $T } from '@/i18n/index'\nimport { useStore } from '@/hooks/useStore'\nimport type { IPicGoCloudUserInfo } from '#/types/cloud'\nimport { IPicGoCloudLoginStatus, IPicGoCloudRequestStatus } from '@/store'\nimport { invokeRPC } from '@/utils/dataSender'\nimport { IRPCActionType } from '~/universal/types/enum'\nimport { openURL } from '@/utils/common'\nimport { ElMessage, ElMessageBox } from 'element-plus'\nimport ConfigSyncConflictDialog from '@/components/picgoCloud/ConfigSyncConflictDialog.vue'\nimport dayjs from 'dayjs'\nimport { QuestionFilled } from '@element-plus/icons-vue'\nimport {\n  IPicGoCloudConfigSyncSessionStatus,\n  IPicGoCloudConfigSyncToastType,\n  IPicGoCloudEncryptionMethod,\n  type IPicGoCloudConfigSyncRunResult,\n  type IPicGoCloudConfigSyncState,\n  type IPicGoCloudConfigSyncResolution\n} from '#/types/cloudConfigSync'\n\nconst store = useStore()\n\nconst userInfo = computed(() => store?.state.picgoCloud.userInfo)\nconst userInfoStatus = computed(() => store?.state.picgoCloud.userInfoStatus ?? IPicGoCloudRequestStatus.IDLE)\nconst userInfoError = computed(() => store?.state.picgoCloud.userInfoError)\nconst loginStatus = computed(() => store?.state.picgoCloud.loginStatus ?? IPicGoCloudLoginStatus.IDLE)\nconst loginError = computed(() => store?.state.picgoCloud.loginError)\nconst hasAgreedToTermsAndPrivacy = computed({\n  get: () => store?.state.picgoCloud.hasAgreedToTermsAndPrivacy ?? false,\n  set: (value: boolean) => {\n    store?.setPicGoCloudHasAgreedToTermsAndPrivacy(value)\n  }\n})\n\nconst TERMS_URL = 'https://picgo.app/terms/'\nconst PRIVACY_URL = 'https://picgo.app/privacy/'\nconst DOC_URL = 'https://picgo.app/blog/2026/picgo-configuration-sync-release/'\nconst CLOUD_URL = 'https://cloud.picgo.app'\n\nconst isUserInfoLoading = computed(() => userInfoStatus.value === IPicGoCloudRequestStatus.LOADING)\nconst isLoginInProgress = computed(() => loginStatus.value === IPicGoCloudLoginStatus.IN_PROGRESS)\n\nconst errorMessage = computed(() => loginError.value || userInfoError.value)\n\nconst configSyncState = ref<IPicGoCloudConfigSyncState>({\n  sessionStatus: IPicGoCloudConfigSyncSessionStatus.IDLE,\n  encryptionMethod: undefined\n})\nconst isConfigSyncStateLoading = ref(false)\nconst isE2EPreferenceUpdating = ref(false)\nconst isApplyResolutionLoading = ref(false)\nconst isConflictDialogVisible = ref(false)\n\nconst configSyncSessionStatus = computed(() => configSyncState.value.sessionStatus)\nconst isConfigSyncRunning = computed(() => configSyncSessionStatus.value === IPicGoCloudConfigSyncSessionStatus.SYNCING)\nconst isConfigSyncBusy = computed(() => configSyncSessionStatus.value !== IPicGoCloudConfigSyncSessionStatus.IDLE)\nconst configSyncConflicts = computed(() => configSyncState.value.conflicts ?? [])\nconst lastSyncedAtText = computed<string | undefined>(() => {\n  const raw = configSyncState.value.lastSyncedAt\n  if (!raw) return undefined\n  const date = dayjs(raw)\n  if (!date.isValid()) return undefined\n  return date.format('YYYY-MM-DD HH:mm:ss')\n})\n\nconst isEncryptionModeDisabled = computed(() => {\n  if (isConfigSyncBusy.value) return true\n  return isConfigSyncStateLoading.value || isE2EPreferenceUpdating.value\n})\n\nconst encryptionMethodValue = computed<IPicGoCloudEncryptionMethod>({\n  get: () => configSyncState.value.encryptionMethod ?? IPicGoCloudEncryptionMethod.AUTO,\n  set: (value: IPicGoCloudEncryptionMethod) => {\n    handleSetEncryptionMethod(value)\n  }\n})\n\nconst handleOpenTerms = () => {\n  openURL(TERMS_URL)\n}\n\nconst handleOpenPrivacy = () => {\n  openURL(PRIVACY_URL)\n}\n\nconst handleOpenDocs = () => {\n  openURL(DOC_URL)\n}\n\nconst handleOpenCloud = () => {\n  openURL(CLOUD_URL)\n}\n\nconst refreshAppStateAfterSync = async () => {\n  if (!store) return\n  await store.refreshAppConfig()\n  await store.refreshPicBeds()\n}\n\nonBeforeMount(() => {\n  // First entry: only fetch when store is empty (undefined). Subsequent page entries read store.\n  if (!store) return\n  if (store.state.picgoCloud.userInfo !== undefined) {\n    if (store.state.picgoCloud.userInfo) {\n      loadConfigSyncState()\n    }\n    return\n  }\n  loadUserInfoAndMaybeHydrateCloudState()\n})\n\nconst loadUserInfoAndMaybeHydrateCloudState = async () => {\n  await loadUserInfo()\n  if (store?.state.picgoCloud.userInfo) {\n    await loadConfigSyncState()\n  }\n}\n\nconst loadUserInfo = async () => {\n  if (!store) return\n\n  store.setPicGoCloudUserInfoStatus(IPicGoCloudRequestStatus.LOADING)\n  store.setPicGoCloudUserInfoError(null)\n\n  const res = await invokeRPC<IPicGoCloudUserInfo | null>(IRPCActionType.PICGO_CLOUD_GET_USER_INFO)\n  if (!res.success) {\n    store.setPicGoCloudUserInfoStatus(IPicGoCloudRequestStatus.ERROR)\n    store.setPicGoCloudUserInfoError(res.error)\n    return\n  }\n  store.setPicGoCloudUserInfo(res.data)\n  store.setPicGoCloudUserInfoStatus(IPicGoCloudRequestStatus.IDLE)\n}\n\nconst applyConfigSyncState = (state: IPicGoCloudConfigSyncState) => {\n  configSyncState.value = state\n\n  if (state.sessionStatus === IPicGoCloudConfigSyncSessionStatus.CONFLICT) {\n    isConflictDialogVisible.value = true\n  }\n}\n\nconst loadConfigSyncState = async () => {\n  isConfigSyncStateLoading.value = true\n  const res = await invokeRPC<IPicGoCloudConfigSyncState>(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_GET_STATE)\n  isConfigSyncStateLoading.value = false\n\n  if (!res.success) {\n    ElMessage.error(res.error)\n    return\n  }\n\n  applyConfigSyncState(res.data)\n}\n\nconst showConfigSyncToast = (toastType: IPicGoCloudConfigSyncToastType, message: string) => {\n  if (toastType === IPicGoCloudConfigSyncToastType.SUCCESS) {\n    ElMessage.success(message)\n    return\n  }\n  if (toastType === IPicGoCloudConfigSyncToastType.WARNING) {\n    ElMessage.warning(message)\n    return\n  }\n  if (toastType === IPicGoCloudConfigSyncToastType.ERROR) {\n    ElMessage.error(message)\n    return\n  }\n  ElMessage.info(message)\n}\n\nconst promptRestartIfNeeded = async () => {\n  try {\n    await ElMessageBox.confirm(\n      $T('PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE'),\n      $T('PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE'),\n      {\n        type: 'warning',\n        confirmButtonText: $T('PICGO_CLOUD_CONFIG_SYNC_RESTART_NOW'),\n        cancelButtonText: $T('PICGO_CLOUD_CONFIG_SYNC_RESTART_LATER'),\n        closeOnClickModal: false,\n        closeOnPressEscape: false\n      }\n    )\n  } catch {\n    return\n  }\n\n  const res = await invokeRPC<void>(IRPCActionType.RELOAD_APP)\n  if (!res.success) {\n    ElMessage.error(res.error)\n  }\n}\n\nconst handleConfigSyncStart = async () => {\n  if (isConfigSyncBusy.value) return\n\n  // Optimistically switch to SYNCING for immediate loading feedback.\n  // The main process still owns the source-of-truth session state.\n  configSyncState.value = {\n    ...configSyncState.value,\n    sessionStatus: IPicGoCloudConfigSyncSessionStatus.SYNCING,\n    conflicts: undefined\n  }\n  ElMessage.info($T('PICGO_CLOUD_CONFIG_SYNC_STARTING'))\n\n  const res = await invokeRPC<IPicGoCloudConfigSyncRunResult>(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_START)\n  if (!res.success) {\n    ElMessage.error(res.error)\n    await loadConfigSyncState()\n    return\n  }\n\n  const runRes = res.data\n  applyConfigSyncState(runRes.state)\n  showConfigSyncToast(runRes.toastType, runRes.message)\n\n  if (runRes.authInvalidated && store) {\n    store.setPicGoCloudUserInfo(null)\n    store.setPicGoCloudUserInfoError(null)\n    store.setPicGoCloudUserInfoStatus(IPicGoCloudRequestStatus.IDLE)\n  }\n\n  if (runRes.shouldShowRestartPrompt) {\n    await refreshAppStateAfterSync()\n    await promptRestartIfNeeded()\n  }\n}\n\nconst handleAbortConfigSync = async () => {\n  const res = await invokeRPC<IPicGoCloudConfigSyncState>(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_ABORT)\n  if (!res.success) {\n    ElMessage.error(res.error)\n    return\n  }\n  applyConfigSyncState(res.data)\n  isConflictDialogVisible.value = false\n  ElMessage.warning($T('PICGO_CLOUD_CONFIG_SYNC_ABORTED'))\n}\n\nconst handleConfirmConfigSyncResolution = async (resolution: IPicGoCloudConfigSyncResolution) => {\n  isApplyResolutionLoading.value = true\n  const res = await invokeRPC<IPicGoCloudConfigSyncRunResult>(IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_APPLY_RESOLUTION, resolution)\n  isApplyResolutionLoading.value = false\n\n  if (!res.success) {\n    ElMessage.error(res.error)\n    return\n  }\n\n  const runRes = res.data\n  applyConfigSyncState(runRes.state)\n  showConfigSyncToast(runRes.toastType, runRes.message)\n\n  if (runRes.authInvalidated && store) {\n    isConflictDialogVisible.value = false\n    store.setPicGoCloudUserInfo(null)\n    store.setPicGoCloudUserInfoError(null)\n    store.setPicGoCloudUserInfoStatus(IPicGoCloudRequestStatus.IDLE)\n    return\n  }\n\n  if (runRes.shouldShowRestartPrompt) {\n    isConflictDialogVisible.value = false\n    await refreshAppStateAfterSync()\n    await promptRestartIfNeeded()\n  }\n}\n\nconst handleSetEncryptionMethod = async (nextMode: IPicGoCloudEncryptionMethod) => {\n  if (isE2EPreferenceUpdating.value) return\n  if (isConfigSyncBusy.value) return\n\n  const currentMode = encryptionMethodValue.value\n  if (nextMode === currentMode) return\n\n  const previousEncryptionMethod = configSyncState.value.encryptionMethod\n\n  // Only turning on E2E needs a warning confirmation.\n  // If the user cancels, we persist \"server-side encryption\" (`encryptionMethod='sse'`) per product requirement,\n  // rather than leaving it at AUTO.\n  let modeToPersist = nextMode\n  if (nextMode === IPicGoCloudEncryptionMethod.E2EE) {\n    try {\n      await ElMessageBox.confirm(\n        $T('PICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE'),\n        $T('PICGO_CLOUD_E2E_ENABLE_WARNING_TITLE'),\n        {\n          type: 'warning',\n          confirmButtonText: $T('CONFIRM'),\n          cancelButtonText: $T('CANCEL'),\n          closeOnClickModal: false,\n          closeOnPressEscape: false\n        }\n      )\n    } catch {\n      modeToPersist = IPicGoCloudEncryptionMethod.SSE\n    }\n  }\n\n  // Optimistically update local UI state so the dropdown reflects the user's selection immediately.\n  configSyncState.value = {\n    ...configSyncState.value,\n    encryptionMethod: modeToPersist\n  }\n\n  isE2EPreferenceUpdating.value = true\n  const res = await invokeRPC<IPicGoCloudEncryptionMethod | undefined>(\n    IRPCActionType.PICGO_CLOUD_CONFIG_SYNC_SET_E2E_PREFERENCE,\n    modeToPersist\n  )\n  isE2EPreferenceUpdating.value = false\n\n  if (!res.success) {\n    // Restore previous selection on failure.\n    configSyncState.value = {\n      ...configSyncState.value,\n      encryptionMethod: previousEncryptionMethod\n    }\n    ElMessage.error(res.error)\n    return\n  }\n\n  configSyncState.value = {\n    ...configSyncState.value,\n    encryptionMethod: res.data\n  }\n}\n\nconst handleLogin = async () => {\n  if (!store) return\n  if (!hasAgreedToTermsAndPrivacy.value) return\n  store.setPicGoCloudLoginStatus(IPicGoCloudLoginStatus.IN_PROGRESS)\n  store.setPicGoCloudLoginError(null)\n\n  const res = await invokeRPC<IPicGoCloudUserInfo>(IRPCActionType.PICGO_CLOUD_LOGIN)\n  if (!res.success) {\n    store.setPicGoCloudLoginError(res.error)\n    store.setPicGoCloudLoginStatus(IPicGoCloudLoginStatus.IDLE)\n    return\n  }\n\n  store.setPicGoCloudUserInfo(res.data)\n  store.setPicGoCloudUserInfoStatus(IPicGoCloudRequestStatus.IDLE)\n  store.setPicGoCloudUserInfoError(null)\n  store.setPicGoCloudLoginStatus(IPicGoCloudLoginStatus.IDLE)\n  await loadConfigSyncState()\n}\n\nconst handleDisposeLoginFlow = async () => {\n  if (!store) return\n  const res = await invokeRPC<boolean>(IRPCActionType.PICGO_CLOUD_DISPOSE_LOGIN_FLOW)\n  if (!res.success) {\n    store.setPicGoCloudLoginError(res.error)\n  }\n  store.setPicGoCloudLoginStatus(IPicGoCloudLoginStatus.IDLE)\n}\n\nconst handleLogout = async () => {\n  if (!store) return\n  const res = await invokeRPC<boolean>(IRPCActionType.PICGO_CLOUD_LOGOUT)\n  if (!res.success) {\n    store.setPicGoCloudLoginError(res.error)\n    return\n  }\n  store.setPicGoCloudUserInfo(null)\n  store.setPicGoCloudLoginError(null)\n\n  // Clear config-sync related UI state after logout.\n  configSyncState.value = {\n    sessionStatus: IPicGoCloudConfigSyncSessionStatus.IDLE,\n    encryptionMethod: undefined\n  }\n  isConflictDialogVisible.value = false\n}\n\nconst pollTimer = ref<number | null>(null)\n\nconst stopPolling = () => {\n  if (pollTimer.value === null) return\n  window.clearInterval(pollTimer.value)\n  pollTimer.value = null\n}\n\nwatch(isConfigSyncRunning, (running) => {\n  if (!running) {\n    stopPolling()\n    return\n  }\n  if (pollTimer.value !== null) return\n  pollTimer.value = window.setInterval(() => {\n    loadConfigSyncState()\n  }, 1500)\n})\n\nonBeforeUnmount(() => {\n  stopPolling()\n})\n\n</script>\n\n<script lang=\"ts\">\nexport default {\n  name: 'PicGoCloudPage'\n}\n</script>\n"
  },
  {
    "path": "src/renderer/pages/PicGoSetting.vue",
    "content": "<template>\n  <div id=\"picgo-setting\">\n    <el-row\n      class=\"view-title\"\n      align=\"middle\"\n      justify=\"center\"\n    >\n      {{ $T('PICGO_SETTINGS') }} -\n      <el-icon\n        class=\"el-icon-document\"\n        @click=\"goConfigPage\"\n      >\n        <Reading />\n      </el-icon>\n    </el-row>\n    <el-row class=\"setting-list\">\n      <el-col\n        :span=\"20\"\n        :offset=\"2\"\n      >\n        <el-row style=\"width: 100%\">\n          <el-form\n            label-position=\"left\"\n            label-width=\"50%\"\n            size=\"small\"\n          >\n            <SelectAreaSettings\n              :settings=\"form\"\n            />\n            <ButtonAreaSettings\n              v-model:proxy=\"proxy\"\n              :settings=\"form\"\n            />\n            <SwitchAreaSettings\n              :settings=\"form\"\n            />\n            <CustomAreaSettings\n              :settings=\"form\"\n            />\n          </el-form>\n        </el-row>\n      </el-col>\n    </el-row>\n  </div>\n</template>\n<script lang=\"ts\" setup>\nimport { ElForm } from 'element-plus'\nimport { Reading } from '@element-plus/icons-vue'\nimport { IConfig } from 'picgo'\nimport { T as $T } from '@/i18n/index'\nimport { enforceNumber, isLinux } from '~/universal/utils/common'\nimport { computed, onBeforeMount, reactive, ref, watch, type DeepReadonly } from 'vue'\nimport ButtonAreaSettings from './components/settings/buttonArea/ButtonAreaSettings.vue'\nimport SwitchAreaSettings from './components/settings/switchArea/SwitchAreaSettings.vue'\nimport CustomAreaSettings from './components/settings/customArea/CustomAreaSettings.vue'\nimport SelectAreaSettings from './components/settings/selectArea/SelectAreaSettings.vue'\nimport { openURL } from '@/utils/common'\nimport { IStartupMode } from '#/types/enum'\nimport { useStore } from '@/hooks/useStore'\n\nconst form = reactive<ISettingForm>({\n  showUpdateTip: false,\n  showPicBedList: [],\n  autoStart: false,\n  rename: false,\n  autoRename: false,\n  uploadNotification: false,\n  notificationSound: true,\n  miniWindowOnTop: false,\n  logLevel: ['all'],\n  autoCopyUrl: true,\n  checkBetaUpdate: true,\n  useBuiltinClipboard: false,\n  language: 'en',\n  logFileSizeLimit: 10,\n  encodeOutputURL: true,\n  showDockIcon: true,\n  showMenubarIcon: true,\n  customLink: '$url',\n  npmProxy: '',\n  npmRegistry: '',\n  server: {\n    port: 36677,\n    host: '127.0.0.1',\n    enable: true\n  },\n  startupMode: IStartupMode.HIDE\n})\n\nconst proxy = ref('')\nconst store = useStore()\nconst appConfig = computed(() => store?.state.appConfig ?? null)\n\nonBeforeMount(() => {\n  store?.refreshAppConfig()\n})\n\nconst applyAppConfig = (config: DeepReadonly<IConfig> | null) => {\n  if (!config) return\n  const settings = config.settings || {}\n  const picBed = config.picBed\n  form.showUpdateTip = settings.showUpdateTip || false\n  form.autoStart = settings.autoStart || false\n  form.rename = settings.rename || false\n  form.autoRename = settings.autoRename || false\n  form.uploadNotification = settings.uploadNotification || false\n  form.notificationSound = settings.notificationSound === undefined ? true : settings.notificationSound\n  form.miniWindowOnTop = settings.miniWindowOnTop || false\n  form.logLevel = initLogLevel(settings.logLevel ? [...settings.logLevel] : [])\n  form.autoCopyUrl = settings.autoCopyUrl === undefined ? true : settings.autoCopyUrl\n  form.checkBetaUpdate = settings.checkBetaUpdate === undefined ? true : settings.checkBetaUpdate\n  form.useBuiltinClipboard = settings.useBuiltinClipboard === undefined ? false : settings.useBuiltinClipboard\n  form.language = settings.language ?? 'en'\n  form.encodeOutputURL = settings.encodeOutputURL === undefined ? false : settings.encodeOutputURL\n  form.customLink = settings.customLink || '$url'\n  form.npmProxy = settings.npmProxy || ''\n  form.npmRegistry = settings.npmRegistry || ''\n  proxy.value = picBed.proxy || ''\n  const server = settings.server ?? {}\n  form.server = {\n    port: enforceNumber(server.port ?? 36677) || 36677,\n    host: server.host || '127.0.0.1',\n    enable: server.enable ?? true\n  }\n  form.logFileSizeLimit = enforceNumber(settings.logFileSizeLimit ?? 10) || 10\n  form.showDockIcon = settings.showDockIcon === undefined ? true : settings.showDockIcon\n  form.showMenubarIcon = settings.showMenubarIcon === undefined ? true : settings.showMenubarIcon\n  form.startupMode = settings.startupMode || (isLinux ? IStartupMode.SHOW_MINI_WINDOW : IStartupMode.HIDE)\n}\n\nwatch(appConfig, (config) => {\n  applyAppConfig(config)\n}, { immediate: true })\n\nfunction initLogLevel (logLevel: string | string[]) {\n  if (!Array.isArray(logLevel)) {\n    if (logLevel && logLevel.length > 0) {\n      logLevel = [logLevel]\n    } else {\n      logLevel = ['all']\n    }\n  }\n  return logLevel\n}\n\nfunction goConfigPage () {\n  openURL('https://docs.picgo.app/gui/guide/config#picgo-setting')\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'SettingPage'\n}\n</script>\n<style lang='stylus'>\n.el-message\n  left 60%\n.view-title\n  .el-icon-document\n    margin-left 8px\n    cursor pointer\n    transition color .2s ease-in-out\n    &:hover\n      color #49B1F5\n#picgo-setting\n  .sub-title\n    font-size 14px\n  .setting-list\n    height 360px\n    box-sizing border-box\n    overflow-y auto\n    overflow-x hidden\n    width 100%\n  .setting-list\n    .el-form\n      width: 100%\n      &-item\n        display: flex\n        justify-content space-between\n        padding-top 8px\n        padding-bottom 8px\n        border-bottom 1px solid darken(#eee, 50%)\n        margin-bottom 0\n        &:last-child\n          border-bottom none\n        &::after\n          display none\n        &::before\n          display none\n        &__content\n          display flex\n          justify-content flex-end\n          flex-basis: 50%\n      .el-form-item__label\n        line-height 32px\n        padding-bottom 0\n        color #eee\n        flex-basis: 50%\n        flex-shrink: 0\n      .el-form-item__custom-label\n        display flex\n        align-items center\n      .el-button-group\n        width 100%\n        .el-button\n          width 50%\n      .el-radio-group\n        margin-left 25px\n      .el-switch__label\n        color #eee\n        &.is-active\n          color #409EFF\n      .el-icon-question\n        margin-left 4px\n        color #eee\n        cursor pointer\n        transition .2s color ease-in-out\n        &:hover\n          color #409EFF\n      .el-checkbox-group\n        label\n          margin-right 30px\n          width 100px\n      .el-checkbox+.el-checkbox\n        margin-right 30px\n        margin-left 0\n      .confirm-button\n        width 100%\n  .server-dialog\n    .notice-text\n      color: #49B1F5\n    .el-dialog__body\n      padding-top: 0\n    .el-form-item\n      margin-bottom: 10px\n</style>\n"
  },
  {
    "path": "src/renderer/pages/Plugin.vue",
    "content": "<template>\n  <div id=\"plugin-view\">\n    <div class=\"view-title\">\n      {{ $T('PLUGIN_SETTINGS') }} -\n      <el-tooltip\n        :content=\"pluginListToolTip\"\n        placement=\"right\"\n      >\n        <el-icon\n          class=\"el-icon-goods\"\n          @click=\"goAwesomeList\"\n        >\n          <Goods />\n        </el-icon>\n      </el-tooltip>\n      <el-tooltip\n        :content=\"importLocalPluginToolTip\"\n        placement=\"left\"\n      >\n        <el-icon\n          class=\"el-icon-download\"\n          @click=\"handleImportLocalPlugin\"\n        >\n          <Download />\n        </el-icon>\n      </el-tooltip>\n    </div>\n    <el-row\n      class=\"handle-bar\"\n      :class=\"{ 'cut-width': pluginList.length > 6 }\"\n    >\n      <el-input\n        v-model=\"searchText\"\n        :placeholder=\"$T('PLUGIN_SEARCH_PLACEHOLDER')\"\n        size=\"small\"\n      >\n        <template #suffix>\n          <el-icon\n            class=\"el-input__icon\"\n            style=\"cursor: pointer;\"\n            @click=\"cleanSearch\"\n          >\n            <close />\n          </el-icon>\n        </template>\n      </el-input>\n    </el-row>\n    <el-row\n      v-loading=\"loading\"\n      :gutter=\"10\"\n      class=\"plugin-list\"\n    >\n      <el-col\n        v-for=\"item in pluginList\"\n        :key=\"item.fullName\"\n        class=\"plugin-item__container\"\n        :span=\"12\"\n      >\n        <div\n          class=\"plugin-item\"\n        >\n          <div\n            v-if=\"!item.gui\"\n            class=\"cli-only-badge\"\n            title=\"CLI only\"\n          >\n            CLI\n          </div>\n          <img\n            class=\"plugin-item__logo\"\n            :src=\"item.logo\"\n            :onerror=\"defaultLogo\"\n          >\n          <div\n            class=\"plugin-item__content\"\n            :class=\"{ disabled: !item.enabled }\"\n          >\n            <div\n              class=\"plugin-item__name\"\n              @click=\"openHomepage(item.homepage)\"\n            >\n              {{ item.name }} <small>{{ ' ' + item.version }}</small>\n            </div>\n            <div\n              class=\"plugin-item__desc\"\n              :title=\"item.description\"\n            >\n              {{ item.description }}\n            </div>\n            <div class=\"plugin-item__info-bar\">\n              <span class=\"plugin-item__author\">\n                {{ item.author }}\n              </span>\n              <span class=\"plugin-item__config\">\n                <template v-if=\"searchText\">\n                  <template v-if=\"!item.hasInstall\">\n                    <span\n                      v-if=\"!item.ing\"\n                      class=\"config-button install\"\n                      @click=\"installPlugin(item)\"\n                    >\n                      {{ $T('PLUGIN_INSTALL') }}\n                    </span>\n                    <span\n                      v-else-if=\"item.ing\"\n                      class=\"config-button ing\"\n                    >\n                      {{ $T('PLUGIN_INSTALLING') }}\n                    </span>\n                  </template>\n                  <span\n                    v-else\n                    class=\"config-button ing\"\n                  >\n                    {{ $T('PLUGIN_INSTALLED') }}\n                  </span>\n                </template>\n                <template v-else>\n                  <span\n                    v-if=\"item.ing\"\n                    class=\"config-button ing\"\n                  >\n                    {{ $T('PLUGIN_DOING_SOMETHING') }}\n                  </span>\n                  <template v-else>\n                    <el-icon\n                      v-if=\"item.enabled\"\n                      class=\"el-icon-setting\"\n                      @click=\"buildContextMenu(item)\"\n                    >\n                      <Setting />\n                    </el-icon>\n                    <el-icon\n                      v-else\n                      class=\"el-icon-remove-outline\"\n                      @click=\"buildContextMenu(item)\"\n                    >\n                      <Remove />\n                    </el-icon>\n                  </template>\n                </template>\n              </span>\n            </div>\n          </div>\n        </div>\n      </el-col>\n    </el-row>\n    <el-row\n      v-show=\"needReload\"\n      class=\"reload-mask\"\n      :class=\"{ 'cut-width': pluginList.length > 6 }\"\n      justify=\"center\"\n    >\n      <el-button\n        type=\"primary\"\n        size=\"small\"\n        round\n        @click=\"reloadApp\"\n      >\n        {{ $T('TIPS_NEED_RELOAD') }}\n      </el-button>\n    </el-row>\n    <el-dialog\n      v-model=\"dialogVisible\"\n      :modal-append-to-body=\"false\"\n      :title=\"$T('CONFIG_THING', {\n        c: configName\n      })\"\n      width=\"70%\"\n    >\n      <config-form\n        :id=\"configName\"\n        :key=\"configFormKey\"\n        ref=\"$configForm\"\n        :config=\"config\"\n        :type=\"currentType\"\n        color-mode=\"white\"\n      />\n      <template #footer>\n        <el-button\n          round\n          @click=\"handleCancelConfig\"\n        >\n          {{ $T('CANCEL') }}\n        </el-button>\n        <el-button\n          type=\"primary\"\n          round\n          @click=\"handleConfirmConfig\"\n        >\n          {{ $T('CONFIRM') }}\n        </el-button>\n      </template>\n    </el-dialog>\n  </div>\n</template>\n<script lang=\"ts\" setup>\nimport { Close, Download, Goods, Remove, Setting } from '@element-plus/icons-vue'\nimport { T as $T } from '@/i18n/index'\nimport ConfigForm from '@/components/ConfigForm.vue'\nimport { debounce, DebouncedFunc } from 'lodash'\nimport type { IpcRendererEvent } from 'electron'\nimport { handleStreamlinePluginName } from '~/universal/utils/common'\nimport {\n  OPEN_URL,\n  PICGO_CONFIG_PLUGIN,\n  PICGO_HANDLE_PLUGIN_ING,\n  PICGO_TOGGLE_PLUGIN,\n  SHOW_PLUGIN_PAGE_MENU,\n  GET_PICBEDS,\n  PICGO_HANDLE_PLUGIN_DONE\n} from '#/events/constants'\nimport { computed, ref, onBeforeMount, watch } from 'vue'\nimport { getConfig, saveConfig, sendRPC, sendToMain } from '@/utils/dataSender'\nimport { showNotification } from '@/utils/notification'\nimport { ElMessageBox } from 'element-plus'\nimport axios from 'axios'\nimport { getRendererStaticFileUrl } from '@/utils/static'\nimport { IRPCActionType } from '~/universal/types/enum'\nimport { useIPCOn } from '@/hooks/useIPC'\nconst $confirm = ElMessageBox.confirm\nconst searchText = ref('')\nconst pluginList = ref<IPicGoPlugin[]>([])\nconst config = ref<IPicGoPluginConfig[]>([])\nconst currentType = ref<'plugin' | 'uploader' | 'transformer'>('plugin')\nconst configName = ref('')\nconst dialogVisible = ref(false)\nconst pluginNameList = ref<string[]>([])\nconst loading = ref(true)\nconst needReload = ref(false)\nconst configFormKey = computed(() => `${currentType.value}:${configName.value}`)\nconst pluginListToolTip = $T('PLUGIN_LIST')\nconst importLocalPluginToolTip = $T('PLUGIN_IMPORT_LOCAL')\n// const id = ref('')\nconst defaultLogo = ref(`this.src=\"${getRendererStaticFileUrl('roundLogo.png')}\"`)\nconst $configForm = ref<InstanceType<typeof ConfigForm> | null>(null)\nconst npmSearchText = computed(() => {\n  return searchText.value.match('picgo-plugin-')\n    ? searchText.value\n    : searchText.value !== ''\n      ? `picgo-plugin-${searchText.value}`\n      : searchText.value\n})\nlet getSearchResult: DebouncedFunc<(val: string) => void>\n\nwatch(npmSearchText, (val: string) => {\n  if (val) {\n    loading.value = true\n    pluginList.value = []\n    getSearchResult(val)\n  } else {\n    getPluginList()\n  }\n})\n\nwatch(dialogVisible, (val: boolean) => {\n  if (val) {\n    // @ts-ignore\n    document.querySelector('.main-content.el-row').style.zIndex = 101\n  } else {\n    // @ts-ignore\n    document.querySelector('.main-content.el-row').style.zIndex = 10\n  }\n})\n\nuseIPCOn('hideLoading', () => {\n  loading.value = false\n})\n\nuseIPCOn(PICGO_HANDLE_PLUGIN_DONE, (evt: IpcRendererEvent, fullName: string) => {\n  pluginList.value.forEach(item => {\n    if (item.fullName === fullName || (item.name === fullName)) {\n      item.ing = false\n    }\n  })\n  loading.value = false\n})\n\nuseIPCOn('pluginList', (evt: IpcRendererEvent, list: IPicGoPlugin[]) => {\n  pluginList.value = list\n  pluginNameList.value = list.map(item => item.fullName)\n  loading.value = false\n})\n\nuseIPCOn('installPlugin', (evt: IpcRendererEvent, { success, body }: {\n  success: boolean,\n  body: string\n}) => {\n  loading.value = false\n  pluginList.value.forEach(item => {\n    if (item.fullName === body) {\n      item.ing = false\n      item.hasInstall = success\n    }\n  })\n})\n\nuseIPCOn('updateSuccess', (evt: IpcRendererEvent, plugin: string) => {\n  loading.value = false\n  pluginList.value.forEach(item => {\n    if (item.fullName === plugin) {\n      item.ing = false\n      item.hasInstall = true\n    }\n    getPicBeds()\n  })\n  handleReload()\n  getPluginList()\n})\n\nuseIPCOn('uninstallSuccess', (evt: IpcRendererEvent, plugin: string) => {\n  loading.value = false\n  pluginList.value = pluginList.value.filter(item => {\n    if (item.fullName === plugin) { // restore Uploader & Transformer after uninstalling\n      if (item.config.transformer.name) {\n        handleRestoreState('transformer', item.config.transformer.name)\n      }\n      if (item.config.uploader.name) {\n        handleRestoreState('uploader', item.config.uploader.name)\n      }\n      getPicBeds()\n    }\n    return item.fullName !== plugin\n  })\n  pluginNameList.value = pluginNameList.value.filter(item => item !== plugin)\n})\n\nuseIPCOn(PICGO_CONFIG_PLUGIN, (evt: IpcRendererEvent, _currentType: 'plugin' | 'transformer' | 'uploader', _configName: string, _config: IPicGoPluginConfig[]) => {\n  currentType.value = _currentType\n  configName.value = _configName\n  config.value = _config\n  dialogVisible.value = true\n})\n\nuseIPCOn(PICGO_HANDLE_PLUGIN_ING, (evt: IpcRendererEvent, fullName: string) => {\n  pluginList.value.forEach(item => {\n    if (item.fullName === fullName || (item.name === fullName)) {\n      item.ing = true\n    }\n  })\n  loading.value = true\n})\n\nuseIPCOn(PICGO_TOGGLE_PLUGIN, (evt: IpcRendererEvent, fullName: string, enabled: boolean) => {\n  const plugin = pluginList.value.find(item => item.fullName === fullName)\n  if (plugin) {\n    plugin.enabled = enabled\n    getPicBeds()\n    needReload.value = true\n  }\n})\n\nonBeforeMount(async () => {\n  getPluginList()\n  getSearchResult = debounce(_getSearchResult, 50)\n  needReload.value = await getConfig<boolean>('needReload') || false\n})\n\nasync function buildContextMenu (plugin: IPicGoPlugin) {\n  sendToMain(SHOW_PLUGIN_PAGE_MENU, plugin)\n}\n\nfunction getPluginList () {\n  sendToMain('getPluginList')\n}\n\nfunction getPicBeds () {\n  sendToMain(GET_PICBEDS)\n}\n\nfunction installPlugin (item: IPicGoPlugin) {\n  if (!item.gui) {\n    $confirm($T('TIPS_PLUGIN_NOT_GUI_IMPLEMENT'), $T('TIPS_NOTICE'), {\n      confirmButtonText: $T('CONFIRM'),\n      cancelButtonText: $T('CANCEL'),\n      type: 'warning'\n    }).then(() => {\n      item.ing = true\n      sendToMain('installPlugin', item.fullName)\n    }).catch(() => {\n      console.log('Install canceled')\n    })\n  } else {\n    item.ing = true\n    sendToMain('installPlugin', item.fullName)\n  }\n}\n\n// function uninstallPlugin (val: string) {\n//   pluginList.value.forEach(item => {\n//     if (item.name === val) {\n//       item.ing = true\n//     }\n//   })\n//   loading.value = true\n//   sendToMain('uninstallPlugin', val)\n// }\n\n// function updatePlugin (val: string) {\n//   pluginList.value.forEach(item => {\n//     if (item.fullName === val) {\n//       item.ing = true\n//     }\n//   })\n//   loading.value = true\n//   sendToMain('updatePlugin', val)\n// }\n\nfunction reloadApp () {\n  sendRPC(IRPCActionType.RELOAD_APP)\n}\n\nasync function handleReload () {\n  saveConfig({\n    needReload: true\n  })\n  needReload.value = true\n  showNotification({\n    title: $T('PLUGIN_UPDATE_SUCCEED'),\n    body: $T('TIPS_NEED_RELOAD'),\n    callback: reloadApp\n  })\n}\n\nfunction cleanSearch () {\n  searchText.value = ''\n}\n\nasync function handleConfirmConfig () {\n  const result = (await $configForm.value?.validate() || false)\n  if (result !== false) {\n    switch (currentType.value) {\n      case 'plugin':\n        saveConfig({\n          [`${configName.value}`]: result\n        })\n        break\n      case 'uploader':\n        saveConfig({\n          [`picBed.${configName.value}`]: result\n        })\n        break\n      case 'transformer':\n        saveConfig({\n          [`transformer.${configName.value}`]: result\n        })\n        break\n    }\n    showNotification({\n      title: $T('SETTINGS_RESULT'),\n      body: $T('TIPS_SET_SUCCEED')\n    })\n    dialogVisible.value = false\n    getPluginList()\n  }\n}\n\nfunction handleCancelConfig () {\n  dialogVisible.value = false\n  $configForm.value?.resetConfig()\n}\n\nfunction _getSearchResult (val: string) {\n  // this.$http.get(`https://api.npms.io/v2/search?q=${val}`)\n  axios.get(`https://registry.npmjs.com/-/v1/search?text=${val}`)\n    .then((res: INPMSearchResult) => {\n      pluginList.value = res.data.objects\n        .filter((item:INPMSearchResultObject) => {\n          return item.package.name.includes('picgo-plugin-')\n        })\n        .filter((item: INPMSearchResultObject) => {\n          // filter out fake picgo plugins from picgo.net\n          if (item.package.description?.includes('picgo.net') || item.package.description?.includes('PicGo官方')) {\n            return false\n          }\n          return true\n        })\n        .map((item: INPMSearchResultObject) => {\n          return handleSearchResult(item)\n        })\n      loading.value = false\n    })\n    .catch(err => {\n      console.log(err)\n      loading.value = false\n    })\n}\n\nfunction handleSearchResult (item: INPMSearchResultObject) {\n  const name = handleStreamlinePluginName(item.package.name)\n  let gui = false\n  if (item.package.keywords && item.package.keywords.length > 0) {\n    if (item.package.keywords.includes('picgo-gui-plugin')) {\n      gui = true\n    }\n  }\n  return {\n    name,\n    fullName: item.package.name,\n    author: item.package.maintainers[0]?.username || '',\n    description: item.package.description,\n    logo: `https://cdn.jsdelivr.net/npm/${item.package.name}/logo.png`,\n    config: {},\n    homepage: item.package.links ? item.package.links.homepage : '',\n    hasInstall: pluginNameList.value.some(plugin => plugin === item.package.name),\n    version: item.package.version,\n    gui,\n    ing: false // installing or uninstalling\n  }\n}\n\n// restore Uploader & Transformer\nasync function handleRestoreState (item: string, name: string) {\n  if (item === 'uploader') {\n    const current = await getConfig('picBed.current')\n    if (current === name) {\n      saveConfig({\n        'picBed.current': 'smms',\n        'picBed.uploader': 'smms'\n      })\n    }\n  }\n  if (item === 'transformer') {\n    const current = await getConfig('picBed.transformer')\n    if (current === name) {\n      saveConfig({\n        'picBed.transformer': 'path'\n      })\n    }\n  }\n}\n\nfunction openHomepage (url: string) {\n  if (url) {\n    sendToMain(OPEN_URL, url)\n  }\n}\n\nfunction goAwesomeList () {\n  sendToMain(OPEN_URL, 'https://github.com/PicGo/Awesome-PicGo')\n}\n\nfunction handleImportLocalPlugin () {\n  sendToMain('importLocalPlugin')\n  loading.value = true\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'PluginPage'\n}\n</script>\n<style lang='stylus'>\n$bg = #172426\n#plugin-view\n  position relative\n  padding 0 20px 0\n  .el-loading-mask\n    background-color rgba(0, 0, 0, 0.8)\n  .plugin-list\n    align-content flex-start\n    height: 339px;\n    box-sizing: border-box;\n    padding: 8px 15px;\n    overflow-y: auto;\n    overflow-x: hidden;\n    position: absolute;\n    top: 70px;\n    left: 5px;\n    transition: all 0.2s ease-in-out 0.1s;\n    width: 100%\n    .el-loading-mask\n      left: 20px\n      width: calc(100% - 40px)\n  .view-title\n    color #eee\n    font-size 20px\n    text-align center\n    margin 10px auto\n    position relative\n    i.el-icon-goods\n      margin-left 4px\n      font-size 20px\n      vertical-align middle\n      cursor pointer\n      transition color .2s ease-in-out\n      &:hover\n        color #49B1F5\n    i.el-icon-download\n      position absolute\n      right 0\n      top 8px\n      font-size 20px\n      vertical-align middle\n      cursor pointer\n      transition color .2s ease-in-out\n      &:hover\n        color #49B1F5\n  .handle-bar\n    margin-bottom 20px\n    &.cut-width\n      padding-right: 8px\n  .el-input__inner\n    border-radius 0\n  .plugin-item\n    box-sizing border-box\n    height 80px\n    background #444\n    padding 8px\n    user-select text\n    transition all .2s ease-in-out\n    position relative\n    &__container\n      height 80px\n      margin-bottom 10px\n    .cli-only-badge\n      position absolute\n      right 0px\n      top 0\n      font-size 12px\n      padding 3px 8px\n      background #49B1F5\n      color #eee\n    background transparentify($bg, #000, 0.75)\n    &:hover\n      background transparentify($bg, #000, 0.85)\n    &__logo\n      width 64px\n      height 64px\n      float left\n    &__content\n      float left\n      width calc(100% - 72px)\n      height 64px\n      color #ddd\n      margin-left 8px\n      display flex\n      flex-direction column\n      justify-content space-between\n      &.disabled\n        color #aaa\n    &__name\n      font-size 16px\n      height 22px\n      line-height 22px\n      // font-weight 600\n      font-weight 600\n      cursor pointer\n      transition all .2s ease-in-out\n      &:hover\n        color: #1B9EF3\n    &__desc\n      font-size 14px\n      height 21px\n      line-height 21px\n      overflow hidden\n      text-overflow ellipsis\n      white-space nowrap\n    &__info-bar\n      font-size 14px\n      height 21px\n      line-height 28px\n      position relative\n    &__author\n      overflow hidden\n      text-overflow ellipsis\n      white-space nowrap\n    &__config\n      float right\n      font-size 16px\n      cursor pointer\n      transition all .2s ease-in-out\n      &:hover\n        color: #1B9EF3\n    .config-button\n      font-size 12px\n      color #ddd\n      background #222\n      padding 1px 8px\n      height 18px\n      line-height 18px\n      text-align center\n      position absolute\n      top 4px\n      right 20px\n      transition all .2s ease-in-out\n      &.reload\n        right 0px\n      &.ing\n        right 0px\n      &.install\n        right 0px\n        &:hover\n          background: #1B9EF3\n          color #fff\n  .reload-mask\n    position absolute\n    width calc(100% - 40px)\n    bottom -320px\n    text-align center\n    background rgba(0,0,0,0.4)\n    padding 10px 0\n    &.cut-width\n      width calc(100% - 48px)\n</style>\n"
  },
  {
    "path": "src/renderer/pages/RenamePage.vue",
    "content": "<template>\n  <div class=\"p-[20px] flex flex-col justify-between\">\n    <el-form\n      ref=\"formRef\"\n      :model=\"form\"\n      @submit.prevent\n    >\n      <el-form-item\n        :label=\"$T('FILE_RENAME')\"\n        prop=\"fileName\"\n        :rules=\"[\n          { required: true, message: 'file name is required', trigger: 'blur' }\n        ]\"\n      >\n        <el-input\n          v-model=\"form.fileName\"\n          size=\"small\"\n          autofocus\n          @keyup.enter=\"confirmName\"\n        >\n          <template #suffix>\n            <el-icon\n              class=\"el-input__icon\"\n              style=\"cursor: pointer;\"\n              @click=\"form.fileName = ''\"\n            >\n              <close />\n            </el-icon>\n          </template>\n        </el-input>\n      </el-form-item>\n    </el-form>\n    <el-row>\n      <div class=\"w-full flex justify-end mt-[20px]\">\n        <el-button\n          round\n          size=\"small\"\n          @click=\"cancel\"\n        >\n          {{ $T('CANCEL') }}\n        </el-button>\n        <el-button\n          type=\"primary\"\n          round\n          size=\"small\"\n          @click=\"confirmName\"\n        >\n          {{ $T('CONFIRM') }}\n        </el-button>\n      </div>\n    </el-row>\n  </div>\n</template>\n<script lang=\"ts\" setup>\nimport { Close } from '@element-plus/icons-vue'\nimport { GET_RENAME_FILE_NAME, RENAME_FILE_NAME } from '#/events/constants'\nimport { sendToMain } from '@/utils/dataSender'\nimport { T as $T } from '@/i18n/index'\nimport {\n  ipcRenderer,\n  IpcRendererEvent\n} from 'electron'\nimport { onBeforeUnmount, onBeforeMount, ref, reactive } from 'vue'\nimport { useIPCOn } from '@/hooks/useIPC'\nimport { FormInstance } from 'element-plus'\nconst id = ref<string | null>(null)\nconst formRef = ref<FormInstance>()\n\nconst form = reactive({\n  fileName: '',\n  originName: ''\n})\n\nconst handleFileName = (event: IpcRendererEvent, newName: string, _originName: string, _id: string) => {\n  form.fileName = newName\n  form.originName = _originName\n  id.value = _id\n}\n\nuseIPCOn(RENAME_FILE_NAME, handleFileName)\n\nonBeforeMount(() => {\n  ipcRenderer.send(GET_RENAME_FILE_NAME)\n})\n\nfunction confirmName () {\n  formRef.value?.validate((valid) => {\n    if (valid) {\n      sendToMain(`${RENAME_FILE_NAME}${id.value}`, form.fileName)\n    }\n  })\n}\n\nfunction cancel () {\n  // if cancel, use origin file name\n  sendToMain(`${RENAME_FILE_NAME}${id.value}`, form.originName)\n}\n\nonBeforeUnmount(() => {\n  ipcRenderer.removeAllListeners(RENAME_FILE_NAME)\n})\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'RenamePage'\n}\n</script>\n<style lang='stylus'>\n  .el-form-item__label\n    color #ddd\n</style>\n"
  },
  {
    "path": "src/renderer/pages/ShortKey.vue",
    "content": "<template>\n  <div id=\"shortcut-page\">\n    <div class=\"view-title\">\n      {{ $T('SETTINGS_SET_SHORTCUT') }}\n    </div>\n    <el-row>\n      <el-col\n        :span=\"20\"\n        :offset=\"2\"\n      >\n        <el-table\n          class=\"shortcut-page-table-border\"\n          :data=\"list\"\n          size=\"small\"\n          header-cell-class-name=\"shortcut-page-table-border\"\n          cell-class-name=\"shortcut-page-table-border\"\n        >\n          <el-table-column\n            :label=\"$T('SHORTCUT_NAME')\"\n          >\n            <template #default=\"scope\">\n              {{ scope.row.label ? scope.row.label : scope.row.name }}\n            </template>\n          </el-table-column>\n          <el-table-column\n            width=\"160px\"\n            :label=\"$T('SHORTCUT_BIND')\"\n            prop=\"key\"\n          />\n          <el-table-column\n            :label=\"$T('SHORTCUT_STATUS')\"\n          >\n            <template #default=\"scope\">\n              <el-tag\n                size=\"small\"\n                :type=\"scope.row.enable ? 'success' : 'danger'\"\n              >\n                {{ scope.row.enable ? $T('SHORTCUT_ENABLED') : $T('SHORTCUT_DISABLED') }}\n              </el-tag>\n            </template>\n          </el-table-column>\n          <el-table-column\n            :label=\"$T('SHORTCUT_SOURCE')\"\n            width=\"100px\"\n          >\n            <template #default=\"scope\">\n              {{ calcOriginShowName(scope.row.from) }}\n            </template>\n          </el-table-column>\n          <el-table-column\n            :label=\"$T('SHORTCUT_HANDLE')\"\n            width=\"100px\"\n          >\n            <template #default=\"scope\">\n              <el-row>\n                <el-button\n                  size=\"small\"\n                  :class=\"{\n                    disabled: scope.row.enable\n                  }\"\n                  type=\"text\"\n                  @click=\"toggleEnable(scope.row)\"\n                >\n                  {{ scope.row.enable ? $T('SHORTCUT_DISABLE') : $T('SHORTCUT_ENABLE') }}\n                </el-button>\n                <el-button\n                  class=\"edit\"\n                  size=\"small\"\n                  type=\"text\"\n                  @click=\"openKeyBindingDialog(scope.row, scope.$index)\"\n                >\n                  {{ $T('SHORTCUT_EDIT') }}\n                </el-button>\n              </el-row>\n            </template>\n          </el-table-column>\n        </el-table>\n      </el-col>\n    </el-row>\n    <el-dialog\n      v-model=\"keyBindingVisible\"\n      :title=\"$T('SHORTCUT_CHANGE_UPLOAD')\"\n      :modal-append-to-body=\"false\"\n    >\n      <el-form\n        label-position=\"top\"\n        label-width=\"80px\"\n      >\n        <el-form-item>\n          <el-input\n            v-model=\"shortKey\"\n            class=\"align-center\"\n            :autofocus=\"true\"\n            @keydown.prevent=\"keyDetect($event as KeyboardEvent)\"\n          />\n        </el-form-item>\n      </el-form>\n      <template #footer>\n        <el-button\n          round\n          @click=\"cancelKeyBinding\"\n        >\n          {{ $T('CANCEL') }}\n        </el-button>\n        <el-button\n          type=\"primary\"\n          round\n          @click=\"confirmKeyBinding\"\n        >\n          {{ $T('CONFIRM') }}\n        </el-button>\n      </template>\n    </el-dialog>\n  </div>\n</template>\n<script lang=\"ts\" setup>\nimport keyBinding from '@/utils/key-binding'\nimport { ipcRenderer, IpcRendererEvent } from 'electron'\nimport { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'\nimport { onBeforeUnmount, onBeforeMount, ref, watch } from 'vue'\nimport { getConfig, sendToMain } from '@/utils/dataSender'\nimport { T as $T } from '@/i18n/index'\n\nconst list = ref<IShortKeyConfig[]>([])\nconst keyBindingVisible = ref(false)\nconst command = ref('')\nconst shortKey = ref('')\nconst currentIndex = ref(0)\n\nonBeforeMount(async () => {\n  const shortKeyConfig = (await getConfig<IShortKeyConfigs>('settings.shortKey'))!\n  list.value = Object.keys(shortKeyConfig).map(item => {\n    return {\n      ...shortKeyConfig[item],\n      from: calcOrigin(item)\n    }\n  })\n})\n\nwatch(keyBindingVisible, (val: boolean) => {\n  sendToMain(TOGGLE_SHORTKEY_MODIFIED_MODE, val)\n})\n\nfunction calcOrigin (item: string) {\n  const [origin] = item.split(':')\n  return origin\n}\n\nfunction calcOriginShowName (item: string) {\n  return item.replace('picgo-plugin-', '')\n}\n\nfunction toggleEnable (item: IShortKeyConfig) {\n  const status = !item.enable\n  item.enable = status\n  sendToMain('bindOrUnbindShortKey', item, item.from)\n}\n\nfunction keyDetect (event: KeyboardEvent) {\n  shortKey.value = keyBinding(event).join('+')\n}\n\nasync function openKeyBindingDialog (config: IShortKeyConfig, index: number) {\n  command.value = `${config.from}:${config.name}`\n  shortKey.value = await getConfig(`settings.shortKey.${command.value}.key`) || ''\n  currentIndex.value = index\n  keyBindingVisible.value = true\n}\n\nasync function cancelKeyBinding () {\n  keyBindingVisible.value = false\n  shortKey.value = await getConfig<string>(`settings.shortKey.${command.value}.key`) || ''\n}\n\nasync function confirmKeyBinding () {\n  const oldKey = await getConfig<string>(`settings.shortKey.${command.value}.key`)\n  const config = Object.assign({}, list.value[currentIndex.value])\n  config.key = shortKey.value\n  sendToMain('updateShortKey', config, oldKey, config.from)\n  ipcRenderer.once('updateShortKeyResponse', (evt: IpcRendererEvent, result) => {\n    if (result) {\n      keyBindingVisible.value = false\n      list.value[currentIndex.value].key = shortKey.value\n    }\n  })\n}\n\nonBeforeUnmount(() => {\n  sendToMain(TOGGLE_SHORTKEY_MODIFIED_MODE, false)\n})\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'ShortkeyPage'\n}\n</script>\n<style lang='stylus'>\n#shortcut-page\n  .shortcut-page-table-border\n    border-color darken(#eee, 50%)\n  .el-dialog__body\n    padding 10px 20px\n    .el-form-item\n      margin-bottom 0\n  .el-button\n    &.disabled\n      color: #F56C6C\n    &.edit\n      color: #67C23A\n    &--text\n      padding-left 4px\n      padding-right 4px\n  .el-table\n    background-color: transparent\n    color #ddd\n    &::before\n      background-color darken(#eee, 50%)\n    thead\n      color #bbb\n    th,tr\n      background-color: transparent\n    &__body\n      tr.el-table__row--striped\n        td\n          background transparent\n    &--enable-row-hover\n      .el-table__body\n        tr:hover\n          &>td\n            background #333\n  .el-button+.el-button\n    margin-left 4px\n</style>\n"
  },
  {
    "path": "src/renderer/pages/Toolbox.vue",
    "content": "<template>\n  <div class=\"toolbox\">\n    <el-row>\n      <el-row\n        class=\"toolbox-header\"\n      >\n        <el-row>\n          <img\n            class=\"toolbox-header__logo\"\n            :src=\"defaultLogo\"\n          >\n          <el-row class=\"toolbox-header__text\">\n            <el-row class=\"toolbox-header__title\">\n              {{ $T('TOOLBOX_TITLE') }}\n            </el-row>\n            <el-row class=\"toolbox-header__sub-title\">\n              {{ $T('TOOLBOX_SUB_TITLE') }}\n            </el-row>\n          </el-row>\n        </el-row>\n        <el-row>\n          <template v-if=\"progress !== 100\">\n            <el-button\n              type=\"primary\"\n              round\n              :disabled=\"isLoading\"\n              @click=\"handleCheck\"\n            >\n              {{ $T('TOOLBOX_START_SCAN') }}\n            </el-button>\n          </template>\n          <template v-else-if=\"isAllSuccess\">\n            <div class=\"toolbox-tips\">\n              {{ $T('TOOLBOX_SUCCESS_TIPS') }}\n            </div>\n          </template>\n          <template v-else-if=\"!isAllSuccess\">\n            <template v-if=\"canFixLength !== 0\">\n              <el-button\n                type=\"primary\"\n                round\n                @click=\"handleFix\"\n              >\n                {{ $T('TOOLBOX_START_FIX') }}\n              </el-button>\n            </template>\n            <template v-else>\n              <div class=\"toolbox-cant-fix toolbox-tips\">\n                {{ $T('TOOLBOX_CANT_AUTO_FIX') }}\n                <el-button\n                  type=\"primary\"\n                  round\n                  class=\"toolbox-cant-fix__btn\"\n                  @click=\"handleCheck\"\n                >\n                  {{ $T('TOOLBOX_RE_SCAN') }}\n                </el-button>\n              </div>\n            </template>\n          </template>\n        </el-row>\n      </el-row>\n    </el-row>\n    <el-row class=\"progress\">\n      <el-progress\n        :percentage=\"progress\"\n        :format=\"format\"\n      />\n    </el-row>\n    <el-collapse\n      v-model=\"activeTypes\"\n      accordion\n    >\n      <el-collapse-item\n        v-for=\"(item, key) in fixList\"\n        :key=\"key\"\n        :name=\"key\"\n      >\n        <template #title>\n          {{ item.title }} <toolbox-status-icon :status=\"item.status\" />\n        </template>\n        <div class=\"toolbox-item-msg\">\n          {{ item.msg || '' }}\n          <template v-if=\"item.handler && item.handlerText && item.value\">\n            <toolbox-handler\n              :value=\"item.value\"\n              :status=\"item.status\"\n              :handler=\"item.handler\"\n              :handler-text=\"item.handlerText\"\n            />\n          </template>\n        </div>\n      </el-collapse-item>\n    </el-collapse>\n  </div>\n</template>\n<script lang=\"ts\" setup>\nimport { useIPC } from '@/hooks/useIPC'\nimport { sendRPC, invokeRPC } from '@/utils/dataSender'\nimport { ElMessage, ElMessageBox } from 'element-plus'\nimport { computed, reactive, ref } from 'vue'\nimport { IToolboxItemType, IToolboxItemCheckStatus, IRPCActionType } from '~/universal/types/enum'\nimport { T as $T } from '@/i18n'\nimport ToolboxStatusIcon from '@/components/ToolboxStatusIcon.vue'\nimport ToolboxHandler from '@/components/ToolboxHandler.vue'\nimport { getRendererStaticFileUrl } from '@/utils/static'\n\nconst $confirm = ElMessageBox.confirm\nconst defaultLogo = ref(getRendererStaticFileUrl('roundLogo.png'))\nconst activeTypes = ref<IToolboxItemType[]>([])\nconst fixList = reactive<IToolboxMap>({\n  [IToolboxItemType.IS_CONFIG_FILE_BROKEN]: {\n    title: $T('TOOLBOX_CHECK_CONFIG_FILE_BROKEN'),\n    status: IToolboxItemCheckStatus.INIT,\n    handlerText: $T('SETTINGS_OPEN_CONFIG_FILE'),\n    handler (value: string) {\n      sendRPC(IRPCActionType.OPEN_FILE, value)\n    }\n  },\n  [IToolboxItemType.IS_GALLERY_FILE_BROKEN]: {\n    title: $T('TOOLBOX_CHECK_GALLERY_FILE_BROKEN'),\n    status: IToolboxItemCheckStatus.INIT\n  },\n  [IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: {\n    title: $T('TOOLBOX_CHECK_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD'), // picgo-image-clipboard folder\n    status: IToolboxItemCheckStatus.INIT,\n    handlerText: $T('OPEN_FILE_PATH'),\n    handler (value: string) {\n      sendRPC(IRPCActionType.OPEN_FILE, value)\n    }\n  },\n  [IToolboxItemType.HAS_PROBLEM_WITH_PROXY]: {\n    title: $T('TOOLBOX_CHECK_PROBLEM_WITH_PROXY'),\n    status: IToolboxItemCheckStatus.INIT,\n    hasNoFixMethod: true\n  }\n})\n\nconst progress = computed(() => {\n  const total = Object.keys(fixList).length\n  const done = Object.keys(fixList).filter(key => {\n    const status = fixList[key as IToolboxItemType].status\n    return status !== IToolboxItemCheckStatus.INIT && status !== IToolboxItemCheckStatus.LOADING\n  }).length\n  return done / total * 100\n})\n\nconst isAllSuccess = computed(() => {\n  return Object.keys(fixList).every(key => {\n    const status = fixList[key as IToolboxItemType].status\n    return status === IToolboxItemCheckStatus.SUCCESS\n  })\n})\n\nconst isLoading = computed(() => {\n  return Object.keys(fixList).some(key => {\n    const status = fixList[key as IToolboxItemType].status\n    return status === IToolboxItemCheckStatus.LOADING\n  })\n})\n\nconst canFixLength = computed(() => {\n  return Object.keys(fixList).filter(key => {\n    const status = fixList[key as IToolboxItemType].status\n    return status === IToolboxItemCheckStatus.ERROR && !fixList[key as IToolboxItemType].hasNoFixMethod\n  }).length\n})\n\nconst format = (percentage: number) => ''\n\nconst ipc = useIPC()\n\nipc.on(IRPCActionType.TOOLBOX_CHECK_RES, (event, { type, msg = '', status, value = '' }: IToolboxCheckRes) => {\n  fixList[type].status = status\n  fixList[type].msg = msg\n  fixList[type].value = value\n  if (status === IToolboxItemCheckStatus.ERROR) {\n    activeTypes.value.push(type)\n  }\n})\n\nconst handleCheck = () => {\n  activeTypes.value = []\n  Object.keys(fixList).forEach(key => {\n    fixList[key as IToolboxItemType].status = IToolboxItemCheckStatus.LOADING\n    fixList[key as IToolboxItemType].msg = ''\n    fixList[key as IToolboxItemType].value = ''\n  })\n  sendRPC(IRPCActionType.TOOLBOX_CHECK)\n}\n\nconst handleFix = async () => {\n  const fixRes = await Promise.all(Object.keys(fixList).filter(key => {\n    const status = fixList[key as IToolboxItemType].status\n    return status === IToolboxItemCheckStatus.ERROR && !fixList[key as IToolboxItemType].hasNoFixMethod\n  }).map(async key => {\n    const res = await invokeRPC<IToolboxCheckRes>(IRPCActionType.TOOLBOX_CHECK_FIX, key as IToolboxItemType)\n    if (!res.success) {\n      ElMessage.warning(res.error)\n      return null\n    }\n    return res.data\n  }))\n\n  fixRes.filter(item => item !== null).forEach(item => {\n    if (item) {\n      fixList[item.type].status = item.status\n      fixList[item.type].msg = item.msg\n      fixList[item.type].value = item.value\n    }\n  })\n\n  $confirm($T('TOOLBOX_FIX_DONE_NEED_RELOAD'), $T('TIPS_NOTICE'), {\n    confirmButtonText: $T('CONFIRM'),\n    cancelButtonText: $T('CANCEL'),\n    type: 'info'\n  }).then(() => {\n    sendRPC(IRPCActionType.RELOAD_APP)\n  }).catch(() => {})\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'ToolBoxPage'\n}\n</script>\n<style lang='stylus'>\n.toolbox\n  padding 0 40px\n  &-header\n    width 100%\n    color #eee\n    justify-content space-between\n    align-items center\n    padding 20px 0px\n    &__logo\n      width 64px\n      height 64px\n      margin-right 20px\n    &__text\n      flex-direction column\n      justify-content center\n    &__title\n      color #ddd\n      font-size 20px\n      margin-bottom 4px\n    &__sub-title\n      color #aaa\n      font-size 16px\n  .progress\n    width 100%\n    .el-progress--line\n      width 100%\n    .el-progress__text\n      min-width 0\n  .el-collapse\n    margin-top 20px\n    --el-collapse-border-color: #777;\n    --el-collapse-header-height: 48px;\n    --el-collapse-header-bg-color: transparent;\n    --el-collapse-header-text-color: #ddd;\n    --el-collapse-header-font-size: 13px;\n    --el-collapse-content-bg-color: transparent;\n    --el-collapse-content-font-size: 13px;\n    --el-collapse-content-text-color: #ddd;\n    &-item__content\n      padding-bottom: 12px\n  &-item-msg\n    color: #aaa\n  &-tips\n    padding: 12px 0\n  &-cant-fix\n    display flex\n    justify-content center\n    align-items center\n    &__btn\n      margin-left: 8px\n</style>\n"
  },
  {
    "path": "src/renderer/pages/TrayPage.vue",
    "content": "<template>\n  <div id=\"tray-page\">\n    <div\n      class=\"open-main-window\"\n      @click=\"openSettingWindow\"\n    >\n      {{ $T('OPEN_MAIN_WINDOW') }}\n    </div>\n    <div class=\"content\">\n      <div\n        v-if=\"clipboardFiles.length > 0\"\n        class=\"wait-upload-img\"\n      >\n        <div class=\"list-title\">\n          {{ $T('WAIT_TO_UPLOAD') }}\n        </div>\n        <div\n          v-for=\"(item, index) in clipboardFiles\"\n          :key=\"index\"\n          class=\"img-list\"\n        >\n          <div\n            class=\"upload-img__container\"\n            :class=\"{ upload: uploadFlag }\"\n            @click=\"uploadClipboardFiles\"\n          >\n            <img\n              :src=\"item.imgUrl\"\n              class=\"upload-img\"\n            >\n          </div>\n        </div>\n      </div>\n      <div class=\"uploaded-img\">\n        <div class=\"list-title\">\n          {{ $T('ALREADY_UPLOAD') }}\n        </div>\n        <div\n          v-for=\"item in files\"\n          :key=\"item.imgUrl\"\n          class=\"img-list\"\n        >\n          <div\n            class=\"upload-img__container\"\n            @click=\"copyTheLink(item)\"\n          >\n            <img\n              v-lazy=\"item.imgUrl\"\n              class=\"upload-img\"\n            >\n            <div\n              class=\"upload-img__title\"\n              :title=\"item.fileName\"\n            >\n              {{ item.fileName }}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { reactive, ref, onBeforeUnmount, onBeforeMount } from 'vue'\nimport { ipcRenderer } from 'electron'\nimport $$db from '@/utils/db'\nimport { T as $T } from '@/i18n/index'\nimport type { IResult } from '@picgo/store/dist/types'\nimport { PASTE_TEXT, OPEN_WINDOW } from '#/events/constants'\nimport { IWindowList } from '#/types/enum'\nimport { sendToMain } from '@/utils/dataSender'\nimport { getRawData } from '@/utils/common'\nimport { showNotification } from '@/utils/notification'\nimport { IpcRendererEvent } from 'electron/renderer'\n\nconst files = ref<IResult<ImgInfo>[]>([])\nconst notification = reactive({\n  title: $T('COPY_LINK_SUCCEED'),\n  body: ''\n})\n\nconst clipboardFiles = ref<ImgInfo[]>([])\nconst uploadFlag = ref(false)\n\n// const reverseList = computed(() => files.value.slice().reverse())\n\nfunction openSettingWindow () {\n  sendToMain(OPEN_WINDOW, IWindowList.SETTING_WINDOW)\n}\n\nasync function getData () {\n  files.value = (await $$db.get<ImgInfo>({ orderBy: 'desc', limit: 5 })).data\n}\n\nasync function copyTheLink (item: ImgInfo) {\n  notification.body = item.imgUrl!\n  await ipcRenderer.invoke(PASTE_TEXT, getRawData(item))\n  showNotification({\n    title: notification.title,\n    body: notification.body\n  })\n}\n\n// function calcHeight (width: number, height: number): number {\n//   return height * 160 / width\n// }\n\nfunction disableDragFile () {\n  window.addEventListener('dragover', (e) => {\n    e = e || event\n    e.preventDefault()\n  }, false)\n  window.addEventListener('drop', (e) => {\n    e = e || event\n    e.preventDefault()\n  }, false)\n}\n\nfunction uploadClipboardFiles () {\n  if (uploadFlag.value) {\n    return\n  }\n  uploadFlag.value = true\n  sendToMain('uploadClipboardFiles')\n}\n\nonBeforeMount(() => {\n  disableDragFile()\n  getData()\n  ipcRenderer.on('dragFiles', async (event: IpcRendererEvent, _files: string[]) => {\n    for (let i = 0; i < _files.length; i++) {\n      const item = _files[i]\n      await $$db.insert(item)\n    }\n    files.value = (await $$db.get<ImgInfo>({ orderBy: 'desc', limit: 5 })).data\n  })\n  ipcRenderer.on('clipboardFiles', (event: IpcRendererEvent, files: ImgInfo[]) => {\n    clipboardFiles.value = files\n  })\n  ipcRenderer.on('uploadFiles', async () => {\n    files.value = (await $$db.get<ImgInfo>({ orderBy: 'desc', limit: 5 })).data\n    uploadFlag.value = false\n  })\n  ipcRenderer.on('updateFiles', () => {\n    getData()\n  })\n})\n\nonBeforeUnmount(() => {\n  ipcRenderer.removeAllListeners('dragFiles')\n  ipcRenderer.removeAllListeners('clipboardFiles')\n  ipcRenderer.removeAllListeners('uploadClipboardFiles')\n  ipcRenderer.removeAllListeners('updateFiles')\n})\n</script>\n\n<script lang=\"ts\">\nexport default {\n  name: 'TrayPage'\n}\n</script>\n\n<style lang=\"stylus\">\nbody::-webkit-scrollbar\n  width 0px\n#tray-page\n  .open-main-window\n    background #000\n    height 20px\n    line-height 20px\n    text-align center\n    color #858585\n    font-size 12px\n    cursor pointer\n    transition all .2s ease-in-out\n    &:hover\n      color: #fff;\n      background #49B1F5\n  .list-title\n    text-align center\n    color #858585\n    font-size 12px\n    padding 6px 0\n    position relative\n    &:before\n      content ''\n      position absolute\n      height 1px\n      width calc(100% - 36px)\n      bottom 0\n      left 18px\n      background #858585\n  // .header-arrow\n  //   position absolute\n  //   top 12px\n  //   left 50%\n  //   margin-left -10px\n  //   width: 0;\n  //   height: 0;\n  //   border-left: 10px solid transparent\n  //   border-right: 10px solid transparent\n  //   border-bottom: 10px solid rgba(255,255,255, 1)\n  .content\n    position absolute\n    top 20px\n    width 100%\n    background-color transparentify(#172426, #000, 0.9)\n  .img-list\n    padding 4px 8px\n    display flex\n    justify-content space-between\n    align-items center\n    // height 45px\n    cursor pointer\n    transition all .2s ease-in-out\n    &:hover\n      background #49B1F5\n      .upload-img__index\n        color #fff\n    .upload-img__container\n      display flex\n      flex-direction column\n      justify-content center\n      align-items center\n  .upload-img\n    max-width 100%\n    object-fit scale-down\n    margin 0 auto\n    &__container\n      display flex\n      flex-direction column\n      justify-content center\n      align-items center\n      width 100%\n      padding 8px 8px 4px\n      height 100%\n      &.upload\n        cursor not-allowed\n    &__title\n      text-align center\n      width 100%\n      overflow hidden\n      text-overflow ellipsis\n      white-space normal\n      word-break break-all\n      display -webkit-box\n      -webkit-box-orient vertical\n      -webkit-line-clamp 2\n      color #ddd\n      font-size 14px\n      margin-top 4px\n</style>\n"
  },
  {
    "path": "src/renderer/pages/Upload.vue",
    "content": "<template>\n  <div id=\"upload-view\">\n    <el-row :gutter=\"16\">\n      <el-col\n        :span=\"20\"\n        :offset=\"2\"\n      >\n        <div class=\"view-title text-[22px]\">\n          {{ $T('PICTURE_UPLOAD') }} - {{ picBedName }} - {{ configName }}\n          <el-icon\n            style=\"cursor: pointer; margin-left: 4px;\"\n            @click=\"handleChangePicBed\"\n          >\n            <CaretBottom />\n          </el-icon>\n        </div>\n        <div\n          id=\"upload-area\"\n          :class=\"{ 'is-dragover': dragover }\"\n          @drop.prevent=\"onDrop\"\n          @dragover.prevent=\"dragover = true\"\n          @dragleave.prevent=\"dragover = false\"\n        >\n          <div\n            id=\"upload-dragger\"\n            @click=\"openUploadWindow\"\n          >\n            <el-icon>\n              <UploadFilled />\n            </el-icon>\n            <div class=\"upload-dragger__text\">\n              {{ $T('DRAG_FILE_TO_HERE') }} <span>{{ $T('CLICK_TO_UPLOAD') }}</span>\n            </div>\n            <input\n              id=\"file-uploader\"\n              type=\"file\"\n              multiple\n              @change=\"onChange\"\n            >\n          </div>\n        </div>\n        <el-progress\n          :percentage=\"progress\"\n          :show-text=\"false\"\n          class=\"upload-progress\"\n          :class=\"{ 'show': showProgress }\"\n          :status=\"showError ? 'exception' : undefined\"\n        />\n        <div class=\"paste-style\">\n          <div class=\"el-col-16\">\n            <div class=\"paste-style__text\">\n              {{ $T('LINK_FORMAT') }}\n            </div>\n            <el-radio-group\n              v-model=\"pasteStyle\"\n              size=\"small\"\n              @change=\"handlePasteStyleChange\"\n            >\n              <el-radio-button label=\"markdown\">\n                Markdown\n              </el-radio-button>\n              <el-radio-button label=\"HTML\" />\n              <el-radio-button label=\"URL\" />\n              <el-radio-button label=\"UBB\" />\n              <el-radio-button\n                label=\"Custom\"\n                :title=\"$T('CUSTOM')\"\n              />\n            </el-radio-group>\n          </div>\n          <div class=\"el-col-8\">\n            <div class=\"paste-style__text\">\n              {{ $T('QUICK_UPLOAD') }}\n            </div>\n            <el-button\n              type=\"primary\"\n              round\n              size=\"small\"\n              class=\"quick-upload\"\n              style=\"width: 50%\"\n              @click=\"uploadClipboardFiles\"\n            >\n              {{ $T('CLIPBOARD_PICTURE') }}\n            </el-button>\n            <el-button\n              type=\"primary\"\n              round\n              size=\"small\"\n              class=\"quick-upload\"\n              style=\"width: 46%; margin-left: 6px\"\n              @click=\"uploadURLFiles\"\n            >\n              URL\n            </el-button>\n          </div>\n        </div>\n      </el-col>\n    </el-row>\n  </div>\n</template>\n<script lang=\"ts\" setup>\n// import { Component, Vue, Watch } from 'vue-property-decorator'\nimport { T as $T } from '@/i18n'\nimport $bus from '@/utils/bus'\nimport { getFilePath } from '@/utils/common'\nimport { saveConfig, sendToMain } from '@/utils/dataSender'\nimport { CaretBottom, UploadFilled } from '@element-plus/icons-vue'\nimport {\n  IpcRendererEvent,\n  ipcRenderer\n} from 'electron'\nimport { ElMessage as $message, ElMessageBox } from 'element-plus'\nimport { computed, onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'\nimport {\n  LOG_INVALID_URL_LINES,\n  SHOW_INPUT_BOX,\n  SHOW_INPUT_BOX_RESPONSE,\n  SHOW_UPLOAD_PAGE_MENU\n} from '~/universal/events/constants'\nimport {\n  extractHttpUrlsFromText,\n  isUrl,\n  parseNewlineSeparatedUrls\n} from '~/universal/utils/common'\nimport { useStore } from '@/hooks/useStore'\nconst dragover = ref(false)\nconst progress = ref(0)\nconst showProgress = ref(false)\nconst showError = ref(false)\nconst pasteStyle = ref('')\nconst store = useStore()\nconst currentPicBedType = computed(() => {\n  const config = store?.state.appConfig\n  return config?.picBed?.uploader || config?.picBed?.current || store?.state.defaultPicBed || 'smms'\n})\nconst picBedName = computed(() => {\n  const currentType = currentPicBedType.value\n  const list = store?.state.picBeds ?? []\n  const match = list.find(item => item.type === currentType)\n  return match?.name || currentType\n})\nconst configName = computed(() => {\n  const configEntry = store?.state.appConfig?.picBed?.[currentPicBedType.value] as IStringKeyMap | undefined\n  if (configEntry && typeof configEntry._configName === 'string') {\n    return configEntry._configName\n  }\n  return 'Default'\n})\nconst $confirm = ElMessageBox.confirm\nonBeforeMount(() => {\n  ipcRenderer.on('uploadProgress', (event: IpcRendererEvent, _progress: number) => {\n    if (_progress !== -1) {\n      showProgress.value = true\n      progress.value = _progress\n    } else {\n      progress.value = 100\n      showError.value = true\n    }\n  })\n  store?.refreshAppConfig()\n  store?.refreshPicBeds()\n  ipcRenderer.on('syncPicBed', () => {\n    store?.refreshAppConfig()\n  })\n  $bus.on(SHOW_INPUT_BOX_RESPONSE, handleInputBoxValue)\n})\n\nwatch(progress, onProgressChange)\n\nfunction onProgressChange (val: number) {\n  if (val === 100) {\n    setTimeout(() => {\n      showProgress.value = false\n      showError.value = false\n    }, 1000)\n    setTimeout(() => {\n      progress.value = 0\n    }, 1200)\n  }\n}\n\nonBeforeUnmount(() => {\n  $bus.off(SHOW_INPUT_BOX_RESPONSE)\n  ipcRenderer.removeAllListeners('uploadProgress')\n  ipcRenderer.removeAllListeners('syncPicBed')\n})\n\nasync function onDrop (e: DragEvent) {\n  dragover.value = false\n  const files = e.dataTransfer?.files!\n\n  // send files first\n  if (files?.length) {\n    ipcSendFiles(e.dataTransfer?.files!)\n    return\n  }\n\n  const dataTransfer = e.dataTransfer\n  if (!dataTransfer) return\n\n  const uriList = dataTransfer.getData('text/uri-list')\n  if (uriList) {\n    await handleUriListDrop(uriList, dataTransfer.getData('text/html'))\n    return\n  }\n\n  const plainText = dataTransfer.getData('text/plain')\n  if (plainText) {\n    await handlePlainTextDrop(plainText)\n    return\n  }\n\n  $message.error($T('TIPS_DRAG_VALID_PICTURE_OR_URL'))\n}\n\nasync function confirmLargeUrlBatch (count: number, onCancel?: () => void): Promise<boolean> {\n  if (count <= 10) return true\n  try {\n    await $confirm(\n      $T('TIPS_TOO_MANY_URLS_CONFIRM', { n: count }),\n      $T('TIPS_WARNING'),\n      {\n        type: 'warning',\n        confirmButtonText: $T('CONFIRM'),\n        cancelButtonText: $T('CANCEL')\n      }\n    )\n    return true\n  } catch (e) {\n    onCancel?.()\n    return false\n  }\n}\n\nasync function uploadUrls (urls: string[], invalidLines: string[], onCancel?: () => void) {\n  if (invalidLines.length) {\n    sendToMain(LOG_INVALID_URL_LINES, invalidLines)\n    $message.warning($T('TIPS_SKIPPED_INVALID_URLS', { n: invalidLines.length }))\n  }\n\n  const canUpload = await confirmLargeUrlBatch(urls.length, onCancel)\n  if (!canUpload) return\n\n  sendToMain('uploadChoosedFiles', urls.map((url) => ({ path: url })))\n}\n\nasync function handlePlainTextDrop (plainText: string) {\n  const { urls, invalidLines } = parseNewlineSeparatedUrls(plainText, { source: 'plain' })\n  if (!urls.length) {\n    $message.error($T('TIPS_DRAG_VALID_PICTURE_OR_URL'))\n    return\n  }\n  await uploadUrls(urls, invalidLines)\n}\n\nasync function handleUriListDrop (uriListText: string, urlString: string) {\n  const { urls, invalidLines } = parseNewlineSeparatedUrls(uriListText, { source: 'uri-list' })\n  if (urls.length) {\n    await uploadUrls(urls, invalidLines)\n    return\n  }\n\n  const urlMatch = urlString.match(/<img.*src=\"(.*?)\"/)\n  if (urlMatch && isUrl(urlMatch[1])) {\n    await uploadUrls([urlMatch[1]], invalidLines)\n    return\n  }\n\n  $message.error($T('TIPS_DRAG_VALID_PICTURE_OR_URL'))\n}\n\nfunction openUploadWindow () {\n  document.getElementById('file-uploader')!.click()\n}\n\nfunction onChange (e: any) {\n  ipcSendFiles(e.target.files);\n  (document.getElementById('file-uploader') as HTMLInputElement).value = ''\n}\n\nfunction ipcSendFiles (files: FileList) {\n  const sendFiles: IFileWithPath[] = []\n  Array.from(files).forEach((item) => {\n    const filePath = getFilePath(item)\n    if (!filePath) return\n    sendFiles.push({\n      name: item.name,\n      path: filePath\n    })\n  })\n  if (!sendFiles.length) return\n  sendToMain('uploadChoosedFiles', sendFiles)\n}\n\nconst applyAppConfig = () => {\n  const settings = store?.state.appConfig?.settings ?? {}\n  pasteStyle.value = settings.pasteStyle || 'markdown'\n}\n\nwatch(() => store?.state.appConfig, () => {\n  applyAppConfig()\n}, { immediate: true })\n\nfunction handlePasteStyleChange (val: string | number | boolean | undefined) {\n  saveConfig({\n    'settings.pasteStyle': val\n  })\n}\n\nfunction uploadClipboardFiles () {\n  sendToMain('uploadClipboardFilesFromUploadPage')\n}\n\nfunction openUrlInputBox (value: string) {\n  $bus.emit(SHOW_INPUT_BOX, {\n    value,\n    title: $T('TIPS_INPUT_URL'),\n    placeholder: $T('TIPS_HTTP_PREFIX'),\n    inputType: 'textarea'\n  })\n}\n\nasync function uploadURLFiles () {\n  let str = ''\n  try {\n    str = await navigator.clipboard.readText()\n  } catch (e) {}\n  const urls = extractHttpUrlsFromText(str)\n  openUrlInputBox(urls.join('\\n'))\n}\n\nasync function handleInputBoxValue (val: string) {\n  if (val === '') return\n\n  const { urls, invalidLines } = parseNewlineSeparatedUrls(val, { source: 'plain' })\n  if (!urls.length) {\n    if (invalidLines.length) {\n      sendToMain(LOG_INVALID_URL_LINES, invalidLines)\n      $message.error($T('TIPS_SKIPPED_INVALID_URLS', { n: invalidLines.length }))\n      return\n    }\n    $message.error($T('TIPS_NO_VALID_URLS'))\n    return\n  }\n\n  await uploadUrls(urls, invalidLines, () => openUrlInputBox(val))\n}\n\nasync function handleChangePicBed () {\n  sendToMain(SHOW_UPLOAD_PAGE_MENU)\n}\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'UploadPage'\n}\n</script>\n<style lang='stylus'>\n.view-title\n  display flex\n  color #eee\n  font-size 20px\n  text-align center\n  margin 10px auto\n  align-items center\n  justify-content center\n#upload-view\n  .view-title\n    margin 20px auto\n  #upload-area\n    height 220px\n    border 2px dashed #dddddd\n    border-radius 8px\n    text-align center\n    width 450px\n    margin 0 auto\n    color #dddddd\n    cursor pointer\n    transition all .2s ease-in-out\n    #upload-dragger\n      height 100%\n    &.is-dragover,\n    &:hover\n      border 2px dashed #A4D8FA\n      background-color rgba(164, 216, 250, 0.3)\n      color #fff\n    i\n      font-size 66px\n      margin 50px 0 16px\n      line-height 66px\n    span\n      color #409EFF\n  #file-uploader\n    display none\n  .upload-progress\n    opacity 0\n    transition all .2s ease-in-out\n    width 450px\n    margin 20px auto 0\n    &.show\n      opacity 1\n    .el-progress-bar__inner\n      transition all .2s ease-in-out\n  .paste-style\n    text-align center\n    margin-top 16px\n    display flex\n    align-items flex-end\n    &__text\n      font-size 12px\n      color #eeeeee\n      margin-bottom 4px\n  .el-radio-button:first-child\n    .el-radio-button__inner\n      border-left none\n  .el-radio-button:first-child\n    .el-radio-button__inner\n      border-left none\n      border-radius 14px 0 0 14px\n  .el-radio-button:last-child\n    .el-radio-button__inner\n      border-left none\n      border-radius 0 14px 14px 0\n  .el-icon-caret-bottom\n    cursor pointer\n</style>\n"
  },
  {
    "path": "src/renderer/pages/UploaderConfigPage.vue",
    "content": "<template>\n  <div\n    id=\"config-list-view\"\n    class=\"h-[425px]\"\n  >\n    <div class=\"view-title\">\n      {{ $T('SETTINGS') }}\n    </div>\n    <el-row\n      :gutter=\"15\"\n      justify=\"space-between\"\n      align=\"middle\"\n      type=\"flex\"\n      class=\"config-list\"\n    >\n      <el-col\n        v-for=\"item in curConfigList\"\n        :key=\"item._id\"\n        class=\"config-item-col\"\n        :span=\"11\"\n        :offset=\"1\"\n      >\n        <div\n          :class=\"`config-item ${defaultConfigId === item._id ? 'selected' : ''}`\"\n          @click=\"() => selectItem(item._configName)\"\n        >\n          <div class=\"config-name\">\n            {{ item._configName }}\n          </div>\n          <div class=\"config-update-time\">\n            {{ formatTime(item._updatedAt) }}\n          </div>\n          <div\n            v-if=\"defaultConfigId === item._id\"\n            class=\"default-text\"\n          >\n            {{ $T('SELECTED_SETTING_HINT') }}\n          </div>\n          <div class=\"operation-container\">\n            <el-icon\n              class=\"el-icon-edit\"\n              @click.stop=\"openEditPage(item._id)\"\n            >\n              <Edit />\n            </el-icon>\n            <el-icon\n              class=\"el-icon-copy\"\n              @click.stop=\"() => copyConfig(item._configName)\"\n            >\n              <DocumentCopy />\n            </el-icon>\n            <el-icon\n              class=\"el-icon-delete\"\n              :class=\"curConfigList.length <= 1 ? 'disabled' : ''\"\n              @click.stop=\"() => deleteConfig(item._configName)\"\n            >\n              <Delete />\n            </el-icon>\n          </div>\n        </div>\n      </el-col>\n      <el-col\n        class=\"config-item-col\"\n        :span=\"11\"\n        :offset=\"1\"\n      >\n        <div\n          class=\"config-item config-item-add\"\n          @click=\"addNewConfig\"\n        >\n          <el-icon\n            class=\"el-icon-plus\"\n          >\n            <Plus />\n          </el-icon>\n        </div>\n      </el-col>\n    </el-row>\n    <el-row\n      type=\"flex\"\n      justify=\"center\"\n      :span=\"24\"\n      class=\"set-default-container\"\n    >\n      <el-button\n        class=\"set-default-btn\"\n        type=\"success\"\n        round\n        :disabled=\"store?.state.defaultPicBed === type\"\n        @click=\"setDefaultPicBed(type)\"\n      >\n        {{ $T('SETTINGS_SET_DEFAULT_PICBED') }}\n      </el-button>\n    </el-row>\n  </div>\n</template>\n<script lang=\"ts\" setup>\nimport { Delete, DocumentCopy, Edit, Plus } from '@element-plus/icons-vue'\nimport { ElMessage, ElMessageBox } from 'element-plus'\nimport { saveConfig, invokeRPC } from '@/utils/dataSender'\nimport { showNotification } from '@/utils/notification'\nimport dayjs from 'dayjs'\nimport { IRPCActionType } from '~/universal/types/enum'\nimport { T as $T } from '@/i18n/index'\nimport { useRouter, useRoute, onBeforeRouteUpdate } from 'vue-router'\nimport { computed, onBeforeMount, ref, watch } from 'vue'\nimport { PICBEDS_PAGE, UPLOADER_CONFIG_PAGE } from '@/router/config'\nimport { useStore } from '@/hooks/useStore'\nconst $router = useRouter()\nconst $route = useRoute()\n\nconst type = ref('')\nconst curConfigList = ref<IUploaderConfigListItem[]>([])\nconst defaultConfigId = ref('')\nconst store = useStore()\nconst appConfig = computed(() => store?.state.appConfig ?? null)\nconst $confirm = ElMessageBox.confirm\nconst $prompt = ElMessageBox.prompt\n\ntype MessageBoxAction = 'confirm' | 'cancel' | 'close'\n\ninterface PromptBoxState {\n  inputValue?: string\n  confirmButtonLoading?: boolean\n}\n\nasync function selectItem (configName: string) {\n  const res = await invokeRPC<string>(IRPCActionType.SELECT_UPLOADER, type.value, configName)\n  if (!res.success) {\n    ElMessage.warning(res.error)\n    return\n  }\n  defaultConfigId.value = res.data\n}\n\nonBeforeRouteUpdate((to, from, next) => {\n  if (to.params.type && (to.name === UPLOADER_CONFIG_PAGE)) {\n    type.value = to.params.type as string\n    getCurrentConfigList()\n  }\n  next()\n})\n\nonBeforeMount(() => {\n  type.value = $route.params.type as string\n  getCurrentConfigList()\n  store?.refreshAppConfig()\n})\n\nwatch(appConfig, () => {\n  if (!type.value) return\n  getCurrentConfigList()\n})\n\nasync function getCurrentConfigList () {\n  const configList = await invokeRPC<IUploaderConfigItem>(IRPCActionType.GET_PICBED_CONFIG_LIST, type.value)\n  if (!configList.success) {\n    ElMessage.warning(configList.error)\n    return\n  }\n  curConfigList.value = configList.data?.configList ?? []\n  defaultConfigId.value = configList.data?.defaultId ?? ''\n}\n\nfunction openEditPage (configId: string) {\n  $router.push({\n    name: PICBEDS_PAGE,\n    params: {\n      type: type.value,\n      configId\n    },\n    query: {\n      defaultConfigId: defaultConfigId.value\n    }\n  })\n}\n\nfunction formatTime (time: number): string {\n  return dayjs(time).format('YYYY-MM-DD HH:mm:ss')\n}\n\nfunction deleteConfig (configName: string) {\n  if (curConfigList.value.length <= 1) {\n    return\n  }\n  $confirm($T('TIPS_DELETE_UPLOADER_CONFIG'), $T('TIPS_NOTICE'), {\n    confirmButtonText: $T('CONFIRM'),\n    cancelButtonText: $T('CANCEL'),\n    type: 'warning'\n  }).then(async () => {\n    const res = await invokeRPC<IUploaderConfigItem>(IRPCActionType.DELETE_PICBED_CONFIG, type.value, configName)\n    if (!res.success) {\n      ElMessage.warning(res.error)\n      return\n    }\n    curConfigList.value = res.data.configList\n    defaultConfigId.value = res.data.defaultId\n  }).catch((e) => {\n    console.log(e)\n    return true\n  })\n}\n\nfunction copyConfig (configName: string) {\n  $prompt($T('TIPS_COPY_UPLOADER_CONFIG'), $T('TIPS_NOTICE'), {\n    confirmButtonText: $T('CONFIRM'),\n    cancelButtonText: $T('CANCEL'),\n    inputValue: `${configName} - Copy`,\n    inputPlaceholder: $T('UPLOADER_CONFIG_PLACEHOLDER'),\n    beforeClose: async (action: MessageBoxAction, instance: PromptBoxState, done: () => void) => {\n      if (action !== 'confirm') {\n        done()\n        return\n      }\n\n      const newConfigName = String(instance.inputValue ?? '').trim()\n      if (!newConfigName) {\n        ElMessage.warning($T('TIPS_UPLOADER_CONFIG_NAME_EMPTY'))\n        return\n      }\n\n      instance.confirmButtonLoading = true\n      const res = await invokeRPC<IUploaderConfigItem>(IRPCActionType.COPY_UPLOADER_CONFIG, type.value, configName, newConfigName)\n      instance.confirmButtonLoading = false\n\n      if (!res.success) {\n        ElMessage.warning(res.error)\n        return\n      }\n      curConfigList.value = res.data.configList\n      defaultConfigId.value = res.data.defaultId\n      done()\n    }\n  }).catch((e) => {\n    console.log(e)\n    return true\n  })\n}\n\nfunction addNewConfig () {\n  $router.push({\n    name: PICBEDS_PAGE,\n    params: {\n      type: type.value,\n      configId: ''\n    }\n  })\n}\n\nfunction setDefaultPicBed (type: string) {\n  saveConfig({\n    'picBed.current': type,\n    'picBed.uploader': type\n  })\n\n  store?.setDefaultPicBed(type)\n  showNotification({\n    title: $T('SETTINGS_DEFAULT_PICBED'),\n    body: $T('TIPS_SET_SUCCEED')\n  })\n}\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'UploaderConfigPage'\n}\n</script>\n<style lang='stylus'>\n#config-list-view\n  position relative\n  min-height 100%\n  overflow-x hidden\n  overflow-y auto\n  padding-bottom 50px\n  box-sizing border-box\n  .config-list\n    flex-wrap wrap\n    width: 98%\n    .config-item\n      height 100px\n      margin-bottom 20px\n      border-radius 4px\n      cursor pointer\n      box-sizing border-box\n      padding 8px\n      background rgba(130, 130, 130, .2)\n      border 1px solid transparent\n      box-shadow 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)\n      position relative\n      .config-name\n        color #eee\n        font-size 16px\n      .config-update-time\n        color #aaa\n        font-size 14px\n        margin-top 10px\n      .default-text\n        color #67C23A\n        font-size 12px\n        margin-top 5px\n      .operation-container\n        position absolute\n        right 5px\n        top 8px\n        font-size 18pxc\n        display flex\n        align-items center\n        color #eee\n        .el-icon-edit\n        .el-icon-copy\n        .el-icon-delete\n          cursor pointer\n        .el-icon-edit\n        .el-icon-copy\n          margin-right 10px\n        .disabled\n          cursor not-allowed\n          color #aaa\n    .config-item-add\n      display: flex\n      justify-content: center\n      align-items: center\n      color: #eee\n      font-size: 28px\n    .selected\n      border 1px solid #409EFF\n  .set-default-container\n    position fixed\n    bottom 20px\n    width 100%\n    .set-default-btn\n      width 250px\n      transform translateX(-50%)\n</style>\n"
  },
  {
    "path": "src/renderer/pages/UrlRewrite.vue",
    "content": "<template>\n  <div id=\"url-rewrite-page\">\n    <div class=\"view-title\">\n      {{ $T('SETTINGS_URL_REWRITE') }}\n    </div>\n    <el-row\n      class=\"url-rewrite-list\"\n      justify=\"center\"\n    >\n      <el-col\n        :span=\"20\"\n        :offset=\"2\"\n      >\n        <div class=\"flex mb-[12px] justify-between align-middle\">\n          <div class=\"text-[12px] text-[#bbb] leading-[18px]\">\n            {{ $T('URL_REWRITE_HELP') }}\n          </div>\n          <el-button\n            type=\"primary\"\n            size=\"small\"\n            @click=\"openAddDialog\"\n          >\n            <el-icon class=\"mr-[4px]\">\n              <Plus />\n            </el-icon>\n            {{ $T('URL_REWRITE_ADD_RULE') }}\n          </el-button>\n        </div>\n\n        <el-table\n          class=\"url-rewrite-table-border\"\n          :data=\"rules\"\n          size=\"small\"\n          header-cell-class-name=\"url-rewrite-table-border\"\n          cell-class-name=\"url-rewrite-table-border\"\n          :empty-text=\"$T('URL_REWRITE_EMPTY')\"\n        >\n          <el-table-column\n            width=\"50px\"\n            :label=\"$T('URL_REWRITE_ORDER')\"\n          >\n            <template #default=\"scope\">\n              {{ scope.$index + 1 }}\n            </template>\n          </el-table-column>\n\n          <el-table-column\n            min-width=\"300px\"\n            :label=\"$T('URL_REWRITE_MATCH')\"\n          >\n            <template #default=\"scope\">\n              <span class=\"font-mono text-[12px] break-all\">\n                {{ scope.row.match }}\n              </span>\n            </template>\n          </el-table-column>\n\n          <el-table-column\n            width=\"80px\"\n            :label=\"$T('URL_REWRITE_FLAGS')\"\n          >\n            <template #default=\"scope\">\n              <el-tag\n                v-if=\"scope.row.global\"\n                size=\"small\"\n              >\n                G\n              </el-tag>\n              <el-tag\n                v-if=\"scope.row.ignoreCase\"\n                size=\"small\"\n                class=\"ml-[4px]\"\n              >\n                I\n              </el-tag>\n              <span v-if=\"!scope.row.global && !scope.row.ignoreCase\">-</span>\n            </template>\n          </el-table-column>\n\n          <el-table-column\n            width=\"70px\"\n            :label=\"$T('URL_REWRITE_ENABLED')\"\n          >\n            <template #default=\"scope\">\n              <el-switch\n                v-model=\"scope.row.enable\"\n                @change=\"handleToggleEnable(scope.$index, $event as boolean)\"\n              />\n            </template>\n          </el-table-column>\n\n          <el-table-column\n            width=\"220px\"\n            :label=\"$T('URL_REWRITE_ACTIONS')\"\n          >\n            <template #default=\"scope\">\n              <el-row class=\"mb-[2px]\">\n                <el-button\n                  size=\"small\"\n                  type=\"text\"\n                  :disabled=\"scope.$index === 0\"\n                  @click=\"moveRule(scope.$index, scope.$index - 1)\"\n                >\n                  <el-icon class=\"mr-[2px]\">\n                    <ArrowUp />\n                  </el-icon>\n                  {{ $T('URL_REWRITE_MOVE_UP') }}\n                </el-button>\n                <el-button\n                  size=\"small\"\n                  type=\"text\"\n                  :disabled=\"scope.$index === rules.length - 1\"\n                  @click=\"moveRule(scope.$index, scope.$index + 1)\"\n                >\n                  <el-icon class=\"mr-[2px]\">\n                    <ArrowDown />\n                  </el-icon>\n                  {{ $T('URL_REWRITE_MOVE_DOWN') }}\n                </el-button>\n              </el-row>\n              <el-row>\n                <el-button\n                  size=\"small\"\n                  type=\"text\"\n                  @click=\"openEditDialog(scope.row, scope.$index)\"\n                >\n                  <el-icon class=\"mr-[2px]\">\n                    <Edit />\n                  </el-icon>\n                  {{ $T('URL_REWRITE_EDIT') }}\n                </el-button>\n                <el-button\n                  class=\"danger\"\n                  size=\"small\"\n                  type=\"text\"\n                  @click=\"confirmDelete(scope.$index)\"\n                >\n                  <el-icon class=\"mr-[2px]\">\n                    <Delete />\n                  </el-icon>\n                  {{ $T('URL_REWRITE_DELETE') }}\n                </el-button>\n              </el-row>\n            </template>\n          </el-table-column>\n        </el-table>\n\n        <div class=\"mt-[16px] rounded-[8px] url-rewrite-panel p-[12px]\">\n          <div class=\"text-[14px] mb-[8px] text-[#bbb] font-bold\">\n            {{ $T('URL_REWRITE_PREVIEW_TITLE') }}\n          </div>\n          <div class=\"text-[12px] text-[#bbb] leading-[18px] mb-[10px]\">\n            {{ $T('URL_REWRITE_PREVIEW_TIPS') }}\n          </div>\n\n          <el-row\n            class=\"mb-[10px]\"\n            :gutter=\"10\"\n          >\n            <el-col :span=\"18\">\n              <el-input\n                v-model=\"previewInputUrl\"\n                :placeholder=\"$T('URL_REWRITE_PREVIEW_PLACEHOLDER')\"\n                size=\"small\"\n              />\n            </el-col>\n            <el-col :span=\"6\">\n              <el-button\n                type=\"primary\"\n                size=\"small\"\n                class=\"w-full\"\n                @click=\"runPreview\"\n              >\n                {{ $T('URL_REWRITE_PREVIEW_RUN') }}\n              </el-button>\n            </el-col>\n          </el-row>\n\n          <el-alert\n            v-if=\"previewStatus === 'error'\"\n            :title=\"previewMessage\"\n            type=\"error\"\n            show-icon\n            :closable=\"false\"\n          />\n          <el-alert\n            v-else-if=\"previewStatus === 'matched'\"\n            :title=\"previewMessage\"\n            type=\"success\"\n            show-icon\n            :closable=\"false\"\n          />\n          <el-alert\n            v-else-if=\"previewStatus === 'noMatch'\"\n            :title=\"previewMessage\"\n            type=\"info\"\n            show-icon\n            :closable=\"false\"\n          />\n\n          <div\n            v-if=\"previewStatus !== 'idle'\"\n            class=\"mt-[10px]\"\n          >\n            <div class=\"text-[12px] text-[#bbb] mb-[6px]\">\n              {{ $T('URL_REWRITE_PREVIEW_OUTPUT') }}\n            </div>\n            <div class=\"rounded-[6px] url-rewrite-mono-box p-[10px] font-mono text-[12px] break-all text-[#bbb]\">\n              {{ previewOutputUrl }}\n            </div>\n          </div>\n        </div>\n      </el-col>\n    </el-row>\n\n    <el-dialog\n      v-model=\"editDialogVisible\"\n      :title=\"editDialogTitle\"\n      width=\"500px\"\n      :append-to-body=\"true\"\n    >\n      <el-form\n        label-position=\"top\"\n        label-width=\"80px\"\n        size=\"small\"\n      >\n        <el-form-item\n          :label=\"$T('URL_REWRITE_MATCH')\"\n        >\n          <div class=\"flex flex-col gap-[6px] w-full\">\n            <div class=\"text-[12px] text-[#bbb] leading-[18px]\">\n              {{ $T('URL_REWRITE_MATCH_TIPS') }}\n            </div>\n            <el-input\n              v-model=\"ruleForm.match\"\n              class=\"align-center\"\n              :autofocus=\"true\"\n              :placeholder=\"$T('URL_REWRITE_MATCH_PLACEHOLDER')\"\n            />\n          </div>\n        </el-form-item>\n\n        <el-form-item\n          :label=\"$T('URL_REWRITE_REPLACE')\"\n        >\n          <div class=\"flex flex-col gap-[6px] w-full\">\n            <div class=\"text-[12px] text-[#bbb] leading-[18px]\">\n              {{ $T('URL_REWRITE_REPLACE_TIPS') }}\n            </div>\n            <el-input\n              v-model=\"ruleForm.replace\"\n              class=\"align-center\"\n              :placeholder=\"$T('URL_REWRITE_REPLACE_PLACEHOLDER')\"\n            />\n          </div>\n        </el-form-item>\n\n        <el-form-item\n          :label=\"$T('URL_REWRITE_OPTIONS')\"\n        >\n          <div class=\"flex flex-col gap-[10px] w-full\">\n            <el-row\n              justify=\"space-between\"\n              align=\"middle\"\n            >\n              <div class=\"text-[13px]\">\n                {{ $T('URL_REWRITE_RULE_ENABLED') }}\n              </div>\n              <el-switch v-model=\"ruleForm.enable\" />\n            </el-row>\n\n            <div class=\"grid grid-cols-2 gap-[12px]\">\n              <div class=\"rounded-[6px] url-rewrite-option-card p-[10px]\">\n                <el-checkbox\n                  v-model=\"ruleForm.global\"\n                >\n                  {{ $T('URL_REWRITE_FLAG_GLOBAL_LABEL') }}\n                </el-checkbox>\n                <div class=\"text-[12px] text-[#bbb] leading-[18px] mt-[6px]\">\n                  {{ $T('URL_REWRITE_FLAG_GLOBAL_DESC') }}\n                </div>\n              </div>\n              <div class=\"rounded-[6px] url-rewrite-option-card p-[10px]\">\n                <el-checkbox\n                  v-model=\"ruleForm.ignoreCase\"\n                >\n                  {{ $T('URL_REWRITE_FLAG_IGNORE_CASE_LABEL') }}\n                </el-checkbox>\n                <div class=\"text-[12px] text-[#bbb] leading-[18px] mt-[6px]\">\n                  {{ $T('URL_REWRITE_FLAG_IGNORE_CASE_DESC') }}\n                </div>\n              </div>\n            </div>\n          </div>\n        </el-form-item>\n      </el-form>\n\n      <template #footer>\n        <el-button\n          round\n          @click=\"cancelEditDialog\"\n        >\n          {{ $T('CANCEL') }}\n        </el-button>\n        <el-button\n          type=\"primary\"\n          round\n          @click=\"confirmEditDialog\"\n        >\n          {{ $T('CONFIRM') }}\n        </el-button>\n      </template>\n    </el-dialog>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ArrowDown, ArrowUp, Delete, Edit, Plus } from '@element-plus/icons-vue'\nimport { ElMessage as $message, ElMessageBox } from 'element-plus'\nimport { computed, onBeforeMount, reactive, ref } from 'vue'\nimport { getConfig, saveConfig } from '@/utils/dataSender'\nimport { T as $T } from '@/i18n'\n\ninterface IUrlRewriteRule {\n  match: string\n  replace: string\n  enable: boolean\n  global: boolean\n  ignoreCase: boolean\n}\n\nconst $confirm = ElMessageBox.confirm\n\nconst rules = ref<IUrlRewriteRule[]>([])\n\nconst editDialogVisible = ref(false)\nconst editDialogMode = ref<'add' | 'edit'>('add')\nconst editRuleIndex = ref(-1)\n\nconst previewInputUrl = ref('')\nconst previewOutputUrl = ref('')\nconst previewStatus = ref<'idle' | 'matched' | 'noMatch' | 'error'>('idle')\nconst previewMessage = ref('')\n\nconst ruleForm = reactive<IUrlRewriteRule>({\n  match: '',\n  replace: '',\n  enable: true,\n  global: false,\n  ignoreCase: false\n})\n\nconst editDialogTitle = computed(() => {\n  return editDialogMode.value === 'add' ? $T('URL_REWRITE_ADD_RULE') : $T('URL_REWRITE_EDIT_RULE')\n})\n\nonBeforeMount(async () => {\n  await initRules()\n})\n\nasync function initRules () {\n  const configRules = await getConfig<unknown>('settings.urlRewrite.rules')\n  rules.value = normalizeRules(configRules)\n}\n\nfunction normalizeRules (value: unknown): IUrlRewriteRule[] {\n  if (!Array.isArray(value)) return []\n  return value.map(item => {\n    const raw = (item ?? {}) as Partial<Record<keyof IUrlRewriteRule, unknown>>\n    return {\n      match: String(raw.match ?? ''),\n      replace: String(raw.replace ?? ''),\n      enable: raw.enable === false ? false : true,\n      global: raw.global === true,\n      ignoreCase: raw.ignoreCase === true\n    }\n  })\n}\n\nasync function persistRules () {\n  try {\n    await saveConfig('settings.urlRewrite.rules', rules.value)\n  } catch (e) {\n    $message.error($T('OPERATION_FAILED'))\n    throw e\n  }\n}\n\nasync function handleToggleEnable (index: number, value: boolean) {\n  if (!rules.value[index]) return\n  rules.value[index].enable = value\n  await persistRules()\n}\n\nasync function moveRule (fromIndex: number, toIndex: number) {\n  if (fromIndex === toIndex) return\n  if (toIndex < 0 || toIndex >= rules.value.length) return\n  const next = [...rules.value]\n  const [item] = next.splice(fromIndex, 1)\n  next.splice(toIndex, 0, item)\n  rules.value = next\n  await persistRules()\n}\n\nasync function confirmDelete (index: number) {\n  if (!rules.value[index]) return\n  try {\n    await $confirm($T('URL_REWRITE_DELETE_CONFIRM'), $T('TIPS_WARNING'), {\n      type: 'warning',\n      confirmButtonText: $T('CONFIRM'),\n      cancelButtonText: $T('CANCEL')\n    })\n  } catch {\n    return\n  }\n  rules.value.splice(index, 1)\n  await persistRules()\n}\n\nfunction openAddDialog () {\n  editDialogMode.value = 'add'\n  editRuleIndex.value = -1\n  ruleForm.match = ''\n  ruleForm.replace = ''\n  ruleForm.enable = true\n  ruleForm.global = false\n  ruleForm.ignoreCase = false\n  editDialogVisible.value = true\n}\n\nfunction openEditDialog (rule: IUrlRewriteRule, index: number) {\n  editDialogMode.value = 'edit'\n  editRuleIndex.value = index\n  ruleForm.match = rule.match\n  ruleForm.replace = rule.replace\n  ruleForm.enable = rule.enable\n  ruleForm.global = rule.global\n  ruleForm.ignoreCase = rule.ignoreCase\n  editDialogVisible.value = true\n}\n\nfunction cancelEditDialog () {\n  editDialogVisible.value = false\n}\n\nfunction buildRuleFlags (rule: Pick<IUrlRewriteRule, 'global' | 'ignoreCase'>) {\n  return `${rule.global ? 'g' : ''}${rule.ignoreCase ? 'i' : ''}`\n}\n\nfunction validateRuleOrThrow (rule: IUrlRewriteRule) {\n  if (!rule.match.trim()) {\n    throw new Error($T('URL_REWRITE_MATCH_REQUIRED'))\n  }\n  if (!rule.replace.trim()) {\n    throw new Error($T('URL_REWRITE_REPLACE_REQUIRED'))\n  }\n  new RegExp(rule.match, buildRuleFlags(rule))\n}\n\nfunction runPreview () {\n  previewStatus.value = 'idle'\n  previewMessage.value = ''\n  previewOutputUrl.value = previewInputUrl.value\n\n  const url = previewInputUrl.value\n  if (!url) {\n    previewStatus.value = 'error'\n    previewMessage.value = $T('URL_REWRITE_PREVIEW_INPUT_REQUIRED')\n    return\n  }\n\n  for (const [index, rule] of rules.value.entries()) {\n    if (rule.enable === false) continue\n    let regexp: RegExp\n    try {\n      regexp = new RegExp(rule.match, buildRuleFlags(rule))\n    } catch (e) {\n      previewStatus.value = 'error'\n      previewMessage.value = `${$T('URL_REWRITE_PREVIEW_RULE_INVALID')} #${index + 1}: ${(e as Error).message}`\n      return\n    }\n\n    const matched = regexp.test(url)\n    regexp.lastIndex = 0\n    if (!matched) continue\n\n    const output = url.replace(regexp, rule.replace)\n    previewOutputUrl.value = output\n    previewStatus.value = 'matched'\n    previewMessage.value = `${$T('URL_REWRITE_PREVIEW_MATCHED_RULE')} #${index + 1}`\n    return\n  }\n\n  previewStatus.value = 'noMatch'\n  previewMessage.value = $T('URL_REWRITE_PREVIEW_NO_MATCH')\n}\n\nasync function confirmEditDialog () {\n  const nextRule: IUrlRewriteRule = {\n    match: ruleForm.match,\n    replace: ruleForm.replace,\n    enable: ruleForm.enable,\n    global: ruleForm.global,\n    ignoreCase: ruleForm.ignoreCase\n  }\n\n  try {\n    validateRuleOrThrow(nextRule)\n  } catch (e) {\n    $message.error((e as Error).message || $T('URL_REWRITE_INVALID_REGEX'))\n    return\n  }\n\n  if (editDialogMode.value === 'add') {\n    rules.value.push(nextRule)\n  } else if (editRuleIndex.value >= 0 && rules.value[editRuleIndex.value]) {\n    rules.value.splice(editRuleIndex.value, 1, nextRule)\n  }\n\n  await persistRules()\n  editDialogVisible.value = false\n  $message.success($T('TIPS_SET_SUCCEED'))\n}\n\n</script>\n\n<script lang=\"ts\">\nexport default {\n  name: 'UrlRewritePage'\n}\n</script>\n\n<style lang='stylus'>\n#url-rewrite-page\n  .url-rewrite-list\n    height 360px\n    box-sizing border-box\n    overflow-y auto\n    overflow-x hidden\n    width 100%\n  .url-rewrite-panel\n    background rgba(130, 130, 130, .12)\n    border 1px solid darken(#eee, 50%)\n  .url-rewrite-option-card\n    background rgba(130, 130, 130, .12)\n    border 1px solid rgba(255, 255, 255, .06)\n  .url-rewrite-mono-box\n    background rgba(130, 130, 130, .12)\n    border 1px solid rgba(255, 255, 255, .06)\n  .url-rewrite-table-border\n    border-color darken(#eee, 50%)\n  .el-checkbox__label\n    color #aaa\n  .el-table\n    background-color: transparent\n    color #ddd\n    &::before\n      background-color darken(#eee, 50%)\n    thead\n      color #bbb\n    th,tr\n      background-color: transparent\n    &__body\n      tr.el-table__row--striped\n        td\n          background transparent\n    &--enable-row-hover\n      .el-table__body\n        tr:hover\n          &>td\n            background #333\n  .el-button+.el-button\n    margin-left 4px\n  .el-button\n    &.danger\n      color: #F56C6C\n</style>\n"
  },
  {
    "path": "src/renderer/pages/components/gallery/GalleryToolbar.vue",
    "content": "<template>\n  <el-col :span=\"12\">\n    <div class=\"w-full flex justify-between gap-x-[6px]\">\n      <div\n        class=\"item-base copy round gap-x-[10px]\"\n        :class=\"{ active: isMultiple(selectedList)}\"\n        @click=\"$emit('multiCopy')\"\n      >\n        {{ $T('COPY') }}\n      </div>\n      <div\n        class=\"item-base delete round\"\n        :class=\"{ active: isMultiple(selectedList)}\"\n        @click=\"$emit('multiRemove')\"\n      >\n        {{ $T('DELETE') }}\n      </div>\n      <div\n        class=\"item-base all-pick round\"\n        :class=\"{ active: filterList.length > 0}\"\n        @click=\"$emit('toggleSelectAll')\"\n      >\n        {{ isAllSelected ? $T('CANCEL') : $T('SELECT_ALL') }}\n      </div>\n      <div\n        class=\"item-base select-more round !w-[18%]\"\n        :class=\"{ active: filterList.length > 0}\"\n        @click=\"$emit('selectMore')\"\n      >\n        <el-icon name=\"el-icon-arrow-left\">\n          <MoreFilled />\n        </el-icon>\n      </div>\n    </div>\n    <ConfigFormDialog />\n  </el-col>\n</template>\n<script lang=\"ts\" setup>\nimport { T as $T } from '@/i18n'\nimport { MoreFilled } from '@element-plus/icons-vue'\nimport ConfigFormDialog from '@/components/dialog/ConfigFormDialog.vue'\n\ninterface IProps {\n  selectedList: IObjT<boolean>\n  filterList: IGalleryItem[]\n  isAllSelected: boolean\n}\ndefineProps<IProps>()\n\ndefineEmits(['multiCopy', 'multiRemove', 'toggleSelectAll', 'selectMore'])\n\nfunction isMultiple (obj: IObj) {\n  return Object.values(obj).some(item => item)\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'GalleryToolbar'\n}\n</script>\n<style lang='stylus'>\n.item-base\n  background #2E2E2E\n  text-align center\n  width: 22%\n  flex-grow: 1\n  cursor pointer\n  font-size 12px\n  transition all .2s ease-in-out\n  height: 24px\n  line-height: 28px\n  box-sizing: border-box\n  display: flex\n  align-items: center\n  justify-content: center\n  &.copy\n    cursor not-allowed\n    background #49B1F5\n    &.active\n      cursor pointer\n      background #1B9EF3\n      color #fff\n  &.delete\n    cursor not-allowed\n    background #F47466\n    &.active\n      cursor pointer\n      background #F15140\n      color #fff\n  &.all-pick\n    cursor not-allowed\n    background #69C282\n    &.active\n      cursor pointer\n      background #44B363\n      color #fff\n  &.select-more\n    cursor pointer\n    background #858585\n    color #fff\n</style>\n"
  },
  {
    "path": "src/renderer/pages/components/settings/buttonArea/ButtonAreaSettings.vue",
    "content": "<template>\n  <ButtonFormItem\n    :label=\"$T('SETTINGS_OPEN_CONFIG_FILE')\"\n    :button-label=\"$T('SETTINGS_CLICK_TO_OPEN')\"\n    @click=\"openFile('data.json')\"\n  />\n  <ButtonFormItem\n    :label=\"$T('SETTINGS_SET_LOG_FILE')\"\n    :button-label=\"$T('SETTINGS_CLICK_TO_SET')\"\n    @click=\"logFileDialogVisible = true\"\n  />\n  <ButtonFormItem\n    :label=\"$T('SETTINGS_SET_SHORTCUT')\"\n    :button-label=\"$T('SETTINGS_CLICK_TO_SET')\"\n    @click=\"goShortCutPage\"\n  />\n  <ButtonFormItem\n    :label=\"$T('SETTINGS_URL_REWRITE')\"\n    :button-label=\"$T('SETTINGS_CLICK_TO_SET')\"\n    @click=\"goUrlRewritePage\"\n  />\n  <ButtonFormItem\n    :label=\"$T('SETTINGS_CUSTOM_LINK_FORMAT')\"\n    :button-label=\"$T('SETTINGS_CLICK_TO_SET')\"\n    @click=\"customLinkDialogVisible = true\"\n  />\n  <ButtonFormItem\n    :label=\"$T('SETTINGS_SET_PROXY_AND_MIRROR')\"\n    :button-label=\"$T('SETTINGS_CLICK_TO_SET')\"\n    @click=\"proxySettingDialogVisible = true\"\n  />\n  <ButtonFormItem\n    :label=\"$T('SETTINGS_SET_SERVER')\"\n    :button-label=\"$T('SETTINGS_CLICK_TO_SET')\"\n    @click=\"serverSettingDialogVisible = true\"\n  />\n  <ButtonFormItem\n    :label=\"$T('SETTINGS_CHECK_UPDATE')\"\n    :button-label=\"$T('SETTINGS_CLICK_TO_CHECK')\"\n    @click=\"checkUpdateDialogVisible = true\"\n  />\n  <LogSettingDialog\n    v-model=\"logFileDialogVisible\"\n    v-model:log-level=\"form.logLevel\"\n    v-model:log-file-size-limit=\"form.logFileSizeLimit\"\n  />\n  <CustomLinkDialog\n    v-model=\"customLinkDialogVisible\"\n    v-model:custom-link=\"form.customLink\"\n  />\n  <ProxySettingDialog\n    v-model=\"proxySettingDialogVisible\"\n    v-model:proxy=\"proxyString\"\n    v-model:npm-proxy=\"form.npmProxy\"\n    v-model:npm-registry=\"form.npmRegistry\"\n  />\n  <ServerSettingsDialog\n    v-model=\"serverSettingDialogVisible\"\n    v-model:host=\"form.server.host\"\n    v-model:port=\"form.server.port\"\n    v-model:enable=\"form.server.enable\"\n  />\n  <CheckUpdateDialog\n    v-model=\"checkUpdateDialogVisible\"\n    v-model:checkBetaUpdate=\"form.checkBetaUpdate\"\n  />\n</template>\n<script lang=\"ts\" setup>\nimport ButtonFormItem from '@/components/settings/ButtonFormItem.vue'\nimport CustomLinkDialog from './CustomLinkDialog.vue'\nimport ProxySettingDialog from './ProxySettingDialog.vue'\nimport ServerSettingsDialog from './ServerSettingsDialog.vue'\nimport CheckUpdateDialog from './CheckUpdateDialog.vue'\nimport { T as $T } from '@/i18n'\nimport { sendToMain } from '@/utils/dataSender'\nimport { reactive, ref } from 'vue'\nimport { PICGO_OPEN_FILE } from '~/universal/events/constants'\nimport LogSettingDialog from './LogSettingDialog.vue'\nimport { useRouter } from 'vue-router'\nimport { SHORTKEY_PAGE, URL_REWRITE_PAGE } from '@/router/config'\nimport { useVModel } from '@/hooks/useVModel'\n\nconst $router = useRouter()\n\ninterface IProps {\n  settings: ISettingForm\n  proxy: string\n}\n\nconst props = defineProps<IProps>()\n\nconst form = reactive(props.settings)\nconst proxyString = useVModel(props, 'proxy')\n\nconst logFileDialogVisible = ref(false)\nconst customLinkDialogVisible = ref(false)\nconst proxySettingDialogVisible = ref(false)\nconst serverSettingDialogVisible = ref(false)\nconst checkUpdateDialogVisible = ref(false)\n\nfunction openFile (file: string) {\n  sendToMain(PICGO_OPEN_FILE, file)\n}\n\nfunction goShortCutPage () {\n  $router.push({\n    name: SHORTKEY_PAGE\n  })\n}\n\nfunction goUrlRewritePage () {\n  $router.push({\n    name: URL_REWRITE_PAGE\n  })\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'ButtonAreaSettings'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/pages/components/settings/buttonArea/CheckUpdateDialog.vue",
    "content": "<template>\n  <el-dialog\n    v-model=\"dialogVisible\"\n    :title=\"$T('SETTINGS_CHECK_UPDATE')\"\n    :append-to-body=\"true\"\n  >\n    <div>\n      {{ $T('SETTINGS_CURRENT_VERSION') }}: {{ version }}\n    </div>\n    <div>\n      {{ $T('SETTINGS_NEWEST_VERSION') }}: {{ latestVersion ? latestVersion : `${$T('SETTINGS_GETING')}...` }}\n    </div>\n    <div v-if=\"needUpdate\">\n      {{ $T('SETTINGS_TIPS_HAS_NEW_VERSION') }}\n    </div>\n    <template #footer>\n      <el-button\n        size=\"default\"\n        round\n        @click=\"cancelCheckVersion\"\n      >\n        {{ $T('CANCEL') }}\n      </el-button>\n      <el-button\n        size=\"default\"\n        type=\"primary\"\n        round\n        @click=\"confirmCheckVersion\"\n      >\n        {{ $T('CONFIRM') }}\n      </el-button>\n    </template>\n  </el-dialog>\n</template>\n<script lang=\"ts\" setup>\nimport { computed, ref, watch } from 'vue'\nimport { T as $T } from '@/i18n'\nimport { STABLE_RELEASE_URL, BETA_RELEASE_URL } from '#/utils/static'\nimport pkg from 'root/package.json'\nimport { compare } from 'compare-versions'\nimport { IRPCActionType } from '~/universal/types/enum'\nimport { useVModel } from '@/hooks/useVModel'\nimport { invokeRPC } from '@/utils/dataSender'\nimport { openURL } from '@/utils/common'\n\ninterface IProps {\n  modelValue: boolean\n  checkBetaUpdate: boolean\n}\n\nconst props = defineProps<IProps>()\n\nconst dialogVisible = useVModel(props, 'modelValue')\nconst checkBetaUpdate = useVModel(props, 'checkBetaUpdate')\n\nconst version = pkg.version\nconst latestVersion = ref('')\n\nfunction compareVersion2Update (current: string, latest: string): boolean {\n  return compare(current, latest, '<')\n}\n\nconst needUpdate = computed(() => {\n  if (latestVersion.value) {\n    return compareVersion2Update(version, latestVersion.value)\n  } else {\n    return false\n  }\n})\n\nwatch(() => dialogVisible.value, (value) => {\n  if (value) {\n    checkUpdate()\n  }\n})\n\nasync function checkUpdate () {\n  const res = await invokeRPC<string>(IRPCActionType.GET_LATEST_VERSION, checkBetaUpdate.value)\n  if (!res.success) {\n    latestVersion.value = res.error || $T('TIPS_NETWORK_ERROR')\n    return\n  }\n  latestVersion.value = res.data\n}\n\nfunction confirmCheckVersion () {\n  if (needUpdate.value) {\n    openURL(checkBetaUpdate.value ? BETA_RELEASE_URL : STABLE_RELEASE_URL)\n  }\n  dialogVisible.value = false\n}\n\nfunction cancelCheckVersion () {\n  latestVersion.value = ''\n  dialogVisible.value = false\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'CheckUpdateDialog'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/pages/components/settings/buttonArea/CustomLinkDialog.vue",
    "content": "<template>\n  <el-dialog\n    v-model=\"dialogVisible\"\n    :title=\"$T('SETTINGS_CUSTOM_LINK_FORMAT')\"\n    :append-to-body=\"true\"\n  >\n    <el-form\n      ref=\"$customLink\"\n      label-position=\"top\"\n      :model=\"form\"\n      :rules=\"rules\"\n      size=\"small\"\n    >\n      <el-form-item\n        prop=\"customLink\"\n      >\n        <div class=\"flex flex-col mb-[8px]\">\n          <div>\n            {{ $T('SETTINGS_TIPS_PLACEHOLDER_URL') }}\n          </div>\n          <div>\n            {{ $T('SETTINGS_TIPS_PLACEHOLDER_FILENAME') }}\n          </div>\n          <div>\n            {{ $T('SETTINGS_TIPS_PLACEHOLDER_EXTNAME') }}\n          </div>\n        </div>\n        <el-input\n          v-model=\"form.customLink\"\n          class=\"align-center\"\n          :autofocus=\"true\"\n          size=\"default\"\n        />\n      </el-form-item>\n    </el-form>\n    <div>\n      {{ $T('SETTINGS_TIPS_SUCH_AS') }}[$fileName]($url)\n    </div>\n    <template #footer>\n      <el-button\n        size=\"default\"\n        round\n        @click=\"cancelCustomLink\"\n      >\n        {{ $T('CANCEL') }}\n      </el-button>\n      <el-button\n        size=\"default\"\n        type=\"primary\"\n        round\n        @click=\"confirmCustomLink\"\n      >\n        {{ $T('CONFIRM') }}\n      </el-button>\n    </template>\n  </el-dialog>\n</template>\n<script lang=\"ts\" setup>\nimport { reactive, ref } from 'vue'\nimport { T as $T } from '@/i18n'\nimport { useVModel } from '@/hooks/useVModel'\nimport { saveConfig, sendToMain } from '@/utils/dataSender'\nimport { ElForm, FormRules } from 'element-plus'\nimport { useVModelValues } from '@/hooks/useVModelValues'\n\ninterface IProps {\n  modelValue: boolean\n  customLink: string\n}\n\nconst props = defineProps<IProps>()\n\nconst [form, updateProps] = useVModelValues<IProps>(props, ['customLink'])\n\nconst $customLink = ref<InstanceType<typeof ElForm> | null>(null)\n\nconst dialogVisible = useVModel(props, 'modelValue')\n\nconst customLinkRule = (rule: any, value: string, callback: (arg0?: Error) => void) => {\n  if (!/\\$url/.test(value) && !/\\$fileName/.test(value) && !/\\$extName/.test(value)) {\n    return callback(new Error($T('TIPS_MUST_CONTAINS_URL')))\n  } else {\n    return callback()\n  }\n}\n\nconst rules = reactive<FormRules>({\n  value: [\n    { validator: customLinkRule, trigger: 'blur' }\n  ]\n})\n\nfunction confirmCustomLink () {\n  $customLink.value?.validate((valid: boolean) => {\n    if (valid) {\n      saveConfig('settings.customLink', form.customLink)\n      dialogVisible.value = false\n      sendToMain('updateCustomLink')\n      updateProps()\n    }\n  })\n}\n\nasync function cancelCustomLink () {\n  dialogVisible.value = false\n  form.customLink = props.customLink\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'CustomLinkDialog'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/pages/components/settings/buttonArea/LogSettingDialog.vue",
    "content": "<template>\n  <el-dialog\n    v-model=\"dialogVisible\"\n    :title=\"$T('SETTINGS_SET_LOG_FILE')\"\n    :append-to-body=\"true\"\n    width=\"500px\"\n  >\n    <el-form\n      label-position=\"right\"\n      label-width=\"150px\"\n    >\n      <el-form-item\n        :label=\"$T('SETTINGS_LOG_FILE')\"\n      >\n        <el-button\n          type=\"primary\"\n          round\n          size=\"small\"\n          @click=\"openFile('picgo.log')\"\n        >\n          {{ $T('SETTINGS_CLICK_TO_OPEN') }}\n        </el-button>\n      </el-form-item>\n      <el-form-item\n        :label=\"$T('SETTINGS_LOG_LEVEL')\"\n      >\n        <el-select\n          v-model=\"form.logLevel\"\n          multiple\n          collapse-tags\n          style=\"width: 100%;\"\n        >\n          <el-option\n            v-for=\"(value, key) of logLevelMap\"\n            :key=\"key\"\n            :label=\"value\"\n            :value=\"key\"\n            :disabled=\"handleLevelDisabled(key)\"\n          />\n        </el-select>\n      </el-form-item>\n      <el-form-item\n        :label=\"`${$T('SETTINGS_LOG_FILE_SIZE')} (MB)`\"\n      >\n        <el-input-number\n          v-model=\"form.logFileSizeLimit\"\n          style=\"width: 100%;\"\n          :placeholder=\"`${$T('SETTINGS_TIPS_SUCH_AS')}：10`\"\n          :controls=\"false\"\n          :min=\"1\"\n        />\n      </el-form-item>\n    </el-form>\n    <template #footer>\n      <el-button\n        size=\"default\"\n        round\n        @click=\"cancelLogLevelSetting\"\n      >\n        {{ $T('CANCEL') }}\n      </el-button>\n      <el-button\n        size=\"default\"\n        type=\"primary\"\n        round\n        @click=\"confirmLogLevelSetting\"\n      >\n        {{ $T('CONFIRM') }}\n      </el-button>\n    </template>\n  </el-dialog>\n</template>\n<script lang=\"ts\" setup>\nimport { T as $T } from '@/i18n'\nimport { saveConfig } from '@/utils/dataSender'\nimport { useVModel } from '@/hooks/useVModel'\nimport { openFile } from '@/utils/common'\nimport { showNotification } from '@/utils/notification'\nimport { ElMessage as $message } from 'element-plus'\nimport { useVModelValues } from '@/hooks/useVModelValues'\n\nconst logLevelMap = {\n  all: $T('SETTINGS_LOG_LEVEL_ALL'),\n  success: $T('SETTINGS_LOG_LEVEL_SUCCESS'),\n  error: $T('SETTINGS_LOG_LEVEL_ERROR'),\n  info: $T('SETTINGS_LOG_LEVEL_INFO'),\n  warn: $T('SETTINGS_LOG_LEVEL_WARN'),\n  none: $T('SETTINGS_LOG_LEVEL_NONE')\n}\n\ninterface IProps {\n  modelValue: boolean\n  logLevel: string[]\n  logFileSizeLimit: number\n}\n\nconst props = defineProps<IProps>()\n\nconst dialogVisible = useVModel(props, 'modelValue')\n\nconst [form, updateProps] = useVModelValues<IProps>(props, ['logFileSizeLimit', 'logLevel'])\n\nasync function cancelLogLevelSetting () {\n  dialogVisible.value = false\n  let logLevel = props.logLevel as string | string[]\n  if (!Array.isArray(logLevel)) {\n    if (logLevel && logLevel.length > 0) {\n      logLevel = [logLevel]\n    } else {\n      logLevel = ['all']\n    }\n  }\n  form.logLevel = logLevel\n  form.logFileSizeLimit = props.logFileSizeLimit\n}\n\nfunction confirmLogLevelSetting () {\n  if (form.logLevel.length === 0) {\n    return $message.error($T('TIPS_PLEASE_CHOOSE_LOG_LEVEL'))\n  }\n  saveConfig({\n    'settings.logLevel': form.logLevel,\n    'settings.logFileSizeLimit': form.logFileSizeLimit\n  })\n  showNotification({\n    title: $T('SETTINGS_SET_LOG_FILE'),\n    body: $T('TIPS_SET_SUCCEED')\n  })\n  updateProps()\n  dialogVisible.value = false\n}\n\nfunction handleLevelDisabled (val: string) {\n  const currentLevel = val\n  let flagLevel\n  const result = form.logLevel.some((item: any) => {\n    if (item === 'all' || item === 'none') {\n      flagLevel = item\n    }\n    return (item === 'all' || item === 'none')\n  })\n  if (result) {\n    if (currentLevel !== flagLevel) {\n      return true\n    }\n  } else if (form.logLevel.length > 0) {\n    if (val === 'all' || val === 'none') {\n      return true\n    }\n  }\n  return false\n}\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'LogSettingDialog'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/pages/components/settings/buttonArea/ProxySettingDialog.vue",
    "content": "<template>\n  <el-dialog\n    v-model=\"dialogVisible\"\n    :title=\"$T('SETTINGS_SET_PROXY_AND_MIRROR')\"\n    :append-to-body=\"true\"\n    width=\"70%\"\n  >\n    <el-form\n      label-position=\"right\"\n      label-width=\"120px\"\n    >\n      <el-form-item\n        :label=\"$T('SETTINGS_UPLOAD_PROXY')\"\n      >\n        <el-input\n          v-model=\"form.proxy\"\n          :autofocus=\"true\"\n          :placeholder=\"`${$T('SETTINGS_TIPS_SUCH_AS')}：http://127.0.0.1:1080`\"\n        />\n      </el-form-item>\n      <el-form-item\n        :label=\"$T('SETTINGS_PLUGIN_INSTALL_PROXY')\"\n      >\n        <el-input\n          v-model=\"form.npmProxy\"\n          :autofocus=\"true\"\n          :placeholder=\"`${$T('SETTINGS_TIPS_SUCH_AS')}：http://127.0.0.1:1080`\"\n        />\n      </el-form-item>\n      <el-form-item\n        :label=\"$T('SETTINGS_PLUGIN_INSTALL_MIRROR')\"\n      >\n        <el-input\n          v-model=\"form.npmRegistry\"\n          :autofocus=\"true\"\n          :placeholder=\"`${$T('SETTINGS_TIPS_SUCH_AS')}：https://registry.npmmirror.com`\"\n        />\n      </el-form-item>\n    </el-form>\n    <template #footer>\n      <el-button\n        size=\"default\"\n        round\n        @click=\"cancelProxy\"\n      >\n        {{ $T('CANCEL') }}\n      </el-button>\n      <el-button\n        size=\"default\"\n        type=\"primary\"\n        round\n        @click=\"confirmProxy\"\n      >\n        {{ $T('CONFIRM') }}\n      </el-button>\n    </template>\n  </el-dialog>\n</template>\n<script lang=\"ts\" setup>\nimport { T as $T } from '@/i18n'\nimport { useVModel } from '@/hooks/useVModel'\nimport { saveConfig } from '@/utils/dataSender'\nimport { useVModelValues } from '@/hooks/useVModelValues'\nimport { showNotification } from '@/utils/notification'\n\ninterface IProps {\n  modelValue: boolean\n  proxy: string\n  npmProxy: string\n  npmRegistry: string\n}\nconst props = defineProps<IProps>()\n\nconst [form, updateProps] = useVModelValues<IProps>(props, ['proxy', 'npmProxy', 'npmRegistry'])\n\nconst dialogVisible = useVModel(props, 'modelValue')\n\nfunction confirmProxy () {\n  dialogVisible.value = false\n  saveConfig({\n    'picBed.proxy': form.proxy,\n    'settings.npmProxy': form.npmProxy,\n    'settings.npmRegistry': form.npmRegistry\n  })\n  showNotification({\n    title: $T('SETTINGS_SET_PROXY_AND_MIRROR'),\n    body: $T('TIPS_SET_SUCCEED')\n  })\n  updateProps()\n}\n\nasync function cancelProxy () {\n  dialogVisible.value = false\n  form.proxy = props.proxy || ''\n  form.npmProxy = props.npmProxy || ''\n  form.npmRegistry = props.npmRegistry || ''\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'ProxySettingDialog'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/pages/components/settings/buttonArea/ServerSettingsDialog.vue",
    "content": "<template>\n  <el-dialog\n    v-model=\"dialogVisible\"\n    class=\"server-dialog\"\n    width=\"60%\"\n    :title=\"$T('SETTINGS_SET_PICGO_SERVER')\"\n    :append-to-body=\"true\"\n  >\n    <div class=\"notice-text\">\n      {{ $T('SETTINGS_TIPS_SERVER_NOTICE') }}\n    </div>\n    <el-form\n      label-position=\"right\"\n      label-width=\"120px\"\n    >\n      <el-form-item\n        :label=\"$T('SETTINGS_ENABLE_SERVER')\"\n      >\n        <el-switch\n          v-model=\"form.enable\"\n          :active-text=\"$T('SETTINGS_OPEN')\"\n          :inactive-text=\"$T('SETTINGS_CLOSE')\"\n        />\n      </el-form-item>\n      <template v-if=\"form.enable\">\n        <el-form-item\n          :label=\"$T('SETTINGS_SET_SERVER_HOST')\"\n        >\n          <el-input\n            v-model=\"form.host\"\n            type=\"input\"\n            :placeholder=\"$T('SETTINGS_TIP_PLACEHOLDER_HOST')\"\n          />\n        </el-form-item>\n        <el-form-item\n          :label=\"$T('SETTINGS_SET_SERVER_PORT')\"\n        >\n          <el-input\n            v-model=\"form.port\"\n            type=\"number\"\n            :placeholder=\"$T('SETTINGS_TIP_PLACEHOLDER_PORT')\"\n          />\n        </el-form-item>\n      </template>\n    </el-form>\n    <template #footer>\n      <el-button\n        size=\"default\"\n        round\n        @click=\"cancelServerSetting\"\n      >\n        {{ $T('CANCEL') }}\n      </el-button>\n      <el-button\n        size=\"default\"\n        type=\"primary\"\n        round\n        @click=\"confirmServerSetting\"\n      >\n        {{ $T('CONFIRM') }}\n      </el-button>\n    </template>\n  </el-dialog>\n</template>\n<script lang=\"ts\" setup>\nimport { T as $T } from '@/i18n'\nimport { useVModel } from '@/hooks/useVModel'\nimport { useVModelValues } from '@/hooks/useVModelValues'\nimport { saveConfig, sendToMain } from '@/utils/dataSender'\nimport { showNotification } from '@/utils/notification'\n\ninterface IProps {\n  modelValue: boolean\n  port: number\n  host: string\n  enable: boolean\n}\n\nconst props = defineProps<IProps>()\n\nconst dialogVisible = useVModel(props, 'modelValue')\nconst [form, updateProps] = useVModelValues<IProps>(props, ['port', 'host', 'enable'])\n\nfunction confirmServerSetting () {\n  form.port = parseInt(form.port as unknown as string, 10)\n  saveConfig({\n    'settings.server': form\n  })\n  showNotification({\n    title: $T('SETTINGS_SET_PICGO_SERVER'),\n    body: $T('TIPS_SET_SUCCEED')\n  })\n  dialogVisible.value = false\n  sendToMain('updateServer')\n  updateProps()\n}\n\nasync function cancelServerSetting () {\n  dialogVisible.value = false\n  form.port = props.port\n  form.host = props.host\n  form.enable = props.enable\n}\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'ServerSettingsDialog'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/pages/components/settings/customArea/ChoosePicBed.vue",
    "content": "<template>\n  <el-form-item\n    :style=\"{ marginRight: '-64px' }\"\n    :label=\"$T('CHOOSE_SHOWED_PICBED')\"\n  >\n    <el-checkbox-group\n      v-model=\"showPicBedList\"\n      @change=\"handleShowPicBedListChange\"\n    >\n      <el-checkbox\n        v-for=\"item in picBed\"\n        :key=\"item.name\"\n        :label=\"item.name\"\n      />\n    </el-checkbox-group>\n  </el-form-item>\n</template>\n<script lang=\"ts\" setup>\nimport { onBeforeMount, ref } from 'vue'\nimport { T as $T } from '@/i18n'\nimport { GET_PICBEDS } from '#/events/constants'\nimport { saveConfig, sendToMain } from '@/utils/dataSender'\nimport { useIPCOn } from '@/hooks/useIPC'\nimport { useVModel } from '@/hooks/useVModel'\nimport { IpcRendererEvent } from 'electron'\n\ninterface IProps {\n  showPicBedList: string[]\n}\nconst props = defineProps<IProps>()\n\nconst picBed = ref<IPicBedType[]>([])\nconst showPicBedList = useVModel(props, 'showPicBedList')\n\nfunction handleShowPicBedListChange (val: ISwitchValueType[]) {\n  const list = picBed.value.map(item => {\n    if (!val.includes(item.name)) {\n      item.visible = false\n    } else {\n      item.visible = true\n    }\n    return item\n  })\n  saveConfig({\n    'picBed.list': list\n  })\n  sendToMain(GET_PICBEDS)\n}\n\nonBeforeMount(() => {\n  sendToMain(GET_PICBEDS)\n  useIPCOn(GET_PICBEDS, getPicBeds)\n})\n\nfunction getPicBeds (event: IpcRendererEvent, picBeds: IPicBedType[]) {\n  picBed.value = picBeds\n  showPicBedList.value = picBed.value.map(item => {\n    if (item.visible) {\n      return item.name\n    }\n    return null\n  }).filter(item => item) as string[]\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'ChoosePicBed'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/pages/components/settings/customArea/CustomAreaSettings.vue",
    "content": "<template>\n  <ChoosePicBed\n    v-model:showPicBedList=\"form.showPicBedList\"\n  />\n</template>\n<script lang=\"ts\" setup>\nimport { reactive } from 'vue'\nimport ChoosePicBed from './ChoosePicBed.vue'\n\ninterface IProps {\n  settings: ISettingForm\n}\nconst props = defineProps<IProps>()\nconst form = reactive(props.settings)\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'CustomAreaSettings'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/pages/components/settings/selectArea/SelectAreaSettings.vue",
    "content": "<template>\n  <SelectFormItem\n    v-model=\"form.language\"\n    :list=\"languageList\"\n    :label=\"$T('SETTINGS_CHOOSE_LANGUAGE')\"\n    :placeholder=\"$T('SETTINGS_CHOOSE_LANGUAGE')\"\n    @change=\"handleLanguageChange\"\n  />\n  <SelectFormItem\n    v-model=\"form.startupMode\"\n    :list=\"startupModeList\"\n    :label=\"$T('SETTINGS_STARTUP_MODE')\"\n    @change=\"handleChangeStartupMode\"\n  />\n</template>\n<script lang=\"ts\" setup>\nimport { reactive } from 'vue'\nimport { T as $T, i18nManager } from '@/i18n'\nimport { saveConfig, sendToMain } from '@/utils/dataSender'\nimport { GET_PICBEDS } from '~/universal/events/constants'\nimport SelectFormItem from '@/components/settings/SelectFormItem.vue'\nimport { IStartupMode } from '~/universal/types/enum'\nimport { isMacOS } from '~/universal/utils/common'\n\ninterface IProps {\n  settings: ISettingForm\n}\nconst props = defineProps<IProps>()\n\nconst form = reactive(props.settings)\n\nconst languageList = i18nManager.languageList.map(item => ({\n  label: item.label,\n  value: item.value\n}))\n\nconst startupModeList = [\n  {\n    label: $T('SETTINGS_STARTUP_MODE_MAIN_WINDOW'),\n    value: IStartupMode.SHOW_MAIN_WINDOW\n  },\n  {\n    label: $T('SETTINGS_STARTUP_MODE_MINI_WINDOW'),\n    value: IStartupMode.SHOW_MINI_WINDOW,\n    hide: isMacOS\n  },\n  {\n    label: $T('SETTINGS_STARTUP_MODE_HIDE'),\n    value: IStartupMode.HIDE\n  }\n].filter(item => !item.hide)\n\nfunction handleLanguageChange (val: string) {\n  i18nManager.setCurrentLanguage(val)\n  saveConfig({\n    'settings.language': val\n  })\n  sendToMain(GET_PICBEDS)\n}\n\nfunction handleChangeStartupMode (val: IStartupMode) {\n  saveConfig({\n    'settings.startupMode': val\n  })\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'SelectAreaSettings'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/pages/components/settings/switchArea/SwitchAreaSettings.vue",
    "content": "<template>\n  <SwitchFormItem\n    v-model=\"form.showUpdateTip\"\n    setting-props=\"showUpdateTip\"\n    :label=\"$T('SETTINGS_OPEN_UPDATE_HELPER')\"\n  />\n  <SwitchFormItem\n    v-show=\"form.showUpdateTip\"\n    v-model=\"form.checkBetaUpdate\"\n    setting-props=\"checkBetaUpdate\"\n    :label=\"$T('SETTINGS_ACCEPT_BETA_UPDATE')\"\n  />\n  <SwitchFormItem\n    v-model=\"form.autoStart\"\n    setting-props=\"autoStart\"\n    :label=\"$T('SETTINGS_LAUNCH_ON_BOOT')\"\n    @change=\"handleAutoStartChange\"\n  />\n  <SwitchFormItem\n    v-model=\"form.rename\"\n    setting-props=\"rename\"\n    :label=\"$T('SETTINGS_RENAME_BEFORE_UPLOAD')\"\n  />\n  <SwitchFormItem\n    v-model=\"form.autoRename\"\n    setting-props=\"autoRename\"\n    :label=\"$T('SETTINGS_TIMESTAMP_RENAME')\"\n  />\n  <SwitchFormItem\n    v-model=\"form.uploadNotification\"\n    setting-props=\"uploadNotification\"\n    :label=\"$T('SETTINGS_OPEN_UPLOAD_TIPS')\"\n  />\n  <SwitchFormItem\n    v-model=\"form.notificationSound\"\n    setting-props=\"notificationSound\"\n    :label=\"$T('SETTINGS_NOTIFICATION_SOUND')\"\n  />\n  <SwitchFormItem\n    v-if=\"os !== 'darwin'\"\n    v-model=\"form.miniWindowOnTop\"\n    :label=\"$T('SETTINGS_MINI_WINDOW_ON_TOP')\"\n    setting-props=\"miniWindowOnTop\"\n    @change=\"handleMiniWindowOnTop\"\n  />\n  <SwitchFormItem\n    v-model=\"form.autoCopyUrl\"\n    :label=\"$T('SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD')\"\n    setting-props=\"autoCopyUrl\"\n  />\n  <SwitchFormItem\n    v-model=\"form.useBuiltinClipboard\"\n    :label=\"$T('SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD')\"\n    :tooltips=\"$T('BUILTIN_CLIPBOARD_TIPS')\"\n    setting-props=\"useBuiltinClipboard\"\n  />\n  <SwitchFormItem\n    v-model=\"form.encodeOutputURL\"\n    :label=\"$T('SETTINGS_ENCODE_OUTPUT_URL')\"\n    setting-props=\"encodeOutputURL\"\n  />\n  <SwitchFormItem\n    v-if=\"os === 'darwin'\"\n    v-model=\"form.showDockIcon\"\n    setting-props=\"showDockIcon\"\n    :label=\"$T('SETTINGS_SHOW_DOCK_ICON')\"\n    @change=\"handleShowDockIcon\"\n  />\n  <SwitchFormItem\n    v-if=\"os === 'darwin'\"\n    v-model=\"form.showMenubarIcon\"\n    setting-props=\"showMenubarIcon\"\n    :label=\"$T('SETTINGS_SHOW_MENUBAR_ICON')\"\n    :tooltips=\"$T('SETTINGS_SHOW_MENUBAR_ICON_TIPS')\"\n    @change=\"handleShowMenubarIcon\"\n  />\n</template>\n<script lang=\"ts\" setup>\nimport { reactive } from 'vue'\nimport { T as $T } from '@/i18n/index'\nimport { ElMessage as $message } from 'element-plus'\nimport { sendRPC, sendToMain } from '@/utils/dataSender'\nimport { IRPCActionType } from '~/universal/types/enum'\nimport SwitchFormItem from '@/components/settings/SwitchFormItem.vue'\nimport { useOS } from '@/hooks/useOS'\n\nconst os = useOS()\n\ninterface IProps {\n  settings: ISettingForm\n}\nconst props = defineProps<IProps>()\n\nconst form = reactive(props.settings)\n\nfunction handleAutoStartChange (val: ISwitchValueType) {\n  sendToMain('autoStart', val)\n}\n\nfunction handleMiniWindowOnTop () {\n  $message.info($T('TIPS_NEED_RELOAD'))\n}\n\nfunction handleShowDockIcon (val: ISwitchValueType) {\n  sendRPC(IRPCActionType.SHOW_DOCK_ICON, val)\n}\n\nfunction handleShowMenubarIcon (val: ISwitchValueType) {\n  sendRPC(IRPCActionType.SHOW_MENUBAR_ICON, val)\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: 'SwitchAreaSettings'\n}\n</script>\n<style lang='stylus'>\n</style>\n"
  },
  {
    "path": "src/renderer/pages/picbeds/index.vue",
    "content": "<template>\n  <div id=\"picbeds-page\">\n    <el-row\n      :gutter=\"20\"\n      class=\"setting-list\"\n    >\n      <el-col\n        :span=\"20\"\n        :offset=\"2\"\n      >\n        <div class=\"view-title\">\n          {{ picBedName }} {{ $T('SETTINGS') }}\n        </div>\n        <config-form\n          v-if=\"config.length > 0\"\n          :id=\"type\"\n          ref=\"$configForm\"\n          :config=\"config\"\n          type=\"uploader\"\n        >\n          <el-form-item>\n            <el-button\n              class=\"confirm-btn\"\n              type=\"primary\"\n              round\n              @click=\"handleConfirm\"\n            >\n              {{ $T('CONFIRM') }}\n            </el-button>\n          </el-form-item>\n        </config-form>\n        <div\n          v-else\n          class=\"single\"\n        >\n          <div class=\"notice\">\n            {{ $T('SETTINGS_NOT_CONFIG_OPTIONS') }}\n          </div>\n        </div>\n      </el-col>\n    </el-row>\n  </div>\n</template>\n<script lang=\"ts\" setup>\nimport { IRPCActionType } from '~/universal/types/enum'\nimport { computed, ref, onBeforeMount, watch } from 'vue'\nimport { T as $T } from '@/i18n/index'\nimport { sendToMain, invokeRPC } from '@/utils/dataSender'\nimport { showNotification } from '@/utils/notification'\nimport { useRoute, useRouter } from 'vue-router'\nimport ConfigForm from '@/components/ConfigForm.vue'\nimport { ElMessage } from 'element-plus'\n// import mixin from '@/utils/ConfirmButtonMixin'\nimport {\n  IpcRendererEvent\n} from 'electron'\nimport { GET_PICBED_CONFIG } from '~/universal/events/constants'\nimport { useIPCOn } from '@/hooks/useIPC'\nimport { useStore } from '@/hooks/useStore'\nimport { PICBEDS_PAGE } from '@/router/config'\nconst type = ref('')\nconst config = ref<IPicGoPluginConfig[]>([])\nconst picBedName = ref('')\nconst $route = useRoute()\nconst $router = useRouter()\nconst $configForm = ref<InstanceType<typeof ConfigForm> | null>(null)\nconst store = useStore()\nconst appConfig = computed(() => store?.state.appConfig ?? null)\ntype.value = $route.params.type as string\n\nuseIPCOn(GET_PICBED_CONFIG, getPicBeds)\n\nonBeforeMount(() => {\n  sendToMain(GET_PICBED_CONFIG, $route.params.type)\n  store?.refreshAppConfig()\n})\n\nconst handleConfirm = async () => {\n  const result = (await $configForm.value?.validate()) || false\n  if (result !== false) {\n    const configId = ($route.params.configId as string) || ''\n    const res = await invokeRPC<boolean>(IRPCActionType.UPDATE_UPLOADER_CONFIG, type.value, configId, result)\n    if (!res.success) {\n      const message = res.error\n      ElMessage.warning(message)\n      return\n    }\n    showNotification({\n      title: $T('SETTINGS_RESULT'),\n      body: $T('TIPS_SET_SUCCEED')\n    })\n    $router.back()\n  }\n}\n\nfunction getPicBeds (event: IpcRendererEvent, _config: IPicGoPluginConfig[], name: string) {\n  config.value = _config\n  picBedName.value = name\n}\n\n</script>\n<script lang=\"ts\">\nexport default {\n  name: PICBEDS_PAGE\n}\n</script>\n<style lang='stylus'>\n#picbeds-page\n  .setting-list\n    height 425px\n    overflow-y auto\n    overflow-x hidden\n  .confirm-btn\n    width: 250px\n  .el-form\n    label\n      line-height 22px\n      padding-bottom 0\n      color #eee\n    &-item\n      margin-bottom 16px\n    .el-button-group\n      width 100%\n      .el-button\n        width 50%\n    .el-radio-group\n      margin-left 25px\n    .el-switch__label\n      color #eee\n      &.is-active\n        color #409EFF\n  .notice\n    color #eee\n    text-align center\n    margin-bottom 10px\n  .single\n    text-align center\n</style>\n"
  },
  {
    "path": "src/renderer/router/config.ts",
    "content": "export const GALLERY_PAGE = 'GalleryPage'\nexport const TRAY_PAGE = 'TrayPage'\nexport const RENAME_PAGE = 'RenamePage'\nexport const MINI_PAGE = 'MiniPage'\nexport const MAIN_PAGE = 'MainPage'\nexport const UPLOAD_PAGE = 'UploadPage'\nexport const PICBEDS_PAGE = 'PicbedsPage'\nexport const SETTING_PAGE = 'SettingPage'\nexport const PLUGIN_PAGE = 'PluginPage'\nexport const SHORTKEY_PAGE = 'ShortkeyPage'\nexport const URL_REWRITE_PAGE = 'UrlRewritePage'\nexport const UPLOADER_CONFIG_PAGE = 'UploaderConfigPage'\nexport const PICGO_CLOUD_PAGE = 'PicGoCloudPage'\nexport const TOOLBOX_CONFIG_PAGE = 'ToolBoxPage'\n"
  },
  {
    "path": "src/renderer/router/index.ts",
    "content": "import { createRouter, createWebHashHistory } from 'vue-router'\nimport * as config from './config'\n\nexport default createRouter({\n  history: createWebHashHistory(),\n  routes: [\n    {\n      path: '/',\n      name: config.TRAY_PAGE,\n      component: () => import(/* webpackChunkName: \"tray\" */ '@/pages/TrayPage.vue')\n    },\n    {\n      path: '/rename-page',\n      name: config.RENAME_PAGE,\n      component: () => import(/* webpackChunkName: \"RenamePage\" */ '@/pages/RenamePage.vue')\n    },\n    {\n      path: '/mini-page',\n      name: config.MINI_PAGE,\n      component: () => import(/* webpackChunkName: \"MiniPage\" */ '@/pages/MiniPage.vue')\n    },\n    {\n      path: '/main-page',\n      name: config.MAIN_PAGE,\n      component: () => import(/* webpackChunkName: \"SettingPage\" */ '@/layouts/Main.vue'),\n      children: [\n        {\n          path: 'upload',\n          component: () => import(/* webpackChunkName: \"Upload\" */ '@/pages/Upload.vue'),\n          name: config.UPLOAD_PAGE\n        },\n        {\n          path: 'picbeds/:type/:configId?',\n          component: () => import(/* webpackChunkName: \"Other\" */ '@/pages/picbeds/index.vue'),\n          name: config.PICBEDS_PAGE\n        },\n        {\n          path: 'gallery',\n          component: () => import(/* webpackChunkName: \"GalleryView\" */ '@/pages/Gallery.vue'),\n          name: config.GALLERY_PAGE,\n          meta: {\n            keepAlive: true\n          }\n        },\n        {\n          path: 'setting',\n          component: () => import(/* webpackChunkName: \"setting\" */ '@/pages/PicGoSetting.vue'),\n          name: config.SETTING_PAGE\n        },\n        {\n          path: 'plugin',\n          component: () => import(/* webpackChunkName: \"Plugin\" */ '@/pages/Plugin.vue'),\n          name: config.PLUGIN_PAGE\n        },\n        {\n          path: 'cloud',\n          component: () => import(/* webpackChunkName: \"PicGoCloud\" */ '@/pages/PicGoCloud.vue'),\n          name: config.PICGO_CLOUD_PAGE\n        },\n        {\n          path: 'shortKey',\n          component: () => import(/* webpackChunkName: \"ShortkeyPage\" */ '@/pages/ShortKey.vue'),\n          name: config.SHORTKEY_PAGE\n        },\n        {\n          path: 'urlRewrite',\n          component: () => import(/* webpackChunkName: \"UrlRewritePage\" */ '@/pages/UrlRewrite.vue'),\n          name: config.URL_REWRITE_PAGE\n        },\n        {\n          path: 'uploader-config-page/:type',\n          component: () => import(/* webpackChunkName: \"Other\" */ '@/pages/UploaderConfigPage.vue'),\n          name: config.UPLOADER_CONFIG_PAGE\n        }\n      ]\n    },\n    {\n      path: '/toolbox-page',\n      name: config.TOOLBOX_CONFIG_PAGE,\n      component: () => import(/* webpackChunkName: \"ToolboxPage\" */ '@/pages/Toolbox.vue')\n    },\n    {\n      path: '/:pathMatch(.*)*',\n      redirect: '/'\n    }\n  ]\n})\n"
  },
  {
    "path": "src/renderer/store/index.ts",
    "content": "import { reactive, InjectionKey, readonly, App, UnwrapRef, ref, type DeepReadonly } from 'vue'\nimport { getConfig, getPicBeds, saveConfig } from '@/utils/dataSender'\nimport type { IPicGoCloudUserInfo } from '#/types/cloud'\nimport type { IConfig } from 'picgo'\n\nexport enum IPicGoCloudRequestStatus {\n  IDLE = 'IDLE',\n  LOADING = 'LOADING',\n  ERROR = 'ERROR'\n}\n\nexport enum IPicGoCloudLoginStatus {\n  IDLE = 'IDLE',\n  IN_PROGRESS = 'IN_PROGRESS'\n}\n\nexport interface IPicGoCloudState {\n  /**\n   * PicGo Cloud auth tri-state:\n   * - undefined: not loaded yet (first entry triggers auto load)\n   * - null: loaded, but not logged in\n   * - { user }: logged in\n   */\n  userInfo: IPicGoCloudUserInfo | null | undefined;\n  userInfoStatus: IPicGoCloudRequestStatus;\n  userInfoError: string | null;\n  loginStatus: IPicGoCloudLoginStatus;\n  loginError: string | null;\n  /**\n   * Whether user has explicitly checked the acknowledgement before starting login\n   * in the current app session.\n   */\n  hasAgreedToTermsAndPrivacy: boolean;\n}\n\nexport interface IState {\n  defaultPicBed: string;\n  appConfig: IConfig | null;\n  picBeds: IPicBedType[];\n  picgoCloud: IPicGoCloudState;\n}\n\nexport interface IStore {\n  state: DeepReadonly<UnwrapRef<IState>>\n  setDefaultPicBed: (type: string) => void;\n  refreshAppConfig: () => Promise<void>;\n  refreshPicBeds: () => Promise<void>;\n  setPicGoCloudUserInfo: (userInfo: IPicGoCloudUserInfo | null | undefined) => void;\n  setPicGoCloudUserInfoStatus: (status: IPicGoCloudRequestStatus) => void;\n  setPicGoCloudUserInfoError: (error: string | null) => void;\n  setPicGoCloudLoginStatus: (status: IPicGoCloudLoginStatus) => void;\n  setPicGoCloudLoginError: (error: string | null) => void;\n  setPicGoCloudHasAgreedToTermsAndPrivacy: (hasAgreed: boolean) => void;\n  updateForceUpdateTime: () => void;\n}\n\nexport const storeKey: InjectionKey<IStore> = Symbol('store')\n\n// state\nconst state: IState = reactive({\n  defaultPicBed: 'smms',\n  appConfig: null,\n  picBeds: [],\n  picgoCloud: {\n    userInfo: undefined,\n    userInfoStatus: IPicGoCloudRequestStatus.IDLE,\n    userInfoError: null,\n    loginStatus: IPicGoCloudLoginStatus.IDLE,\n    loginError: null,\n    hasAgreedToTermsAndPrivacy: false\n  }\n})\n\nconst forceUpdateTime = ref<number>(Date.now())\n\n// methods\nconst setDefaultPicBed = (type: string) => {\n  saveConfig({\n    'picBed.current': type,\n    'picBed.uploader': type\n  })\n  state.defaultPicBed = type\n}\n\nconst setAppConfig = (config: IConfig | null) => {\n  state.appConfig = config\n  if (config) {\n    const picBed = config.picBed\n    state.defaultPicBed = picBed.uploader || picBed.current || 'smms'\n  }\n}\n\nconst refreshAppConfig = async (): Promise<void> => {\n  const config = await getConfig<IConfig>()\n  setAppConfig(config ?? null)\n}\n\nconst refreshPicBeds = async (): Promise<void> => {\n  const picBeds = await getPicBeds()\n  state.picBeds = picBeds\n}\n\nconst setPicGoCloudUserInfo = (userInfo: IPicGoCloudUserInfo | null | undefined) => {\n  state.picgoCloud.userInfo = userInfo\n}\n\nconst setPicGoCloudUserInfoStatus = (status: IPicGoCloudRequestStatus) => {\n  state.picgoCloud.userInfoStatus = status\n}\n\nconst setPicGoCloudUserInfoError = (error: string | null) => {\n  state.picgoCloud.userInfoError = error\n}\n\nconst setPicGoCloudLoginStatus = (status: IPicGoCloudLoginStatus) => {\n  state.picgoCloud.loginStatus = status\n}\n\nconst setPicGoCloudLoginError = (error: string | null) => {\n  state.picgoCloud.loginError = error\n}\n\nconst setPicGoCloudHasAgreedToTermsAndPrivacy = (hasAgreed: boolean) => {\n  state.picgoCloud.hasAgreedToTermsAndPrivacy = hasAgreed\n}\n\nconst updateForceUpdateTime = () => {\n  forceUpdateTime.value = Date.now()\n}\n\nexport const store = {\n  install (app: App) {\n    app.provide(storeKey, {\n      state: readonly(state),\n      setDefaultPicBed,\n      refreshAppConfig,\n      refreshPicBeds,\n      setPicGoCloudUserInfo,\n      setPicGoCloudUserInfoStatus,\n      setPicGoCloudUserInfoError,\n      setPicGoCloudLoginStatus,\n      setPicGoCloudLoginError,\n      setPicGoCloudHasAgreedToTermsAndPrivacy,\n      updateForceUpdateTime\n    })\n    app.provide('forceUpdateTime', forceUpdateTime)\n  }\n}\n"
  },
  {
    "path": "src/renderer/utils/LS.ts",
    "content": "class LS {\n  get (name: string) {\n    if (localStorage.getItem(name)) {\n      return JSON.parse(localStorage.getItem(name) as string)\n    } else {\n      return {}\n    }\n  }\n\n  set (name: string, value: any) {\n    return localStorage.setItem(name, JSON.stringify(value))\n  }\n}\n\nexport default new LS()\n"
  },
  {
    "path": "src/renderer/utils/analytics.ts",
    "content": "import {\n  REGISTER_DEVICE_ID,\n  TALKING_DATA_APPID, TALKING_DATA_DEVICE_ID_EVENT, TALKING_DATA_EVENT\n} from '~/universal/events/constants'\nimport pkg from 'root/package.json'\nimport { ipcRenderer } from 'electron'\nimport { handleTalkingDataEvent } from './common'\nconst { version } = pkg\n\nexport const initTalkingData = () => {\n  setTimeout(() => {\n    const talkingDataScript = document.createElement('script')\n\n    talkingDataScript.src = `https://jic.talkingdata.com/app/h5/v1?appid=${TALKING_DATA_APPID}&vn=${version}&vc=${version}`\n\n    const head = document.getElementsByTagName('head')[0]\n    head.appendChild(talkingDataScript)\n  }, 0)\n}\n\nipcRenderer.on(TALKING_DATA_EVENT, (_, data: ITalkingDataOptions) => {\n  handleTalkingDataEvent(data)\n})\n// 0：ANONYMOUS，匿名账号；\n// 1：REGISTERED，自有帐户显性注册；\nipcRenderer.on(TALKING_DATA_DEVICE_ID_EVENT, (_, deviceId: string) => {\n  window.TDAPP.register({\n    profileId: deviceId,\n    profileType: 1\n  })\n  window.TDAPP.login({\n    profileId: deviceId,\n    profileType: 1\n  })\n  ipcRenderer.send(REGISTER_DEVICE_ID)\n})\n"
  },
  {
    "path": "src/renderer/utils/bus.ts",
    "content": "import mitt from 'mitt'\nimport {\n  SHOW_INPUT_BOX,\n  SHOW_INPUT_BOX_RESPONSE,\n  FORCE_UPDATE\n} from '~/universal/events/constants'\n\ntype IEvent ={\n  [SHOW_INPUT_BOX_RESPONSE]: string\n  [SHOW_INPUT_BOX]: IShowInputBoxOption,\n  [FORCE_UPDATE]: void\n}\n\nconst emitter = mitt<IEvent>()\n\nexport default emitter\n"
  },
  {
    "path": "src/renderer/utils/common.ts",
    "content": "import { isReactive, isRef, toRaw, unref } from 'vue'\nimport { sendToMain } from './dataSender'\nimport { OPEN_URL, PICGO_OPEN_FILE } from '~/universal/events/constants'\nimport { webUtils } from 'electron'\n\nconst isDevelopment = process.env.NODE_ENV !== 'production'\nexport const handleTalkingDataEvent = (data: ITalkingDataOptions) => {\n  const { EventId, Label = '', MapKv = {} } = data\n  MapKv.from = window.location.href\n  window.TDAPP.onEvent(EventId, Label, MapKv)\n  if (isDevelopment) {\n    console.log('talkingData', data)\n  }\n}\n\nexport const trimValues = (obj: IStringKeyMap) => {\n  const newObj = {} as IStringKeyMap\n  Object.keys(obj).forEach(key => {\n    newObj[key] = typeof obj[key] === 'string' ? obj[key].trim() : obj[key]\n  })\n  return newObj\n}\n\n/**\n * get raw data from reactive or ref\n */\nexport const getRawData = (args: any): any => {\n  if (args === null) return args\n  if (Array.isArray(args)) {\n    const data = args.map((item: any) => {\n      if (isRef(item)) {\n        return getRawData(unref(item))\n      }\n      if (isReactive(item)) {\n        return getRawData(toRaw(item))\n      }\n      return getRawData(item)\n    })\n    return data\n  }\n  if (typeof args === 'object') {\n    const data = {} as IStringKeyMap\n    Object.keys(args).forEach(key => {\n      const item = args[key]\n      if (isRef(item)) {\n        data[key] = getRawData(unref(item))\n      } else if (isReactive(item)) {\n        data[key] = getRawData(toRaw(item))\n      } else {\n        data[key] = getRawData(item)\n      }\n    })\n    return data\n  }\n  return args\n}\n\nexport const openFile = (fileName: string) => {\n  sendToMain(PICGO_OPEN_FILE, fileName)\n}\n\nexport const openURL = (url: string) => {\n  sendToMain(OPEN_URL, url)\n}\n\nexport const getFilePath = (file: File) => {\n  return webUtils.getPathForFile(file)\n}\n"
  },
  {
    "path": "src/renderer/utils/dataSender.ts",
    "content": "import { GET_PICBEDS, PICGO_GET_CONFIG, PICGO_SAVE_CONFIG, RPC_ACTIONS } from '#/events/constants'\nimport { IpcRendererEvent, ipcRenderer } from 'electron'\nimport { v4 as uuid } from 'uuid'\nimport { IRPCActionType } from '~/universal/types/enum'\nimport { getRawData } from './common'\n\nexport async function saveConfig (_config: IObj | string, value?: any) {\n  let config\n  if (typeof _config === 'string') {\n    config = {\n      [_config]: getRawData(value)\n    }\n  } else {\n    config = getRawData(_config)\n  }\n  await ipcRenderer.invoke(PICGO_SAVE_CONFIG, config)\n}\n\nexport function getConfig<T> (key?: string): Promise<T | undefined> {\n  return new Promise((resolve) => {\n    const callbackId = uuid()\n    const callback = (event: IpcRendererEvent, config: T | undefined, returnCallbackId: string) => {\n      if (returnCallbackId === callbackId) {\n        resolve(config)\n        ipcRenderer.removeListener(PICGO_GET_CONFIG, callback)\n      }\n    }\n    ipcRenderer.on(PICGO_GET_CONFIG, callback)\n    ipcRenderer.send(PICGO_GET_CONFIG, key, callbackId)\n  })\n}\n\nexport function getPicBeds (): Promise<IPicBedType[]> {\n  return new Promise((resolve) => {\n    ipcRenderer.once(GET_PICBEDS, (_event: IpcRendererEvent, picBeds: IPicBedType[]) => {\n      resolve(picBeds)\n    })\n    ipcRenderer.send(GET_PICBEDS)\n  })\n}\n\n/**\n * Invoke an RPC action and await its return value.\n *\n * This uses `ipcRenderer.invoke(RPC_ACTIONS, action, args)` which is backed by\n * `ipcMain.handle(RPC_ACTIONS, ...)` in the main process RPC server.\n */\nexport function invokeRPC<T> (action: IRPCActionType, ...args: any[]): Promise<IRPCResult<T>> {\n  const data = getRawData(args)\n  return ipcRenderer.invoke(RPC_ACTIONS, action, data) as Promise<IRPCResult<T>>\n}\n\n/**\n * send a rpc request & do not need to wait for the response\n *\n * or the response will be handled by other listener\n */\nexport function sendRPC (action: IRPCActionType, ...args: any[]): void {\n  const data = getRawData(args)\n  ipcRenderer.send(RPC_ACTIONS, action, data)\n}\n\n/**\n * @deprecated will be replaced by sendRPC in the future\n */\nexport function sendToMain (channel: string, ...args: any[]) {\n  const data = getRawData(args)\n  ipcRenderer.send(channel, ...data)\n}\n"
  },
  {
    "path": "src/renderer/utils/db.ts",
    "content": "import {\n  PICGO_GET_BY_ID_DB,\n  PICGO_GET_DB,\n  PICGO_INSERT_DB,\n  PICGO_INSERT_MANY_DB,\n  PICGO_REMOVE_BY_ID_DB,\n  PICGO_UPDATE_BY_ID_DB\n} from '#/events/constants'\nimport { IGalleryDB } from '#/types/extra-vue'\nimport { IFilter, IGetResult, IObject, IResult } from '@picgo/store/dist/types'\nimport { IpcRendererEvent, ipcRenderer } from 'electron'\nimport { v4 as uuid } from 'uuid'\nimport { getRawData } from './common'\nexport class GalleryDB implements IGalleryDB {\n  async get<T> (filter?: IFilter): Promise<IGetResult<T>> {\n    const res = await this.msgHandler<IGetResult<T>>(PICGO_GET_DB, filter)\n    return res\n  }\n\n  async insert<T> (value: T): Promise<IResult<T>> {\n    const res = await this.msgHandler<IResult<T>>(PICGO_INSERT_DB, value)\n    return res\n  }\n\n  async insertMany<T> (value: T[]): Promise<IResult<T>[]> {\n    const res = await this.msgHandler<IResult<T>[]>(PICGO_INSERT_MANY_DB, value)\n    return res\n  }\n\n  async updateById (id: string, value: IObject): Promise<boolean> {\n    const res = await this.msgHandler<boolean>(PICGO_UPDATE_BY_ID_DB, id, value)\n    return res\n  }\n\n  async getById<T> (id: string): Promise<IResult<T> | undefined> {\n    const res = await this.msgHandler<IResult<T> | undefined>(PICGO_GET_BY_ID_DB, id)\n    return res\n  }\n\n  async removeById (id: string): Promise<void> {\n    const res = await this.msgHandler<void>(PICGO_REMOVE_BY_ID_DB, id)\n    return res\n  }\n\n  private msgHandler<T> (method: string, ...args: any[]): Promise<T> {\n    return new Promise((resolve) => {\n      const callbackId = uuid()\n      const callback = (event: IpcRendererEvent, data: T, returnCallbackId: string) => {\n        if (returnCallbackId === callbackId) {\n          resolve(data)\n          ipcRenderer.removeListener(method, callback)\n        }\n      }\n      const data = getRawData(args)\n      ipcRenderer.on(method, callback)\n      ipcRenderer.send(method, ...data, callbackId)\n    })\n  }\n}\n\nexport default new GalleryDB()\n"
  },
  {
    "path": "src/renderer/utils/key-binding.ts",
    "content": "import keycode from 'keycode'\n\nconst isSpecialKey = (keyCode: number) => {\n  const keyArr = [\n    16, // Shift\n    17, // Ctrl\n    18, // Alt\n    91, // Left Meta\n    93 // Right Meta\n  ]\n\n  return keyArr.includes(keyCode)\n}\n\nconst keyDetect = (event: KeyboardEvent) => {\n  // TODO: remove process\n  const meta = process.platform === 'darwin' ? 'Cmd' : 'Super'\n  const specialKey = {\n    Ctrl: event.ctrlKey,\n    Shift: event.shiftKey,\n    Alt: event.altKey,\n    [meta]: event.metaKey\n  }\n\n  const pressKey = []\n\n  for (const i in specialKey) {\n    if (specialKey[i]) {\n      pressKey.push(i)\n    }\n  }\n\n  if (!isSpecialKey(event.keyCode)) {\n    pressKey.push(keycode(event.keyCode).toUpperCase())\n  }\n  return pressKey\n}\n\nexport default keyDetect\n"
  },
  {
    "path": "src/renderer/utils/mainMixin.ts",
    "content": "import { ComponentOptions } from 'vue'\nimport { FORCE_UPDATE, GET_PICBEDS } from '~/universal/events/constants'\nimport bus from '~/renderer/utils/bus'\nimport { ipcRenderer } from 'electron'\nexport const mainMixin: ComponentOptions = {\n  inject: ['forceUpdateTime'],\n\n  created () {\n    // FIXME: may be memory leak\n    this?.$watch('forceUpdateTime', (newVal: number, oldVal: number) => {\n      if (oldVal !== newVal) {\n        this?.$forceUpdate()\n      }\n    })\n  },\n\n  methods: {\n    forceUpdate () {\n      bus.emit(FORCE_UPDATE)\n    },\n    getPicBeds () {\n      ipcRenderer.send(GET_PICBEDS)\n    }\n  }\n}\n"
  },
  {
    "path": "src/renderer/utils/mixin.ts",
    "content": "import { ComponentOptions } from 'vue'\nexport const dragMixin: ComponentOptions = {\n  mounted () {\n    this.disableDragEvent()\n  },\n\n  methods: {\n    disableDragEvent () {\n      window.addEventListener('dragenter', this.disableDrag, false)\n      window.addEventListener('dragover', this.disableDrag)\n      window.addEventListener('drop', this.disableDrag)\n    },\n\n    disableDrag (e: DragEvent) {\n      const dropzone = document.getElementById('upload-area')\n      if (dropzone === null || !dropzone.contains(<Node>e.target)) {\n        e.preventDefault()\n        e.dataTransfer!.effectAllowed = 'none'\n        e.dataTransfer!.dropEffect = 'none'\n      }\n    }\n  },\n\n  beforeUnmount () {\n    window.removeEventListener('dragenter', this.disableDrag, false)\n    window.removeEventListener('dragover', this.disableDrag)\n    window.removeEventListener('drop', this.disableDrag)\n  }\n}\n"
  },
  {
    "path": "src/renderer/utils/notification.ts",
    "content": "import { ipcRenderer } from 'electron'\nimport { v4 as uuid } from 'uuid'\nimport { sendRPC } from './dataSender'\nimport { PICGO_NOTIFICATION_CLICKED } from '~/universal/events/constants'\nimport { IRPCActionType } from '~/universal/types/enum'\n\nconst notificationCallbacks = new Map<string, () => void>()\nconst MAX_CALLBACK_LIMIT = 10\n\nconst handleNotificationClick = (_event: Electron.IpcRendererEvent, id: string) => {\n  const callback = notificationCallbacks.get(id)\n  if (!callback) return\n  try {\n    callback()\n  } finally {\n    notificationCallbacks.delete(id)\n  }\n}\n\n// HMR Protection: Remove existing listener before adding a new one\nipcRenderer.removeAllListeners(PICGO_NOTIFICATION_CLICKED)\nipcRenderer.on(PICGO_NOTIFICATION_CLICKED, handleNotificationClick)\n\ninterface NotificationOptions {\n  title: string\n  body: string\n  callback?: () => void\n}\n\nexport const showNotification = (options: NotificationOptions) => {\n  const id = uuid()\n\n  if (options.callback) {\n    if (notificationCallbacks.size >= MAX_CALLBACK_LIMIT) {\n      const oldestId = notificationCallbacks.keys().next().value\n      if (oldestId) {\n        notificationCallbacks.delete(oldestId)\n      }\n    }\n    notificationCallbacks.set(id, options.callback)\n  }\n\n  sendRPC(IRPCActionType.SHOW_NOTIFICATION, options.title, options.body, id)\n}\n"
  },
  {
    "path": "src/renderer/utils/static.ts",
    "content": "export const getRendererStaticFileUrl = (fileName: string) => {\n  return import.meta.env.BASE_URL + fileName\n}\n"
  },
  {
    "path": "src/renderer/utils/uploader.ts",
    "content": "import { v4 as uuid } from 'uuid'\n\nexport const completeUploaderMetaConfig = (originData: IStringKeyMap): IStringKeyMap => {\n  return Object.assign({\n    _configName: 'Default'\n  }, originData, {\n    _id: uuid(),\n    _createdAt: Date.now(),\n    _updatedAt: Date.now()\n  })\n}\n"
  },
  {
    "path": "src/universal/events/constants.ts",
    "content": "export const SHOW_INPUT_BOX = 'SHOW_INPUT_BOX'\nexport const SHOW_INPUT_BOX_RESPONSE = 'SHOW_INPUT_BOX_RESPONSE'\nexport const LOG_INVALID_URL_LINES = 'LOG_INVALID_URL_LINES'\nexport const TOGGLE_SHORTKEY_MODIFIED_MODE = 'TOGGLE_SHORTKEY_MODIFIED_MODE'\nexport const TALKING_DATA_APPID = '7E6832BCE3F1438696579E541DFEBFDA'\nexport const TALKING_DATA_EVENT = 'TALKING_DATA_EVENT'\nexport const TALKING_DATA_DEVICE_ID_EVENT = 'TALKING_DATA_DEVICE_ID_EVENT'\nexport const SHOW_PRIVACY_MESSAGE = 'SHOW_PRIVACY_MESSAGE'\nexport const PICGO_SAVE_CONFIG = 'PICGO_SAVE_CONFIG'\nexport const PICGO_GET_CONFIG = 'PICGO_GET_CONFIG'\nexport const PICGO_GET_DB = 'PICGO_GET_DB'\nexport const PICGO_INSERT_DB = 'PICGO_INSERT_DB'\nexport const PICGO_INSERT_MANY_DB = 'PICGO_INSERT_MANY_DB'\nexport const PICGO_UPDATE_BY_ID_DB = 'PICGO_UPDATE_BY_ID_DB'\nexport const PICGO_GET_BY_ID_DB = 'PICGO_GET_BY_ID_DB'\nexport const PICGO_REMOVE_BY_ID_DB = 'PICGO_REMOVE_BY_ID_DB'\nexport const PICGO_OPEN_FILE = 'PICGO_OPEN_FILE'\nexport const OPEN_DEVTOOLS = 'OPEN_DEVTOOLS'\nexport const SHOW_MINI_PAGE_MENU = 'SHOW_MINI_PAGE_MENU'\nexport const SHOW_MAIN_PAGE_MENU = 'SHOW_MAIN_PAGE_MENU'\nexport const SHOW_UPLOAD_PAGE_MENU = 'SHOW_UPLOAD_PAGE_MENU'\nexport const SHOW_PLUGIN_PAGE_MENU = 'SHOW_PLUGIN_PAGE_MENU'\nexport const MINIMIZE_WINDOW = 'MINIMIZE_WINDOW'\nexport const CLOSE_WINDOW = 'CLOSE_WINDOW'\nexport const OPEN_USER_STORE_FILE = 'OPEN_USER_STORE_FILE'\nexport const OPEN_URL = 'OPEN_URL'\nexport const PICGO_CONFIG_PLUGIN = 'PICGO_CONFIG_PLUGIN'\nexport const PICGO_HANDLE_PLUGIN_ING = 'PICGO_HANDLE_PLUGIN_ING'\nexport const PICGO_HANDLE_PLUGIN_DONE = 'PICGO_HANDLE_PLUGIN_DONE'\nexport const PICGO_TOGGLE_PLUGIN = 'PICGO_TOGGLE_PLUGIN'\nexport const PASTE_TEXT = 'PASTE_TEXT'\nexport const SET_MINI_WINDOW_POS = 'SET_MINI_WINDOW_POS'\nexport const RENAME_FILE_NAME = 'RENAME_FILE_NAME'\nexport const GET_RENAME_FILE_NAME = 'GET_RENAME_FILE_NAME'\nexport const SHOW_MAIN_PAGE_QRCODE = 'SHOW_MAIN_PAGE_QRCODE'\nexport const SHOW_MAIN_PAGE_DONATION = 'SHOW_MAIN_PAGE_DONATION'\nexport const FORCE_UPDATE = 'FORCE_UPDATE'\nexport const APP_CONFIG_UPDATED = 'APP_CONFIG_UPDATED'\nexport const OPEN_WINDOW = 'OPEN_WINDOW'\nexport const GET_PICBEDS = 'GET_PICBEDS'\nexport const RPC_ACTIONS = 'RPC_ACTIONS'\nexport const PICGO_NOTIFICATION_CLICKED = 'PICGO_NOTIFICATION_CLICKED'\nexport const GET_PICBED_CONFIG = 'GET_PICBED_CONFIG'\nexport const REGISTER_DEVICE_ID = 'REGISTER_DEVICE_ID'\n// i18n\nexport const GET_CURRENT_LANGUAGE = 'GET_CURRENT_LANGUAGE'\nexport const GET_LANGUAGE_LIST = 'GET_LANGUAGE_LIST'\nexport const SET_CURRENT_LANGUAGE = 'SET_CURRENT_LANGUAGE'\n"
  },
  {
    "path": "src/universal/i18n/index.ts",
    "content": "export const builtinI18nList: II18nItem[] = [{\n  label: '简体中文',\n  value: 'zh-CN'\n}, {\n  label: '繁體中文',\n  value: 'zh-TW'\n}, {\n  label: 'English',\n  value: 'en'\n}]\n"
  },
  {
    "path": "src/universal/types/cloud.ts",
    "content": "export interface IPicGoCloudUserInfo {\n  user: string\n}\n\nexport enum IPicGoCloudErrorCode {\n  LOGIN_TIMEOUT = 'PICGO_CLOUD_LOGIN_TIMEOUT',\n  LOGIN_FAILED = 'PICGO_CLOUD_LOGIN_FAILED'\n}\n"
  },
  {
    "path": "src/universal/types/cloudConfigSync.ts",
    "content": "export enum IPicGoCloudConfigSyncSessionStatus {\n  IDLE = 'IDLE',\n  SYNCING = 'SYNCING',\n  CONFLICT = 'CONFLICT'\n}\n\nexport enum IPicGoCloudConfigSyncRunStatus {\n  SUCCESS = 'success',\n  CONFLICT = 'conflict',\n  FAILED = 'failed'\n}\n\nexport enum IPicGoCloudConfigSyncConflictChoice {\n  LOCAL = 'LOCAL',\n  CLOUD = 'CLOUD'\n}\n\nexport enum IPicGoCloudConfigSyncToastType {\n  SUCCESS = 'success',\n  ERROR = 'error',\n  WARNING = 'warning',\n  INFO = 'info'\n}\n\nexport enum IPicGoCloudEncryptionMethod {\n  /**\n   * AUTO means \"follow remote state\". It corresponds to `settings.picgoCloud.encryptionMethod` being `auto` or missing.\n   */\n  AUTO = 'auto',\n  /**\n   * Server side encryption.\n   * SSE corresponds to `settings.picgoCloud.encryptionMethod` being `sse`.\n   */\n  SSE = 'sse',\n  /**\n   * End-to-end encryption.\n   * E2EE corresponds to `settings.picgoCloud.encryptionMethod` being `e2ee`.\n   */\n  E2EE = 'e2ee'\n}\n\nexport interface IPicGoCloudConfigSyncConflictItem {\n  path: string\n  localValue: unknown\n  remoteValue: unknown\n}\n\nexport type IPicGoCloudConfigSyncResolution = Record<string, IPicGoCloudConfigSyncConflictChoice>\n\nexport interface IPicGoCloudConfigSyncState {\n  sessionStatus: IPicGoCloudConfigSyncSessionStatus\n  encryptionMethod?: IPicGoCloudEncryptionMethod\n  /**\n   * `updatedAt` in `config.snapshot.json` under `baseDir` (ISO string).\n   * Used to display \"last sync time\" in the GUI.\n   */\n  lastSyncedAt?: string\n  conflicts?: IPicGoCloudConfigSyncConflictItem[]\n}\n\nexport interface IPicGoCloudConfigSyncRunResult {\n  status: IPicGoCloudConfigSyncRunStatus\n  message: string\n  toastType: IPicGoCloudConfigSyncToastType\n  state: IPicGoCloudConfigSyncState\n  /**\n   * When true, renderer SHOULD refresh auth state (treat as logged-out).\n   */\n  authInvalidated?: boolean\n  /**\n   * When true, renderer SHOULD show a restart prompt after the flow succeeds.\n   */\n  shouldShowRestartPrompt?: boolean\n}\n"
  },
  {
    "path": "src/universal/types/electron.d.ts",
    "content": "// https://stackoverflow.com/questions/45420448/how-to-import-external-type-into-global-d-ts-file\ndeclare type BrowserWindow = import('electron').BrowserWindow\ndeclare type IWindowList = import('./enum').IWindowList\n\ndeclare interface IWindowListItem {\n  isValid: boolean\n  multiple: boolean\n  options: () => IBrowserWindowOptions,\n  callback: (window: BrowserWindow, windowManager: IWindowManager) => void\n}\n\ndeclare interface IWindowManager {\n  create: (name: IWindowList) => BrowserWindow | null\n  get: (name: IWindowList) => BrowserWindow | null\n  has: (name: IWindowList) => boolean\n  // delete: (name: IWindowList) => void\n  deleteById: (id: number) => void\n  getAvailableWindow: () => BrowserWindow\n}\n\ntype IpcRendererListener = (event: import('electron').IpcRendererEvent, ...args: any[]) => void\n"
  },
  {
    "path": "src/universal/types/enum.ts",
    "content": "export enum IChalkType {\n  success = 'green',\n  info = 'blue',\n  warn = 'yellow',\n  error = 'red'\n}\n\nexport enum IPicGoHelperType {\n  afterUploadPlugins = 'afterUploadPlugins',\n  beforeTransformPlugins = 'beforeTransformPlugins',\n  beforeUploadPlugins = 'beforeUploadPlugins',\n  uploader = 'uploader',\n  transformer = 'transformer'\n}\n\nexport enum IPasteStyle {\n  MARKDOWN = 'markdown',\n  HTML = 'HTML',\n  URL = 'URL',\n  UBB = 'UBB',\n  CUSTOM = 'Custom'\n}\n\nexport enum IWindowList {\n  SETTING_WINDOW = 'SETTING_WINDOW',\n  TRAY_WINDOW = 'TRAY_WINDOW',\n  MINI_WINDOW = 'MINI_WINDOW',\n  RENAME_WINDOW = 'RENAME_WINDOW',\n  TOOLBOX_WINDOW = 'TOOLBOX_WINDOW'\n}\n\nexport enum IRemoteNoticeActionType {\n  OPEN_URL = 'OPEN_URL',\n  SHOW_NOTICE = 'SHOW_NOTICE', // notification\n  SHOW_DIALOG = 'SHOW_DIALOG', // dialog notice\n  COMMON = 'COMMON',\n  VOID = 'VOID', // do nothing\n  SHOW_MESSAGE_BOX = 'SHOW_MESSAGE_BOX'\n}\n\nexport enum IRemoteNoticeTriggerHook {\n  APP_START = 'APP_START',\n  SETTING_WINDOW_OPEN = 'SETTING_WINDOW_OPEN',\n}\n\nexport enum IRemoteNoticeTriggerCount {\n  ONCE = 'ONCE', // default\n  ALWAYS = 'ALWAYS'\n}\n\n/**\n * renderer trigger action from main\n */\nexport enum IRPCActionType {\n  // config rpc\n  GET_PICBED_CONFIG_LIST = 'GET_PICBED_CONFIG_LIST',\n  DELETE_PICBED_CONFIG = 'DELETE_PICBED_CONFIG',\n  CHANGE_CURRENT_UPLOADER = 'CHANGE_CURRENT_UPLOADER',\n  SELECT_UPLOADER = 'SELECT_UPLOADER',\n  UPDATE_UPLOADER_CONFIG = 'UPDATE_UPLOADER_CONFIG',\n  COPY_UPLOADER_CONFIG = 'COPY_UPLOADER_CONFIG',\n\n  // version rpc\n  GET_LATEST_VERSION = 'GET_LATEST_VERSION',\n\n  // toolbox rpc\n  TOOLBOX_CHECK = 'TOOLBOX_CHECK',\n  TOOLBOX_CHECK_RES = 'TOOLBOX_CHECK_RES',\n  TOOLBOX_CHECK_FIX = 'TOOLBOX_CHECK_FIX',\n\n  // system rpc\n  RELOAD_APP = 'RELOAD_APP',\n  OPEN_FILE = 'OPEN_FILE',\n  COPY_TEXT = 'COPY_TEXT',\n  SHOW_DOCK_ICON = 'SHOW_DOCK_ICON',\n  SHOW_MENUBAR_ICON = 'SHOW_MENUBAR_ICON',\n  SHOW_NOTIFICATION = 'SHOW_NOTIFICATION',\n\n  // picgo cloud rpc\n  PICGO_CLOUD_GET_USER_INFO = 'PICGO_CLOUD_GET_USER_INFO',\n  PICGO_CLOUD_LOGIN = 'PICGO_CLOUD_LOGIN',\n  PICGO_CLOUD_LOGOUT = 'PICGO_CLOUD_LOGOUT',\n  PICGO_CLOUD_DISPOSE_LOGIN_FLOW = 'PICGO_CLOUD_DISPOSE_LOGIN_FLOW',\n  PICGO_CLOUD_CONFIG_SYNC_GET_STATE = 'PICGO_CLOUD_CONFIG_SYNC_GET_STATE',\n  PICGO_CLOUD_CONFIG_SYNC_START = 'PICGO_CLOUD_CONFIG_SYNC_START',\n  PICGO_CLOUD_CONFIG_SYNC_APPLY_RESOLUTION = 'PICGO_CLOUD_CONFIG_SYNC_APPLY_RESOLUTION',\n  PICGO_CLOUD_CONFIG_SYNC_ABORT = 'PICGO_CLOUD_CONFIG_SYNC_ABORT',\n  PICGO_CLOUD_CONFIG_SYNC_SET_E2E_PREFERENCE = 'PICGO_CLOUD_CONFIG_SYNC_SET_E2E_PREFERENCE',\n\n  // gallery and toolbox rpc\n  UPDATE_GALLERY = 'UPDATE_GALLERY',\n  GET_GALLERY_MENU_LIST = 'GET_GALLERY_MENU_LIST',\n  OPEN_CONFIG_DIALOG = 'OPEN_CONFIG_DIALOG',\n}\n\nexport enum IToolboxItemType {\n  IS_CONFIG_FILE_BROKEN = 'IS_CONFIG_FILE_BROKEN',\n  IS_GALLERY_FILE_BROKEN = 'IS_GALLERY_FILE_BROKEN',\n  HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD = 'HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD',\n  HAS_PROBLEM_WITH_PROXY = 'HAS_PROBLEM_WITH_PROXY',\n}\n\nexport enum IToolboxItemCheckStatus {\n  INIT = 'init',\n  LOADING = 'loading',\n  SUCCESS = 'success',\n  ERROR = 'error',\n}\n\nexport enum IStartupMode {\n  SHOW_MAIN_WINDOW = 'SHOW_SETTING_WINDOW',\n  SHOW_MINI_WINDOW = 'SHOW_MINI_WINDOW',\n  HIDE = 'HIDE'\n}\n"
  },
  {
    "path": "src/universal/types/extra-vue.d.ts",
    "content": "import axios from 'axios'\nimport { IObject, IResult, IGetResult, IFilter } from '@picgo/store/dist/types'\n\ninterface IGalleryDB {\n  get<T>(filter?: IFilter): Promise<IGetResult<T>>\n  insert<T> (value: T): Promise<IResult<T>>\n  insertMany<T> (value: T[]): Promise<IResult<T>[]>\n  updateById (id: string, value: IObject): Promise<boolean>\n  getById<T> (id: string): Promise<IResult<T> | undefined>\n  removeById (id: string): Promise<void>\n}\n\ndeclare module 'vue/types/vue' {\n  interface Vue {\n  }\n}\n\ndeclare module 'vue' {\n  interface ComponentCustomProperties {\n    $http: typeof axios\n    $builtInPicBed: string[]\n    $$db: IGalleryDB\n    $T: typeof import('~/renderer/i18n/index').T\n    $i18n: import('~/renderer/i18n/index').I18nManager\n    saveConfig(data: IObj | string, value?: any): void\n    getConfig<T>(key?: string): Promise<T | undefined>\n    setDefaultPicBed(picBed: string): void\n    defaultPicBed: string\n    forceUpdate(): void\n    sendToMain(channel: string, ...args: any[]): void\n  }\n  interface GlobalComponents {\n    PhotoProvider: typeof import('vue3-photo-preview').PhotoProvider\n    PhotoConsumer: typeof import('vue3-photo-preview').PhotoConsumer\n    PhotoSlider: typeof import('vue3-photo-preview').PhotoSlider\n  }\n}\n"
  },
  {
    "path": "src/universal/types/global.d.ts",
    "content": "\n// https://stackoverflow.com/questions/35074713/extending-typescript-global-object-in-node-js/44387594#44387594\ndeclare var PICGO_GUI_VERSION: string\ndeclare var PICGO_CORE_VERSION: string\ndeclare var notificationList: IAppNotification[]\n\ndeclare module 'epipebomb' {\n  export default function epipebomb(stream: NodeJS.Process['stdout'], callback: () => void): void\n}\n\n// 扩展原生 IncomingMessage，添加 multer 字段\ndeclare module 'http' {\n  interface IncomingMessage {\n    files?: {\n      path: string;\n      originalname: string;\n      mimetype: string;\n      size: number;\n      [key: string]: any;\n    }[];\n    body?: any;\n  }\n}\n"
  },
  {
    "path": "src/universal/types/i18n.d.ts",
    "content": "interface ILocales {\n  LANG_DISPLAY_LABEL: string\n  ABOUT: string\n  OPEN_MAIN_WINDOW: string\n  CHOOSE_DEFAULT_PICBED: string\n  OPEN_UPDATE_HELPER: string\n  PRIVACY_TERMS_AGREEMENT: string\n  RELOAD_APP: string\n  UPLOAD_SUCCEED: string\n  UPLOAD_FAILED: string\n  UPLOAD_PROGRESS: string\n  OPERATION_SUCCEED: string\n  OPERATION_FAILED: string\n  UPLOADING: string\n  QUICK_UPLOAD: string\n  UPLOAD_BY_CLIPBOARD: string\n  HIDE_WINDOW: string\n  SPONSOR_PICGO: string\n  SHOW_PICBED_QRCODE: string\n  PICBED_QRCODE: string\n  ENABLE: string\n  DISABLE: string\n  CONFIG_THING: string\n  FIND_NEW_VERSION: string\n  NO_MORE_NOTICE: string\n  SHOW_DEVTOOLS: string\n  CURRENT_PICBED: string\n  OPEN_TOOLBOX: string\n  CHOOSE_YOUR_DEFAULT_PICBED: string\n  UPLOAD_AREA: string\n  GALLERY: string\n  PICBEDS_SETTINGS: string\n  PICGO_SETTINGS: string\n  PLUGIN_SETTINGS: string\n  PICGO_CLOUD_TITLE: string\n  PICGO_CLOUD_ERROR_TITLE: string\n  PICGO_CLOUD_NOT_LOGGED_IN: string\n  PICGO_CLOUD_LOGIN: string\n  PICGO_CLOUD_LOGOUT: string\n  PICGO_CLOUD_CANCEL_LOGIN: string\n  PICGO_CLOUD_RETRY: string\n  PICGO_CLOUD_CONFIG_SYNC: string\n  PICGO_CLOUD_LOGIN_IN_PROGRESS: string\n  PICGO_CLOUD_LOGGED_IN_AS: string\n  PICGO_CLOUD_OPEN: string\n  PICGO_CLOUD_LOGIN_TIMEOUT: string\n  PICGO_CLOUD_LOGIN_FAILED: string\n  PICGO_CLOUD_AGREE_PREFIX: string\n  PICGO_CLOUD_TERMS_OF_SERVICE: string\n  PICGO_CLOUD_AGREE_AND: string\n  PICGO_CLOUD_PRIVACY_POLICY: string\n  PICGO_CLOUD_LOGIN_EXPIRED: string\n  PICGO_CLOUD_ENCRYPTION_MODE_LABEL: string\n  PICGO_CLOUD_ENCRYPTION_MODE_AUTO: string\n  PICGO_CLOUD_ENCRYPTION_MODE_SERVER: string\n  PICGO_CLOUD_ENCRYPTION_MODE_E2E: string\n  PICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO: string\n  PICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER: string\n  PICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E: string\n  PICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC: string\n  PICGO_CLOUD_E2E_CHECKBOX_LABEL: string\n  PICGO_CLOUD_E2E_ENABLE_WARNING_TITLE: string\n  PICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE: string\n  PICGO_CLOUD_REMOTE_E2E_AUTO_ENABLED: string\n  PICGO_CLOUD_E2E_PIN_SETUP_TITLE: string\n  PICGO_CLOUD_E2E_PIN_DECRYPT_TITLE: string\n  PICGO_CLOUD_E2E_PIN_RETRY_TITLE: string\n  PICGO_CLOUD_E2E_PIN_PLACEHOLDER: string\n  PICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER: string\n  PICGO_CLOUD_CONFIG_SYNC_SUCCESS: string\n  PICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED: string\n  PICGO_CLOUD_CONFIG_SYNC_FAILED: string\n  PICGO_CLOUD_CONFIG_SYNC_ABORTED: string\n  PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE: string\n  PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY: string\n  PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM: string\n  PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL: string\n  PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED: string\n  PICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON: string\n  PICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY: string\n  PICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID: string\n  PICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS: string\n  PICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING: string\n  PICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION: string\n  PICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE: string\n  PICGO_CLOUD_CONFIG_SYNC_STARTING: string\n  PICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE: string\n  PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL: string\n  PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD: string\n  PICGO_CLOUD_CONFIG_SYNC_RESET_ALL: string\n  PICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION: string\n  PICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION: string\n  PICGO_CLOUD_CONFIG_SYNC_ABORT: string\n  PICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC: string\n  PICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED: string\n  PICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED: string\n  PICGO_CLOUD_LAST_SYNC_TIME: string\n  PICGO_CLOUD_LAST_SYNC_TIME_NONE: string\n  PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE: string\n  PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE: string\n  PICGO_CLOUD_CONFIG_SYNC_RESTART_NOW: string\n  PICGO_CLOUD_CONFIG_SYNC_RESTART_LATER: string\n  INPUT_BOX_CONFIRM_MISMATCH: string\n  PICGO_SPONSOR_TEXT: string\n  ALIPAY: string\n  WECHATPAY: string\n  CHOOSE_PICBED: string\n  COPY_PICBED_CONFIG: string\n  COPY_PICBED_CONFIG_SUCCEED: string\n  INPUT: string\n  CANCEL: string\n  CONFIRM: string\n  CHOOSE_SHOWED_PICBED: string\n  CHOOSE_PASTE_FORMAT: string\n  SEARCH: string\n  COPY: string\n  DELETE: string\n  SELECT_ALL: string\n  CHANGE_IMAGE_URL: string\n  CHANGE_IMAGE_URL_SUCCEED: string\n  COPY_LINK_SUCCEED: string\n  BATCH_COPY_LINK_SUCCEED: string\n  FILE_RENAME: string\n  COPY_FILE_PATH: string\n  OPEN_FILE_PATH: string\n  SUCCESS: string\n  FAILED: string\n  SETTINGS: string\n  SETTINGS_OPEN_CONFIG_FILE: string\n  SETTINGS_CLICK_TO_OPEN: string\n  SETTINGS_SET_LOG_FILE: string\n  SETTINGS_CLICK_TO_SET: string\n  SETTINGS_CLICK_TO_CHECK: string\n  SETTINGS_SET_SHORTCUT: string\n  SETTINGS_URL_REWRITE: string\n  SETTINGS_CUSTOM_LINK_FORMAT: string\n  SETTINGS_SET_PROXY_AND_MIRROR: string\n  SETTINGS_SET_SERVER: string\n  SETTINGS_CHECK_UPDATE: string\n  SETTINGS_OPEN_UPDATE_HELPER: string\n  SETTINGS_OPEN: string\n  SETTINGS_CLOSE: string\n  SETTINGS_ACCEPT_BETA_UPDATE: string\n  SETTINGS_LAUNCH_ON_BOOT: string\n  SETTINGS_RENAME_BEFORE_UPLOAD: string\n  SETTINGS_TIMESTAMP_RENAME: string\n  SETTINGS_OPEN_UPLOAD_TIPS: string\n  SETTINGS_NOTIFICATION_SOUND: string\n  SETTINGS_MINI_WINDOW_ON_TOP: string\n  SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: string\n  SETTINGS_TIPS_PLACEHOLDER_URL: string\n  SETTINGS_TIPS_PLACEHOLDER_FILENAME: string\n  SETTINGS_TIPS_PLACEHOLDER_EXTNAME: string\n  SETTINGS_TIPS_SUCH_AS: string\n  SETTINGS_UPLOAD_PROXY: string\n  SETTINGS_PLUGIN_INSTALL_PROXY: string\n  SETTINGS_PLUGIN_INSTALL_MIRROR: string\n  SETTINGS_CURRENT_VERSION: string\n  SETTINGS_NEWEST_VERSION: string\n  SETTINGS_GETING: string\n  SETTINGS_TIPS_HAS_NEW_VERSION: string\n  SETTINGS_LOG_FILE: string\n  SETTINGS_LOG_LEVEL: string\n  SETTINGS_LOG_FILE_SIZE: string\n  SETTINGS_SET_PICGO_SERVER: string\n  SETTINGS_TIPS_SERVER_NOTICE: string\n  SETTINGS_ENABLE_SERVER: string\n  SETTINGS_SET_SERVER_HOST: string\n  SETTINGS_SET_SERVER_PORT: string\n  SETTINGS_TIP_PLACEHOLDER_HOST: string\n  SETTINGS_TIP_PLACEHOLDER_PORT: string\n  SETTINGS_LOG_LEVEL_ALL: string\n  SETTINGS_LOG_LEVEL_SUCCESS: string\n  SETTINGS_LOG_LEVEL_ERROR: string\n  SETTINGS_LOG_LEVEL_INFO: string\n  SETTINGS_LOG_LEVEL_WARN: string\n  SETTINGS_LOG_LEVEL_NONE: string\n  SETTINGS_RESULT: string\n  SETTINGS_DEFAULT_PICBED: string\n  SETTINGS_SET_DEFAULT_PICBED: string\n  SETTINGS_NOT_CONFIG_OPTIONS: string\n  SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: string\n  SETTINGS_CHOOSE_LANGUAGE: string\n  BUILTIN_CLIPBOARD_TIPS: string\n  UPLOADER_CONFIG_NAME: string\n  URL_REWRITE_HELP: string\n  URL_REWRITE_ADD_RULE: string\n  URL_REWRITE_EDIT_RULE: string\n  URL_REWRITE_EMPTY: string\n  URL_REWRITE_ORDER: string\n  URL_REWRITE_MATCH: string\n  URL_REWRITE_REPLACE: string\n  URL_REWRITE_FLAGS: string\n  URL_REWRITE_ENABLED: string\n  URL_REWRITE_ACTIONS: string\n  URL_REWRITE_MOVE_UP: string\n  URL_REWRITE_MOVE_DOWN: string\n  URL_REWRITE_EDIT: string\n  URL_REWRITE_DELETE: string\n  URL_REWRITE_DELETE_CONFIRM: string\n  URL_REWRITE_MATCH_TIPS: string\n  URL_REWRITE_MATCH_PLACEHOLDER: string\n  URL_REWRITE_REPLACE_TIPS: string\n  URL_REWRITE_REPLACE_PLACEHOLDER: string\n  URL_REWRITE_OPTIONS: string\n  URL_REWRITE_RULE_ENABLED: string\n  URL_REWRITE_FLAG_GLOBAL_LABEL: string\n  URL_REWRITE_FLAG_GLOBAL_DESC: string\n  URL_REWRITE_FLAG_IGNORE_CASE_LABEL: string\n  URL_REWRITE_FLAG_IGNORE_CASE_DESC: string\n  URL_REWRITE_MATCH_REQUIRED: string\n  URL_REWRITE_REPLACE_REQUIRED: string\n  URL_REWRITE_INVALID_REGEX: string\n  URL_REWRITE_PREVIEW_TITLE: string\n  URL_REWRITE_PREVIEW_TIPS: string\n  URL_REWRITE_PREVIEW_PLACEHOLDER: string\n  URL_REWRITE_PREVIEW_RUN: string\n  URL_REWRITE_PREVIEW_OUTPUT: string\n  URL_REWRITE_PREVIEW_INPUT_REQUIRED: string\n  URL_REWRITE_PREVIEW_RULE_INVALID: string\n  URL_REWRITE_PREVIEW_MATCHED_RULE: string\n  URL_REWRITE_PREVIEW_NO_MATCH: string\n  UPLOADER_CONFIG_PLACEHOLDER: string\n  SELECTED_SETTING_HINT: string\n  SETTINGS_ENCODE_OUTPUT_URL: string\n  SETTINGS_SHOW_DOCK_ICON: string\n  SETTINGS_SHOW_MENUBAR_ICON: string\n  SETTINGS_SHOW_MENUBAR_ICON_TIPS: string\n  SETTINGS_STARTUP_MODE: string\n  SETTINGS_STARTUP_MODE_MAIN_WINDOW: string\n  SETTINGS_STARTUP_MODE_MINI_WINDOW: string\n  SETTINGS_STARTUP_MODE_HIDE: string\n  SHORTCUT_NAME: string\n  SHORTCUT_BIND: string\n  SHORTCUT_STATUS: string\n  SHORTCUT_ENABLED: string\n  SHORTCUT_DISABLED: string\n  SHORTCUT_SOURCE: string\n  SHORTCUT_HANDLE: string\n  SHORTCUT_ENABLE: string\n  SHORTCUT_DISABLE: string\n  SHORTCUT_EDIT: string\n  SHORTCUT_CHANGE_UPLOAD: string\n  GALLERY_URL_REWRITE_TITLE: string\n  GALLERY_URL_REWRITE_RESULT_TITLE: string\n  GALLERY_URL_REWRITE_WARN_NO_SELECTION: string\n  GALLERY_URL_REWRITE_APPLY_GLOBAL_RULES: string\n  GALLERY_URL_REWRITE_GLOBAL_RULES_COUNT: string\n  GALLERY_URL_REWRITE_TEMP_RULE_TIPS: string\n  GALLERY_URL_REWRITE_TEMP_RULE_REQUIRED: string\n  GALLERY_URL_REWRITE_NO_RULES_TO_APPLY: string\n  GALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: string\n  GALLERY_URL_REWRITE_APPLY_AND_SAVE: string\n  GALLERY_URL_REWRITE_APPLY_ONLY: string\n  GALLERY_URL_REWRITE_NO_CHANGES: string\n  GALLERY_URL_REWRITE_EMPTY_RESULT_WARN: string\n  WAIT_TO_UPLOAD: string\n  ALREADY_UPLOAD: string\n  PICTURE_UPLOAD: string\n  DRAG_FILE_TO_HERE: string\n  CLICK_TO_UPLOAD: string\n  LINK_FORMAT: string\n  CUSTOM: string\n  CLIPBOARD_PICTURE: string\n  TIPS_DRAG_VALID_PICTURE_OR_URL: string\n  TIPS_INPUT_URL: string\n  TIPS_HTTP_PREFIX: string\n  TIPS_INPUT_VALID_URL: string\n  PLUGIN_SEARCH_PLACEHOLDER: string\n  PLUGIN_INSTALL: string\n  PLUGIN_INSTALLING: string\n  PLUGIN_INSTALLED: string\n  PLUGIN_DOING_SOMETHING: string\n  PLUGIN_LIST: string\n  PLUGIN_IMPORT_LOCAL: string\n  TIPS_REMOVE_LINK: string\n  TIPS_WILL_REMOVE_CHOOSED_IMAGES: string\n  TIPS_MUST_CONTAINS_URL: string\n  TIPS_NETWORK_ERROR: string\n  TIPS_NEED_RELOAD: string\n  TIPS_PLEASE_CHOOSE_LOG_LEVEL: string\n  TIPS_SET_SUCCEED: string\n  TIPS_PLUGIN_NOT_GUI_IMPLEMENT: string\n  TIPS_CLICK_NOTIFICATION_TO_RELOAD: string\n  TIPS_GET_PLUGIN_LIST_FAILED: string\n  PLUGIN_INSTALL_SUCCEED: string\n  PLUGIN_INSTALL_FAILED: string\n  PLUGIN_UNINSTALL_SUCCEED: string\n  PLUGIN_UNINSTALL_FAILED: string\n  PLUGIN_UPDATE_SUCCEED: string\n  PLUGIN_UPDATE_FAILED: string\n  PLUGIN_IMPORT_SUCCEED: string\n  PLUGIN_IMPORT_FAILED: string\n  ENABLE_PLUGIN: string\n  DISABLE_PLUGIN: string\n  UNINSTALL_PLUGIN: string\n  UPDATE_PLUGIN: string\n  TOOLBOX: string\n  TOOLBOX_TITLE: string\n  TOOLBOX_SUB_TITLE: string\n  TOOLBOX_CHECK_CONFIG_FILE_BROKEN: string\n  TOOLBOX_CHECK_GALLERY_FILE_BROKEN: string\n  TOOLBOX_CHECK_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: string\n  TOOLBOX_CHECK_PROBLEM_WITH_PROXY: string\n  TOOLBOX_FIX_DONE_NEED_RELOAD: string\n  TOOLBOX_CANT_AUTO_FIX: string\n  TOOLBOX_START_SCAN: string\n  TOOLBOX_RE_SCAN: string\n  TOOLBOX_START_FIX: string\n  TOOLBOX_SUCCESS_TIPS: string\n  TOOLBOX_CHECK_CONFIG_FILE_PATH_TIPS: string\n  TOOLBOX_CHECK_CONFIG_FILE_BROKEN_TIPS: string\n  TOOLBOX_CHECK_GALLERY_FILE_PATH_TIPS: string\n  TOOLBOX_CHECK_GALLERY_FILE_BROKEN_TIPS: string\n  TOOLBOX_CHECK_PROXY_SUCCESS_TIPS: string\n  TOOLBOX_CHECK_PROXY_NO_PROXY_TIPS: string\n  TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_CORRECT: string\n  TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_WORKING: string\n  TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_TIPS: string\n  TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS: string\n  TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS: string\n  TIPS_NOTICE: string\n  TIPS_WARNING: string\n  TIPS_ERROR: string\n  TIPS_SKIPPED_INVALID_URLS: string\n  TIPS_TOO_MANY_URLS_CONFIRM: string\n  TIPS_NO_VALID_URLS: string\n  TIPS_INSTALL_NODE_AND_RELOAD_PICGO: string\n  TIPS_PLUGIN_REMOVE_GALLERY_ITEM: string\n  TIPS_PLUGIN_OVERWRITE_GALLERY: string\n  TIPS_UPLOAD_NOT_PICTURES: string\n  TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: string\n  TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: string\n  TIPS_PICGO_BACKUP_FILE_VERSION: string\n  TIPS_CUSTOM_CONFIG_FILE_PATH_ERROR: string\n  TIPS_SHORTCUT_MODIFIED_SUCCEED: string\n  TIPS_SHORTCUT_MODIFIED_CONFLICT: string\n  TIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED: string\n  TIPS_FIND_NEW_VERSION: string\n  TIPS_DELETE_UPLOADER_CONFIG: string\n  TIPS_COPY_UPLOADER_CONFIG: string\n  TIPS_UPLOADER_CONFIG_NAME_EMPTY: string\n  TIPS_UPLOADER_CONFIG_NOT_FOUND: string\n  TIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST: string\n  PRIVACY: string\n  PRIVACY_TIPS: string\n  QUIT: string\n}\ntype ILocalesKey = keyof ILocales\n"
  },
  {
    "path": "src/universal/types/rpc.d.ts",
    "content": "\ntype IRPCResult<T> =\n  | { success: true, data: T }\n  | { success: false, error: string }\n\ntype IGetUploaderConfigListArgs = [type: string]\ntype IDeleteUploaderConfigArgs = [type: string, configName: string]\ntype ISelectUploaderConfigArgs = [type: string, configName: string]\ntype IUpdateUploaderConfigArgs = [type: string, configId: string, config: IStringKeyMap]\ntype ICopyUploaderConfigArgs = [type: string, configName: string, newConfigName: string]\ntype IGetLatestVersionArgs = [isCheckBetaVersion: boolean]\ntype IToolboxCheckArgs = [type: import('./enum').IToolboxItemType]\ntype IOpenFileArgs = [filePath: string]\ntype ICopyTextArgs = [text: string]\ntype IShowDockIconArgs = [visible: boolean]\ntype IShowMenubarIconArgs = [visible: boolean]\ntype IShowNotificationArgs = [title: string, body: string, id?: string]\ntype IGetGalleryMenuListArgs = [selectedList: IGalleryItem[]]\n\ninterface IRPCServer {\n  start: () => void\n  stop: () => void\n  use: (routes: IRPCRoutes) => void\n}\n\ntype IRPCRoutes = Map<import('./enum').IRPCActionType, IRPCHandler<any>>\n\ntype IRPCHandler<T> = (args: any[], event: import('electron').IpcMainEvent | import('electron').IpcMainInvokeEvent) => Promise<T>\n\ninterface IRPCRouter {\n  add<T>(action: import('./enum').IRPCActionType, handler: IRPCHandler<T>): IRPCRouter\n  routes: () => IRPCRoutes\n}\n\ntype IToolboxChecker<T = any> = (event: import('electron').IpcMainEvent | import('electron').IpcMainInvokeEvent) => Promise<T>\n\ntype IToolboxCheckerMap<T extends import('./enum').IToolboxItemType> = {\n  [type in T]: IToolboxChecker\n}\n\ntype IToolboxFixMap<T extends import('./enum').IToolboxItemType> = {\n  [type in T]: IToolboxChecker<IToolboxCheckRes>\n}\n\ntype IToolboxCheckRes = {\n  type: import('./enum').IToolboxItemType\n  status: import('./enum').IToolboxItemCheckStatus,\n  msg?: string\n  value?: any\n}\n"
  },
  {
    "path": "src/universal/types/shims-module.d.ts",
    "content": "declare module '*.vue' {\n  import { DefineComponent } from 'vue'\n  const component: DefineComponent<{}, {}, any>\n  export default component\n}\n"
  },
  {
    "path": "src/universal/types/shims-tsx.d.ts",
    "content": "import Vue, { VNode } from 'vue'\n\ndeclare global {\n  interface ElectronApi {\n    getFilePath: (file: File) => string\n  }\n\n  namespace JSX {\n    // tslint:disable no-empty-interface\n    interface Element extends VNode {}\n    // tslint:disable no-empty-interface\n    interface ElementClass extends Vue {}\n    interface IntrinsicElements {\n      [elem: string]: any\n    }\n  }\n\n  interface Window {\n    electronApi: ElectronApi\n    TDAPP: {\n      onEvent: (EventId: string, Label?: string, MapKv?: IStringKeyMap) => void\n      register: (opt: {\n        profileId: string,\n        profileType: number,\n      }) => void\n      login: (opt: {\n        profileId: string,\n        profileType: number,\n      }) => void\n    }\n  }\n}\n"
  },
  {
    "path": "src/universal/types/types.d.ts",
    "content": "// global\n\ntype FN = (...args: any) => any\n\ninterface IObj {\n  [propName: string]: any\n}\n\ninterface IObjT<T> {\n  [propName: string]: T\n}\n\ndeclare interface ErrnoException extends Error {\n  errno?: number | string;\n  code?: string;\n  path?: string;\n  syscall?: string;\n  stack?: string;\n}\n\ndeclare namespace NodeJS {\n  interface ProcessEnv {\n    STATIC_PATH?: string\n  }\n}\n\ndeclare const __static: string\n\ndeclare type ILogType = 'success' | 'info' | 'warn' | 'error'\n\n// Server\ntype routeHandler = (ctx: IServerCTX) => Promise<void>\n\ntype IHttpResponse = import('http').ServerResponse\n\ninterface IServerCTX {\n  response: IHttpResponse\n  [propName: string]: any\n}\n\ninterface IServerConfig {\n  port: number | string\n  host: string\n  enable: boolean\n}\n\n// Image && PicBed\ninterface ImgInfo {\n  buffer?: Buffer\n  base64Image?: string\n  fileName?: string\n  width?: number\n  height?: number\n  extname?: string\n  imgUrl?: string\n  id?: string\n  type?: string\n  originImgUrl?: string\n  [propName: string]: any\n}\n\ninterface IGalleryItem extends ImgInfo {\n  src: string\n  key: string\n  intro: string\n}\n\ninterface IPicBedType {\n  type: string\n  name: string\n  visible: boolean\n}\n\n// Config Settings\ninterface IShortKeyConfig {\n  enable: boolean\n  key: string // 按键\n  name: string\n  label: string\n  from?: string\n}\n\ninterface IPluginShortKeyConfig {\n  key: string\n  name: string\n  label: string\n  handle: IShortKeyHandler\n}\n\ninterface IShortKeyConfigs {\n  [propName: string]: IShortKeyConfig\n}\n\ninterface IOldShortKeyConfigs {\n  upload: string\n}\n\ninterface IKeyCommandType {\n  key: string,\n  command: string\n}\n\n// Main process\ninterface IBrowserWindowOptions {\n  height: number,\n  width: number,\n  show: boolean,\n  fullscreenable: boolean,\n  resizable: boolean,\n  webPreferences: {\n    preload?: string\n    nodeIntegration: boolean,\n    nodeIntegrationInWorker: boolean,\n    contextIsolation: boolean,\n    backgroundThrottling: boolean\n    webSecurity?: boolean\n  },\n  vibrancy?: string | any,\n  frame?: boolean\n  center?: boolean\n  title?: string\n  titleBarStyle?: string | any\n  backgroundColor?: string\n  autoHideMenuBar?: boolean\n  transparent?: boolean\n  icon?: string\n  skipTaskbar?: boolean\n  alwaysOnTop?: boolean\n}\n\ninterface IFileWithPath {\n  path: string\n  name?: string\n}\n\ninterface IBounds {\n  x: number\n  y: number\n}\n\n// PicGo Types\ntype ICtx = import('picgo').PicGo\ninterface IPicGoPlugin {\n  name: string\n  fullName: string\n  author: string\n  description: string\n  logo: string\n  version: string | number\n  gui: boolean\n  config: {\n    plugin: IPluginMenuConfig\n    uploader: IPluginMenuConfig\n    transformer: IPluginMenuConfig\n    [index: string]: IPluginMenuConfig\n  } | {\n    [propName: string]: any\n  }\n  enabled?: boolean\n  homepage: string\n  guiMenu?: any[]\n  ing: boolean\n  hasInstall?: boolean\n}\n\ninterface IPicGoPluginConfig {\n  name: string\n  type: string\n  required: boolean\n  default?: any\n  alias?: string\n  choices?: {\n    name?: string\n    value?: any\n  }[]\n  /** support markdown */\n  tips?: string\n  [propName: string]: any\n}\n\ninterface IPicGoPluginShowConfigDialogOption {\n  title: string\n  config: IPicGoPluginConfig[]\n  tips?: string\n  confirmText?: string\n  cancelText?: string\n  /**\n   * default to 500\n   */\n  width?: number\n}\n\ninterface IPicGoPluginOriginConfig {\n  name: string\n  type: string\n  required: boolean\n  default?: any\n  alias?: string\n  choices?: {\n    name?: string\n    value?: any\n  }[] | (() => {\n    name?: string\n    value?: any\n  }[])\n  [propName: string]: any\n}\n\ninterface IPluginMenuConfig {\n  name: string\n  fullName?: string\n  config: any[]\n}\n\ninterface INPMSearchResult {\n  data: {\n    objects: INPMSearchResultObject[]\n  }\n}\n\ninterface INPMSearchResultObject {\n  package: {\n    name: string\n    scope: string\n    version: string\n    description: string\n    keywords: string[]\n    maintainers: Array<{\n      email: string\n      username: string\n    }>\n    links: {\n      npm: string\n      homepage: string\n    }\n  }\n}\n\ntype IDispose = () => void\n\n// GuiApi\ninterface IGuiApi {\n  showInputBox: (options: IShowInputBoxOption) => Promise<string>\n  showFileExplorer: (options: IShowFileExplorerOption) => Promise<string[]>\n  upload: (input: IUploadOption) => Promise<ImgInfo[]>\n  showNotification: (options?: IShowNotificationOption) => void\n  showMessageBox: (options?: IShowMessageBoxOption) => Promise<IShowMessageBoxResult>\n  showConfigDialog: <T extends IStringKeyMap>(options: IPicGoPluginShowConfigDialogOption) => Promise<T | false>\n  galleryDB: import('@picgo/store').DBStore\n}\ninterface IShowInputBoxOption {\n  value?: string\n  title: string\n  placeholder: string\n  inputType?: 'text' | 'textarea' | 'password'\n  /**\n   * Optional confirm input rendered in the same dialog.\n   * Commonly used for password/PIN setup to avoid user typos.\n   */\n  confirm?: {\n    value?: string\n    placeholder?: string\n  }\n  /**\n   * default to 400\n   */\n  width?: number\n}\n\ntype IShowFileExplorerOption = IObj\n\ntype IUploadOption = string[] | ImgInfo[]\n\ninterface IShowNotificationOption {\n  title: string\n  body: string\n  /**\n   * will log text\n   */\n  text?: string\n}\n\ninterface IPrivateShowNotificationOption extends IShowNotificationOption{\n  /**\n   * click notification to copy the body\n   */\n  clickToCopy?: boolean\n  copyContent?: string // something to copy\n  callback?: () => void\n}\n\ninterface IShowMessageBoxOption {\n  title: string\n  message: string\n  type: import('electron').MessageBoxOptions['type']\n  buttons: string[]\n}\n\ninterface IShowMessageBoxResult {\n  result: number\n  checkboxChecked: boolean\n}\n\ninterface IShortKeyHandlerObj {\n  handle: IShortKeyHandler\n  key: string\n  label: string\n}\n\ntype IShortKeyHandler = (ctx: ICtx, guiApi?: IGuiApi) => Promise<void | ICtx>\n\ninterface shortKeyHandlerMap {\n  from: string\n  handle: IShortKeyHandler\n}\n\n// PicBeds\ninterface IAliYunConfig {\n  accessKeyId: string\n  accessKeySecret: string,\n  bucket: string,\n  area: string,\n  path: string,\n  customUrl: string\n  options: string\n}\n\ninterface IGitHubConfig {\n  repo: string,\n  token: string,\n  path: string,\n  customUrl: string,\n  branch: string\n}\n\ninterface IImgurConfig {\n  clientId: string,\n  proxy: string\n}\n\ninterface IQiniuConfig {\n  accessKey: string,\n  secretKey: string,\n  bucket: string,\n  url: string,\n  area: string,\n  options: string,\n  path: string\n}\n\ninterface ISMMSConfig {\n  token: string\n}\n\ninterface ITcYunConfig {\n  secretId: string,\n  secretKey: string,\n  bucket: string,\n  appId: string,\n  area: string,\n  path: string,\n  customUrl: string,\n  version: 'v4' | 'v5',\n  options: string\n}\n\ninterface IUpYunConfig {\n  bucket: string,\n  operator: string,\n  password: string,\n  options: string,\n  path: string\n}\n\ntype ILoggerType = string | Error | boolean | number | undefined\n\ninterface IAppNotification {\n  title: string\n  body: string\n  text?: string\n}\n\ninterface ITalkingDataOptions {\n  EventId: string\n  Label?: string\n  MapKv?: IStringKeyMap\n}\n\ninterface IAnalyticsData {\n  fromClipboard: boolean\n  type: string\n  count: number\n  duration?: number // 耗时\n}\n\ninterface IStringKeyMap {\n  [propName: string]: any\n}\n\ntype ILogArgvType = string | number\n\ntype ILogArgvTypeWithError = ILogArgvType | Error\n\ninterface IMiniWindowPos {\n  x: number,\n  y: number,\n  height: number,\n  width: number\n}\n\ntype PromiseResType<T> = T extends Promise<infer R> ? R : T\n\n// type ILocalesKey = import('#/i18n/zh-CN').ILocalesKey\n\ninterface II18nItem {\n  label: string\n  value: string\n}\n\ninterface IRemoteNotice {\n  version: number\n  list: Array<{\n    versions: string[] // matched picgo version\n    actions: IRemoteNoticeAction[]\n    versionMatch?: 'exact' | 'gte' | 'lte'\n  }>\n}\n\ninterface IRemoteNoticeAction {\n  type: import('#/types/enum').IRemoteNoticeActionType\n  // trigger time\n  hooks: import('#/types/enum').IRemoteNoticeTriggerHook[]\n  id: string\n  // trigger count: always or once; default: once\n  triggerCount: import('#/types/enum').IRemoteNoticeTriggerCount\n\n  data?: {\n    title?: string\n    content?: string\n    desc?: string // action desc\n    buttons?: IRemoteNoticeButton[]\n    url?: string\n    copyToClipboard?: string\n    options: any // for other case\n  }\n}\n\ninterface IRemoteNoticeButton {\n  label: string\n  labelEN?: string\n  type: 'confirm' | 'cancel' | 'other'\n  action: IRemoteNoticeAction\n}\n\ninterface IRemoteNoticeLocalCountStorage {\n  [id: string]: true | number\n}\n\ninterface IUploaderListItemMetaInfo {\n  _id: string\n  _configName: string\n  _updatedAt: number\n  _createdAt: number\n}\n\ninterface IUploaderConfig { \n  [picBedType: string]: IUploaderConfigItem\n}\n\ninterface IUploaderConfigItem {\n  configList: IUploaderConfigListItem[]\n  defaultId: string\n}\n\ntype IUploaderConfigListItem = IStringKeyMap & IUploaderListItemMetaInfo\n\ntype ISwitchValueType = boolean | string | number\n"
  },
  {
    "path": "src/universal/types/view.d.ts",
    "content": "interface ISettingForm {\n  showUpdateTip: boolean\n  showPicBedList: string[]\n  autoStart: boolean\n  rename: boolean\n  autoRename: boolean\n  uploadNotification: boolean\n  notificationSound: boolean\n  miniWindowOnTop: boolean\n  logLevel: string[]\n  autoCopyUrl: boolean\n  checkBetaUpdate: boolean\n  useBuiltinClipboard: boolean\n  language: string\n  logFileSizeLimit: number\n  encodeOutputURL: boolean\n  showDockIcon: boolean\n  showMenubarIcon: boolean\n  customLink: string\n  npmProxy: string\n  npmRegistry: string\n  server: {\n    port: number\n    host: string\n    enable: boolean\n  }\n  startupMode: import('#/types/enum').IStartupMode\n}\n\ninterface IShortKeyMap {\n  [propName: string]: string\n}\n\ninterface IToolboxItem {\n  title: string\n  status: import('#/types/enum').IToolboxItemCheckStatus\n  msg?: string\n  value?: any // for handler\n  hasNoFixMethod?: boolean\n  handler?: (value: any) => Promise<void> | void\n  handlerText?: string\n}\n\ntype IToolboxMap = {\n  [id in import('#/types/enum').IToolboxItemType]: IToolboxItem\n}\n\ninterface IFormInstance {\n  validate: () => Promise<IStringKeyMap | false>\n}\n"
  },
  {
    "path": "src/universal/utils/common.ts",
    "content": "export const isUrl = (url: string): boolean => (url.startsWith('http://') || url.startsWith('https://'))\n\nexport interface IParseNewlineSeparatedUrlsResult {\n  urls: string[]\n  invalidLines: string[]\n}\n\nexport interface IParseNewlineSeparatedUrlsOptions {\n  source?: 'plain' | 'uri-list'\n}\n\nexport const parseNewlineSeparatedUrls = (\n  input: string,\n  options: IParseNewlineSeparatedUrlsOptions = {}\n): IParseNewlineSeparatedUrlsResult => {\n  const source = options.source || 'plain'\n  const urls: string[] = []\n  const invalidLines: string[] = []\n  const seen = new Set<string>()\n\n  const splitConcatenatedUrls = (line: string): string[] => {\n    const indexes: number[] = []\n    const re = /https?:\\/\\//g\n    let match: RegExpExecArray | null = null\n    while ((match = re.exec(line))) {\n      const index = match.index\n      if (index === 0) {\n        indexes.push(index)\n        continue\n      }\n      const prevChar = line[index - 1]\n      if (prevChar === '=' || prevChar === '?' || prevChar === '&' || prevChar === '#') continue\n      indexes.push(index)\n    }\n\n    if (indexes.length <= 1) return [line]\n\n    return indexes.map((startIndex, i) => {\n      const endIndex = indexes[i + 1] || line.length\n      return line.slice(startIndex, endIndex)\n    })\n  }\n\n  const normalizedInput = input\n    .split('\\u0000').join('\\n')\n    .replace(/\\r\\n/g, '\\n')\n    .replace(/\\r/g, '\\n')\n\n  normalizedInput.split('\\n').forEach((rawLine) => {\n    const line = rawLine.trim()\n    if (!line) return\n    if (source === 'uri-list' && line.startsWith('#')) return\n\n    if (!isUrl(line)) {\n      invalidLines.push(rawLine)\n      return\n    }\n\n    const parts = splitConcatenatedUrls(line)\n    parts.forEach((part) => {\n      if (seen.has(part)) return\n      urls.push(part)\n      seen.add(part)\n    })\n  })\n\n  return {\n    urls,\n    invalidLines\n  }\n}\n\nexport const extractHttpUrlsFromText = (text: string): string[] => {\n  const urls: string[] = []\n  const seen = new Set<string>()\n  const matches = text.match(/https?:\\/\\/[^\\s<>\"']+/g) || []\n\n  matches.forEach((match) => {\n    const maybeUrl = match.replace(/[)\\]}>.,;:!?]+$/, '')\n\n    if (seen.has(maybeUrl)) return\n    if (!isUrl(maybeUrl)) return\n\n    urls.push(maybeUrl)\n    seen.add(maybeUrl)\n  })\n\n  return urls\n}\nexport const isUrlEncode = (url: string): boolean => {\n  url = url || ''\n  try {\n    return url !== decodeURI(url)\n  } catch (e) {\n    // if some error caught, try to let it go\n    return false\n  }\n}\n\nexport const handleUrlEncode = (url: string): string => {\n  if (!isUrlEncode(url)) {\n    url = encodeURI(url)\n  }\n  return url\n}\n\n/**\n * streamline the full plugin name to a simple one\n * for example:\n * 1. picgo-plugin-xxx -> xxx\n * 2. @xxx/picgo-plugin-yyy -> yyy\n * @param name pluginFullName\n */\nexport const handleStreamlinePluginName = (name: string) => {\n  if (/^@[^/]+\\/picgo-plugin-/.test(name)) {\n    return name.replace(/^@[^/]+\\/picgo-plugin-/, '')\n  } else {\n    return name.replace(/picgo-plugin-/, '')\n  }\n}\n\n/**\n * for just simple clone an object\n */\nexport const simpleClone = (obj: any) => {\n  return JSON.parse(JSON.stringify(obj))\n}\n\nexport const enforceNumber = (num: number | string) => {\n  return isNaN(Number(num)) ? 0 : Number(num)\n}\n\nexport const isDev = process.env.NODE_ENV === 'development'\n\nexport const trimValues = (obj: IStringKeyMap) => {\n  const newObj = {} as IStringKeyMap\n  Object.keys(obj).forEach(key => {\n    newObj[key] = typeof obj[key] === 'string' ? obj[key].trim() : obj[key]\n  })\n  return newObj\n}\n\nexport const isMacOS = process.platform === 'darwin'\nexport const isWindows = process.platform === 'win32'\nexport const isLinux = process.platform === 'linux'\n"
  },
  {
    "path": "src/universal/utils/static.ts",
    "content": "export const CLIPBOARD_IMAGE_FOLDER = 'picgo-clipboard-images'\nexport const FORM_IMAGE_FOLDER = 'picgo-form-images'\nexport const RELEASE_URL = 'https://api.github.com/repos/Molunerfinn/PicGo/releases'\nexport const RELEASE_URL_BACKUP = 'https://release.picgo.app'\nexport const STABLE_RELEASE_URL = 'https://github.com/Molunerfinn/PicGo/releases/latest'\nexport const BETA_RELEASE_URL = 'https://github.com/Molunerfinn/PicGo/releases'\n"
  },
  {
    "path": "src/universal/utils/staticPath.ts",
    "content": "import path from 'path'\n\nconst staticBasePath = process.env.STATIC_PATH || path.join(process.cwd(), 'public')\n\nexport const getStaticPath = (...segments: string[]) => path.join(staticBasePath, ...segments)\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nconst colors = require('tailwindcss/colors')\n\nmodule.exports = {\n  content: [\n    './public/index.html',\n    './src/**/*.{js,ts,jsx,tsx,vue}'\n  ],\n  theme: {\n    extend: {\n      colors: {\n        // Keep Tailwind's full color scales so utilities like `bg-blue-100` work.\n        // Override the DEFAULT/500 tone to match PicGo's brand colors.\n        blue: {\n          ...colors.blue,\n          DEFAULT: '#49B1F5',\n          500: '#49B1F5'\n        },\n        green: {\n          ...colors.green,\n          DEFAULT: '#44B363',\n          500: '#44B363'\n        },\n        red: {\n          ...colors.red,\n          DEFAULT: '#F15140',\n          500: '#F15140'\n        },\n        yellow: {\n          ...colors.yellow,\n          DEFAULT: '#F1BE48',\n          500: '#F1BE48'\n        }\n      }\n    }\n  },\n  plugins: [],\n  corePlugins: {\n    // due to https://github.com/tailwindlabs/tailwindcss/issues/6602 - buttons disappear\n    preflight: false\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\", // https://github.com/TypeStrong/ts-loader/issues/1061\n    \"module\": \"esnext\",\n    \"strict\": true,\n    \"jsx\": \"preserve\",\n    \"importHelpers\": true,\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"esModuleInterop\": true,\n    \"experimentalDecorators\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"skipLibCheck\": true,\n    \"sourceMap\": true,\n    \"baseUrl\": \".\",\n    \"types\": [\n      \"vite/client\",\n      \"element-plus/global\",\n      \"vue3-photo-preview\",\n      \"electron\"\n    ],\n    \"typeRoots\": [\n      \"./src/universal/types/\",\n      \"./node_modules/@types\",\n      \"./node_modules\"\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"src/renderer/*\"\n      ],\n      \"~/*\": [\n        \"src/*\"\n      ],\n      \"root/*\": [\n        \"./*\"\n      ],\n      \"#/*\": [\n        \"src/universal/*\"\n      ],\n      \"apis/*\": [\n        \"src/main/apis/*\"\n      ],\n      \"@core/*\": [\n        \"src/main/apis/core/*\"\n      ]\n    },\n    \"lib\": [\n      \"esnext\",\n      \"dom\",\n      \"dom.iterable\",\n      \"scripthost\"\n    ]\n  },\n  \"include\": [\n    \"src/**/*.ts\",\n    \"src/**/*.tsx\",\n    \"src/**/*.vue\",\n    \"tests/**/*.ts\",\n    \"tests/**/*.tsx\",\n    \"electron.vite.config.ts\",\n    \"electron-builder.config.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ],\n  \"vueCompilerOptions\": {\n    \"target\": 3,\n  }\n}"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config'\nimport { resolve } from 'path'\n\nconst alias = {\n  '@': resolve(__dirname, 'src/renderer'),\n  '~': resolve(__dirname, 'src'),\n  '#': resolve(__dirname, 'src/universal'),\n  root: resolve(__dirname, '.'),\n  apis: resolve(__dirname, 'src/main/apis'),\n  '@core': resolve(__dirname, 'src/main/apis/core')\n}\n\nexport default defineConfig({\n  resolve: { alias },\n  test: {\n    environment: 'node',\n    include: ['src/__tests__/**/*.spec.ts']\n  }\n})\n\n"
  }
]