Full Code of vicanso/cyberapi for AI

main 9e0bf4499039 cached
123 files
426.1 KB
129.0k tokens
492 symbols
1 requests
Download .txt
Showing preview only (484K chars total). Download the full file or copy to clipboard to get everything.
Repository: vicanso/cyberapi
Branch: main
Commit: 9e0bf4499039
Files: 123
Total size: 426.1 KB

Directory structure:
gitextract_n8v88nit/

├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .github/
│   └── workflows/
│       ├── publish.yml
│       └── test.yml
├── .gitignore
├── .vscode/
│   └── extensions.json
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── README_uk.md
├── README_zh.md
├── cliff.toml
├── dev.md
├── index.html
├── package.json
├── splashscreen.html
├── src/
│   ├── App.tsx
│   ├── commands/
│   │   ├── api_collection.ts
│   │   ├── api_folder.ts
│   │   ├── api_setting.ts
│   │   ├── cookies.ts
│   │   ├── database.ts
│   │   ├── fake.ts
│   │   ├── fn.ts
│   │   ├── http_request.ts
│   │   ├── http_response.ts
│   │   ├── import_api.ts
│   │   ├── interface.ts
│   │   ├── invoke.ts
│   │   ├── variable.ts
│   │   └── window.ts
│   ├── components/
│   │   ├── APIResponse/
│   │   │   ├── index.tsx
│   │   │   ├── list.tsx
│   │   │   └── status_text.tsx
│   │   ├── APISettingParams/
│   │   │   ├── index.tsx
│   │   │   ├── req_params.tsx
│   │   │   └── uri.tsx
│   │   ├── APISettingTree/
│   │   │   ├── header.tsx
│   │   │   ├── index.tsx
│   │   │   ├── item_dropdown.tsx
│   │   │   └── items.tsx
│   │   ├── ExColumn.tsx
│   │   ├── ExCookieEditor.tsx
│   │   ├── ExDeleteCheck.tsx
│   │   ├── ExDialog.tsx
│   │   ├── ExForm.tsx
│   │   ├── ExKeyValue.tsx
│   │   ├── ExLoading.tsx
│   │   ├── ExPreview.tsx
│   │   └── ExTimer.tsx
│   ├── constants/
│   │   ├── common.ts
│   │   ├── handle_key.ts
│   │   ├── provide.ts
│   │   └── style.ts
│   ├── env.d.ts
│   ├── event.ts
│   ├── helpers/
│   │   ├── curl.js
│   │   ├── editor.ts
│   │   ├── hot_key.ts
│   │   ├── html.ts
│   │   ├── pinyin.ts
│   │   └── util.ts
│   ├── i18n/
│   │   ├── en.ts
│   │   ├── index.ts
│   │   ├── uk.ts
│   │   └── zh.ts
│   ├── icons.ts
│   ├── main.css
│   ├── main.ts
│   ├── root.tsx
│   ├── router/
│   │   ├── index.ts
│   │   └── routes.ts
│   ├── stores/
│   │   ├── api_collection.ts
│   │   ├── api_folder.ts
│   │   ├── api_setting.ts
│   │   ├── app.ts
│   │   ├── cookie.ts
│   │   ├── dialog.ts
│   │   ├── environment.ts
│   │   ├── global_req_header.ts
│   │   ├── header.ts
│   │   ├── local.ts
│   │   ├── pin_request.ts
│   │   ├── setting.ts
│   │   └── variable.ts
│   ├── userWorker.ts
│   └── views/
│       ├── AppHeader.tsx
│       ├── AppSetting.tsx
│       ├── Collection.tsx
│       ├── CookieSetting.tsx
│       ├── Dashboard.tsx
│       ├── StoreSetting.tsx
│       └── VariableSetting.tsx
├── src-tauri/
│   ├── .gitignore
│   ├── Cargo.toml
│   ├── build.rs
│   ├── icons/
│   │   └── icon.icns
│   ├── src/
│   │   ├── commands.rs
│   │   ├── cookies.rs
│   │   ├── entities/
│   │   │   ├── api_collections.rs
│   │   │   ├── api_folders.rs
│   │   │   ├── api_settings.rs
│   │   │   ├── mod.rs
│   │   │   ├── prelude.rs
│   │   │   ├── variables.rs
│   │   │   └── versions.rs
│   │   ├── error.rs
│   │   ├── http_request.rs
│   │   ├── main.rs
│   │   ├── schemas/
│   │   │   ├── api_collection.rs
│   │   │   ├── api_folder.rs
│   │   │   ├── api_setting.rs
│   │   │   ├── database.rs
│   │   │   ├── mod.rs
│   │   │   ├── variable.rs
│   │   │   └── version.rs
│   │   └── util.rs
│   └── tauri.conf.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintignore
================================================
# don't ever lint node_modules
node_modules
# don't lint build output (make sure it's set to your correct build folder name)
dist
# don't lint nyc coverage output
coverage

.eslintrc.js
src-tauri

================================================
FILE: .eslintrc.js
================================================
module.exports = {
  root: true,
  parser: 'vue-eslint-parser',
  plugins: [
    '@typescript-eslint',
  ],
  parserOptions: {
    parser: '@typescript-eslint/parser',
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:vue/vue3-recommended',
  ]
};

================================================
FILE: .gitattributes
================================================
*.ts linguist-language=rust


================================================
FILE: .github/workflows/publish.yml
================================================
name: "publish"
on:
  push:
    branches:
      - release

jobs:
  publish-tauri:
    strategy:
      fail-fast: false
      matrix:
        platform: [macos-latest, ubuntu-20.04, windows-latest]

    runs-on: ${{ matrix.platform }}
    steps:
    - uses: actions/checkout@v3
    - name: setup node
      uses: actions/setup-node@v3
    - name: install Rust stable
      uses: actions-rs/toolchain@v1
      with:
        toolchain: stable
    - name: install dependencies (ubuntu only)
      if: matrix.platform == 'ubuntu-20.04'
      run: |
        sudo apt-get update
        sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
    - name: install app dependencies and build it
      run: yarn && yarn build
    - uses: tauri-apps/tauri-action@v0.4.3
      env:
        GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
      with:
        tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
        releaseName: "CyberAPI v__VERSION__"
        releaseBody: "See the assets to download this version and install."
        prerelease: false

================================================
FILE: .github/workflows/test.yml
================================================
name: "test"
on:
  push:
    branches: [ main ]
jobs:
  publish-tauri:
    strategy:
      fail-fast: false
      matrix:
        platform: [macos-latest]

    runs-on: ${{ matrix.platform }}
    steps:
    - uses: actions/checkout@v3
    - name: setup node
      uses: actions/setup-node@v3
    - name: install Rust stable
      uses: actions-rs/toolchain@v1
      with:
        toolchain: stable
    - name: install app dependencies and build it
      run: |
        yarn
        yarn build
        cargo install tauri-cli
        make build

================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
stats.html


================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": ["Vue.volar"]
}


================================================
FILE: CHANGELOG.md
================================================
# Changelog

All notable changes to this project will be documented in this file.

## [0.1.21] - 2024-05-30

### Bug Fixes

- Fix url input symbols line break
- Fix read text from clipboard no permissions

### Miscellaneous Tasks

- Update dependencies
- Version 0.1.20
- Version 0.1.21

### Refactor

- Set overflow hidden for editor

## [0.1.20] - 2024-04-21

### Bug Fixes

- Fix url input symbols line break

### Miscellaneous Tasks

- Update dependencies

### Refactor

- Set overflow hidden for editor

## [0.1.19] - 2024-02-19

### Bug Fixes

- Fix export settings

### Refactor

- Adjust import api setting

## [0.1.18] - 2023-12-09

### Bug Fixes

- Fix select api item

### Miscellaneous Tasks

- Update dependencies
- Update dependencies

### Refactor

- Adjust menu of windows

## [0.1.17] - 2023-10-10

### Bug Fixes

- Fix expired cookie for cookie store

### Miscellaneous Tasks

- Update dependencies

## [0.1.16] - 2023-09-18

### Documentation

- Update document

### Refactor

- Update dependencies

## [0.1.15] - 2023-08-12

### Bug Fixes

- Fix dashboard scroll
- Fix content type of multipart form

### Miscellaneous Tasks

- Update dependencies
- Update dependencies

## [0.1.14] - 2023-07-10

### Bug Fixes

- Fix media query list add event listener, #27

### Features

- Support import api from swagger, #26

### Refactor

- Enhance api select handle
- Adjust zip feature for small size

## [0.1.13] - 2023-07-02

### Bug Fixes

- Fix parse file of header

### Features

- Support import from curl command

### Miscellaneous Tasks

- Update install cli script

### Refactor

- Enhance query string format

## [0.1.12] - 2023-06-29

### Bug Fixes

- Fix base64 encode, #23

### Refactor

- Improve the performance of select file
- Improve the performance of select file

## [0.1.11] - 2023-06-26

### Bug Fixes

- Fix set variable for req header

### Documentation

- Update readme

### Features

- Support expand view editor

## [0.1.10] - 2023-06-15

### Bug Fixes

- Fix column resize function

### Documentation

- Update readme

### Features

- Support automatic layout for editor

### Miscellaneous Tasks

- Update tauri version to 1.4.0

## [0.1.9] - 2023-06-13

### Features

- Add accept-encoding and support text content preview

### Refactor

- Adjust json format

## [0.1.8] - 2023-06-10

### Bug Fixes

- Fix image preview for response

### Refactor

- Ignore short interval of send request
- Use monaco instead of codemirror
- Adjust editor functions

## [0.1.7] - 2023-05-23

### Miscellaneous Tasks

- Update tauri action

### Refactor

- Adjust fs scope

## [0.1.6] - 2023-05-09

### Bug Fixes

- Fix value of cookie includes "="
- Fix get set cookie value of response header

### Features

- Support url keyword filter
- Show user agent of cyberapi

### Miscellaneous Tasks

- Update dependencies
- Update dependencies
- Update modules

### Refactor

- Add space before header value
- Adjust http timeout option
- Update modules

## [0.1.5] - 2023-01-13

### Bug Fixes

- Fix double click
- Update tauri fix repeat keydown event

### Features

- Support toggle comment for json editor

## [0.1.4] - 2022-12-06

### Bug Fixes

- Fix short cut doesn't work
- Fix generate curl function
- Fix keydown event trigger twice

### Miscellaneous Tasks

- Version 0.1.3
- Update package

## [0.1.3] - 2022-11-26

### Features

- Support select latest response
- Support hide application for mac
- Support clear history of response
- Support hide application of macos

### Refactor

- Show response of http request

## [0.1.2] - 2022-11-08

### Bug Fixes

- Fix import cancel handle

### Documentation

- Update readme

### Features

- Use tracing to get http stats
- Support request timeout setting
- Support tls tracing
- Support clear cookie function

### Miscellaneous Tasks

- Adjust publish workflow
- Update modules

### Refactor

- Remove unused todo
- Show send time of request
- Adjust splash screen
- Adjust cookie setting view
- Adjust cookie view
- Enhance api item drag function
- Try request error and convert to response
- Set auto focus mirror editor



================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: Makefile
================================================
.PHONY: default

lint:
	cd src-tauri && cargo clippy
fmt:
	cd src-tauri && cargo fmt --all --
dev:
	cargo tauri dev
icon:
	cargo tauri icon ./cyberapi.png
build:
	cargo tauri build
clean:
	cd src-tauri && cargo clean
install-cli:
	cargo install tauri-cli --version 1.4.0
install-orm-cli:
	cargo install sea-orm-cli
orm:
	cd src-tauri && sea-orm-cli generate entity --with-serde=both \
    -u "sqlite:///~/Library/Application Support/com.bigtree.cyberapi/my_db.db" \
    -o src/entities
version:
	git cliff --unreleased --tag 0.1.21 --prepend CHANGELOG.md


================================================
FILE: README.md
================================================
# Cyber API


[![test library](https://img.shields.io/github/workflow/status/vicanso/cyberapi/test?label=test)](https://github.com/vicanso/cyberapi/actions?query=workflow%3A%22test%22)
[![License](https://img.shields.io/badge/License-Apache%202-green.svg)](https://github.com/vicanso/cyberapi)
[![donwnload](https://img.shields.io/github/downloads/vicanso/cyberapi/total?label=Downloads&logoColor=fff&logo=GitHub)](https://github.com/vicanso/cyberapi/releases)


<p align="center">
    <img src="./cyberapi.png" alt="cyberapi" width="128">
</p>

<h3 align="center">
<a href="https://github.com/vicanso/cyberapi">CyberAPI</a> is API tool based on <a href="https://github.com/tauri-apps/tauri">tauri</a>.
</h3>

English|[简体中文](./README_zh.md)|[Українська](./README_uk.md)
## Features

- Support macos, windows and linux platforms, the installation package is below 10MB
- Thousands of interfaces for a single project are opened in seconds, with low memory usage
- Support dark/light theme and chinese/english/ukrainian
- Operation and configuration is simple
- Support importing configuration from postman, insonmia or swagger.
- The configuration can be exported by interface, function and project, which is convenient for sharing
- Support many custom functions


<p align="center">
    <img src="./asset/cyberapi.png" alt="cyberapi">
</p>


## Installation

The installer can be downloaded through [release](https://github.com/vicanso/cyberapi/releases), including windows, macos and linux versions.

Windows may need to install webview2.

## macos

> If you can't open it after installation, exec the following command then reopen:<br>`sudo xattr -rd com.apple.quarantine /Applications/CyberAPI.app`

## Development


The project depends on rust and Nodejs. If you want to compile or participate in the development yourself, you can first install the dependencies of tauri by referring to the relevant documents [here](https://tauri.app/v1/guides/getting-started/prerequisites), and then :

```shell
yarn
```

Install tauri-cli:

```shell
cargo install tauri-cli
```

Running in browser mode, the data is mock:

```shell
yarn dev
```

Running in app mode:

```shell
make dev
```

Build the release package:

```shell
make build
```





================================================
FILE: README_uk.md
================================================
# Cyber API


[![test library](https://img.shields.io/github/workflow/status/vicanso/cyberapi/test?label=test)](https://github.com/vicanso/cyberapi/actions?query=workflow%3A%22test%22)
[![License](https://img.shields.io/badge/License-Apache%202-green.svg)](https://github.com/vicanso/cyberapi)
[![donwnload](https://img.shields.io/github/downloads/vicanso/cyberapi/total?label=Downloads&logoColor=fff&logo=GitHub)](https://github.com/vicanso/cyberapi/releases)


<p align="center">
    <img src="./cyberapi.png" alt="cyberapi" width="128">
</p>

<h3 align="center">
<a href="https://github.com/vicanso/cyberapi">CyberAPI</a> це інструмент для запитів до API оснований на <a href="https://github.com/tauri-apps/tauri">tauri</a>.
</h3>

[English](./README.md)|[简体中文](./README_zh.md)|Українська
## Features

- Підтримка платформ macos, windows і linux, інсталяційний пакет менше 10 МБ
- Тисячі інтерфейсів для одного проекту відкриваються за лічені секунди, з низьким споживанням пам'яті
- Підтримка темної / світлої теми та китайської / англійської / української мови
- Проста експлуатація та конфігурація
- Підтримка імпорту конфігурації з postman, insonmia або swagger.
- Конфігурацію можна експортувати за інтерфейсом, функцією та проектом, що зручно для спільного використання
- Підтримка багатьох користувацьких функцій


<p align="center">
    <img src="./asset/cyberapi.png" alt="cyberapi">
</p>


## Installation

Інсталятор можна завантажити з [release](https://github.com/vicanso/cyberapi/releases), включаючи версії для Windows, macos і linux.

У Windows може знадобитися встановити webview2.
## Development


Проект залежить від rust та Nodejs. Якщо ви хочете самостійно компілювати або брати участь у розробці, ви можете спочатку встановити залежності tauri, звернувшись до відповідних документів [тут](https://tauri.app/v1/guides/getting-started/prerequisites), а потім :

```shell
yarn
```

Встановити tauri-cli:

```shell
cargo install tauri-cli
```

При запуску в режимі браузера дані є імітацією:

```shell
yarn dev
```

Запустити в режимі додатку: 

```shell
make dev
```

Зібрати релізний пакет:

```shell
make build
```





================================================
FILE: README_zh.md
================================================
# Cyber API


[![test library](https://img.shields.io/github/workflow/status/vicanso/cyberapi/test?label=test)](https://github.com/vicanso/cyberapi/actions?query=workflow%3A%22test%22)
[![License](https://img.shields.io/badge/License-Apache%202-green.svg)](https://github.com/vicanso/cyberapi)


<p align="center">
    <img src="./cyberapi.png" alt="cyberapi" width="128">
</p>

<h3 align="center">
<a href="https://github.com/vicanso/cyberapi">CyberAPI</a>是基于<a href="https://github.com/tauri-apps/tauri">tauri</a>开发的跨平台API客户端
</h3>

[English](./README.md)|简体中文|[Українська](./README_uk.md)
## 功能点

- 支持macos、windows以及linux平台,安装包均在10MB以下
- 单个项目上千个接口秒级打开,内存占用较低
- 支持Dark/Light主题以及中英语言
- 简单易用的操作及配置方式
- 可快速导入postman,insomnia或者swagger的配置
- 关键字筛选支持中文拼音或者首字母
- 可按接口、按功能、按项目导出配置,方便团队内共用
- 各类自定义的函数,方便各请求间关联数据


<p align="center">
    <img src="./asset/cyberapi.png" alt="cyberapi">
</p>

CyberAPI暂时仅是开发版本,业余时间的个人项目,如果有BUG或期望新增功能可以issue,对于BUG请附上系统版本信息,本人尽可能抽时间处理。


## 安装

安装程序可以通过[release](https://github.com/vicanso/cyberapi/releases)下载,包括windows、macos以及linux版本。

需要注意如果是win7或者未安装Edge的windows,在安装时会提示需要执行MicrosoftEdgeUpdateSetup的程序,如果杀毒软件提示允许执行即可。
如果是macos,由于系统的安全调整,打开应用时会提示"无法打开“CyberAPI”,因为Apple无法检查其是否包含恶意软件。",在“系统设置” -> “安全性与隐私” -> “通用”面板选择继续打开即可。或者执行以下命令:`sudo xattr -rd com.apple.quarantine /Applications/CyberAPI.app`

## 开发者

项目依赖于rust与Nodejs,如果想自行编译或参与开发,可以先参考[这里](https://tauri.app/v1/guides/getting-started/prerequisites)的相关文档安装tauri的依赖,之后执行:

```shell
yarn
```

安装tauri-cli:

```shell
cargo install tauri-cli
```

仅调整前端界面时可直接使用浏览器的方式来测试(增加了各类mock的接口),执行:

```shell
yarn dev
```

如果以APP的形式运行,则执行:

```shell
make dev
```

如果想编译安装包,则执行:

```shell
make build
```

## 创建项目

首次启动后,需要先创建项目,建议按不同的项目来创建,同一项目可共用环境变量的配置。

<p align="center">
    <img src="./asset/home.png" alt="home">
</p>

## 创建项目的环境变量

环境变量主要用于定义HTTP请求的host等场景,用于方便快捷的切换请求对接的环境。

<p align="center">
    <img src="./asset/env-select.png" alt="env-select">
</p>

tiny配置了两个环境的ENV设置,其中`http://tiny.npmtrend.com`未生效(复选框未勾选),如果需要切换不同的环境时,选择勾选不同的配置生效即可,需要注意不要同时选择相同的环境变量生效。

<p align="center">
    <img src="./asset/env-editor.png" alt="env-editor">
</p>

## 创建项目的变量

项目中使用的变量可用于各请求中的参数设置,通过`{{value(key)}}`函数引用。

<p align="center">
    <img src="./asset/variable.png" alt="variable">
</p>

## 创建目录与请求

创建请求之前,建议按功能来创建不同的分组,例如创建用户相关的一个分组:

<p align="center">
    <img src="./asset/create-folder.png" alt="create-folder">
</p>

在创建分组之后,则可以在该分组下创建对应的请求:

<p align="center">
    <img src="./asset/create-request.png" alt="create-request">
</p>


在创建请求之后,则可以选择请求使用的env(自动添加至请求url中),对应的HTTP Method以及输入URL。对于POST类请求的body部分,则可以选择对应的数据类型,如选择了json数据,填写对应的参数,图中的`{{md5(123123)}}`为函数形式,会在请求时执行此函数,填充对应的数据,后续会专门介绍此类函数:

<p align="center">
    <img src="./asset/create-request-detail.png" alt="create-request-detail">
</p>


配置完成后,点击发送则发送该请求,获取到响应后则展示如下图。第一个图标点击时会展示该请求的ID(后续可用于其它请求指定获取该请求的响应时使用),第二个图标点击会展示此请求对应的`curl`。

<p align="center">
    <img src="./asset/request-result.png" alt="request-result">
</p>

### 不同的数据类型

数据提交较为常用的是JSON以及Form类型,下图示例为选择Form类型的数据填写(需要注意切换数据类型时会清除原有数据)。如果临时想不使用某个参数,则可取消勾选即可,不需要的则可以删除,参数也可使用函数的形式:


<p align="center">
    <img src="./asset/request-post-form.png" alt="request-post-form">
</p>

`Query`与`Header`的设置与`Form`类似,不再细说。


## Pin

对于经常使用的请求,可以通过`Pin`操作将其置顶,便于后续使用,操作如下:

<p align="center">
    <img src="./asset/pin-request.png" alt="pin-request">
</p>

## 内置函数

CyberAPI内置支持了部分函数,便于在请求中动态生成参数值,具体函数如下:

- `readTextFile`:读取文本文件,也可使用简写`rtf`
- `readFile`:读取文件,也可使用简写`rf`
- `base64`:转换为base64,也可使用简写`b64`
- `openFile`:打开文件,弹窗让用户选择要打开的文件,也可使用简写`of`
- `get`:获取请求响应数据的值,也可使用简写`g`
- `timestamp`:时间戳(秒),也可使用简写`ts`
- `md5`:md5并以hex形式输出
- `sha256`:sha256并以hex形式输出


### readTextFile

读取文件并以字符的形式返回,其参数为`(file, dir)`,其中`dir`为可选参数,默认为`Download`目录,基于安全考虑,仅允许使用三个目录:`document`,`desktop`以及`download`。例如读取`download`目录下的`test/data.json`,其使用方式:`{{readTextFile(test/data.json, download)}}`

### readFile

读取文件并以字节的形式返回,其参数及使用方法与`readTextFile`一致,仅返回数据的类型不一样

### base64

将字节转换为base64的形式,一般用于函数串联时使用,如读取图片并将其转换为base64,使用方式:`{{base64.readFile(test-files/data.jpg, document)}}`,表示读取图片后,将图片的数据转换为bas64。函数串联可以以多个函数串联使用,处理方式为由右往左,后一个函数参数为前一个函数的返回。


### openFile

弹出文件打开选择框,由客户选择要打开的文件,其返回客户选择的文件路径。

### get

获取指定请求的响应,例如A请求的响应为`{"data": {"name": "CyberAPI"}}`,B请求需要使用A请求的响应的`data.name`,则先获取请求A的ID,在请求A的响应中点图标获取,如下:

<p align="center">
    <img src="./asset/request-id.png" alt="request-id">
</p>

在得到请求A的ID之后(该ID不会变化),则使用方式:`{{get(01GCE5X1X5FXM3D13NQQKYEWF7, data.name)}}`,此方式主要用于接口依赖于其它接口的响应的场景中使用。

注:如果响应数据data为数组,从数组中获取第一个元素的值,则参数如下:`data.0.name`

### timestamp

时间戳(秒),用于返回当前的时间,使用方式:`{{timestamp()}}`

### md5/sha256

计算md5并以hex形式输出,使用方式:`{{md5(123123)`,sha256的使用方式一致。


### value

获取全局配置的值,使用方式`{{value(key)}}`,方便获取项目中配置的变量数据。

## Cookie设置

Cookie的数据为应用共享,在HTTP响应头中有`Set-Cookie`则会自动保存,需要注意对于`Session`有效期的Cookie,在程序关闭之后会自动清除。用户可直接修改Cookie的有效期、值等信息或删除Cookie。

<p align="center">
    <img src="./asset/cookies.png" alt="cookies">
</p>

## 配置导入

配置导入支持四种方式,`JSON`与`File`仅支持`CyberAPI`的配置形式导入,`PostMan`用于导入postman的配置,`Insonmia`则用于导入insonmia的配置。

<p align="center">
    <img src="./asset/import-configurations.png" alt="import-configurations">
</p>


<p align="center">
    <img src="./asset/import-editor.png" alt="import-editor">
</p>



================================================
FILE: cliff.toml
================================================
# configuration file for git-cliff (0.1.0)

[changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
    ## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
    ### {{ group | upper_first }}
    {% for commit in commits %}
        - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
    {% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""

[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
    { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
    { message = "^feat", group = "Features"},
    { message = "^fix", group = "Bug Fixes"},
    { message = "^doc", group = "Documentation"},
    { message = "^perf", group = "Performance"},
    { message = "^refactor", group = "Refactor"},
    { message = "^style", group = "Styling"},
    { message = "^test", group = "Testing"},
    { message = "^chore\\(release\\): prepare for", skip = true},
    { message = "^chore", group = "Miscellaneous Tasks"},
    { body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"


================================================
FILE: dev.md
================================================
# 查询证件列表

```
security find-identity -v -p codesigning
```
# 校验IMG

校验IMG是否使用证件签名:
```
spctl -a -v src-tauri/target/release/bundle/dmg/cyberapi_0.1.0_aarch64.dmg
```

# 版本发布

修改版本号后执行`make version`生成修改记录,提交代码后合并至release

================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CyberAPI</title>
    <script>
      // safari旧版本MediaQueryList未支持addEventListener
      if (typeof matchMedia !== "undefined" && !matchMedia("all").addEventListener) {
        console.log('installing polyfill: MediaQueryList.prototype.addEventListener');
        const originalMatchMedia = matchMedia;
        self.matchMedia = function matchMedia(mediaQuery) {
          const mql = originalMatchMedia(mediaQuery);
          mql.addEventListener = function (event, listener) {
            this.addListener(listener);
          };
          return mql;
        };
      }
    </script>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>


================================================
FILE: package.json
================================================
{
  "name": "cyberapi",
  "private": true,
  "version": "",
  "scripts": {
    "format": "prettier --write src/*.ts src/*.tsx src/**/*.ts src/**/*.tsx src/**/**/*.tsx",
    "lint": "eslint . --ext .js,.tsx,.ts --fix && vue-tsc --noEmit",
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "preview": "vite preview",
    "tauri": "tauri"
  },
  "dependencies": {
    "@linaria/core": "3.0.0-beta.22",
    "@tauri-apps/api": "^1.5.3",
    "@tauri-apps/cli": "^1.5.11",
    "@vicons/ionicons5": "^0.12.0",
    "bluebird": "^3.7.2",
    "crypto-js": "^4.2.0",
    "dayjs": "^1.11.10",
    "debug": "^4.3.4",
    "form-data-encoder": "^4.0.2",
    "js-base64": "^3.7.7",
    "localforage": "^1.10.0",
    "lodash-es": "^4.17.21",
    "mime": "^4.0.1",
    "mitt": "^3.0.1",
    "monaco-editor": "^0.47.0",
    "naive-ui": "^2.38.1",
    "pinia": "^2.1.7",
    "pretty-bytes": "^6.1.1",
    "shellwords": "^1.0.1",
    "ulid": "^2.3.0",
    "vue": "^3.4.21",
    "vue-i18n": "^9.11.1",
    "vue-router": "^4.3.0"
  },
  "devDependencies": {
    "@types/bluebird": "^3.5.42",
    "@types/crypto-js": "^4.2.2",
    "@types/debug": "^4.1.12",
    "@types/mime": "^3.0.4",
    "@types/shortid": "^0.0.32",
    "@types/uuid": "^9.0.8",
    "@typescript-eslint/eslint-plugin": "^7.6.0",
    "@typescript-eslint/parser": "^7.6.0",
    "@vitejs/plugin-vue": "^5.0.4",
    "@vitejs/plugin-vue-jsx": "^3.1.0",
    "@vue/compiler-sfc": "^3.4.21",
    "eslint": "^9.0.0",
    "eslint-plugin-vue": "^9.24.1",
    "glob-parent": ">=6.0.2",
    "prettier": "^3.2.5",
    "rollup-plugin-visualizer": "^5.12.0",
    "typescript": "^5.4.4",
    "vite": "^5.2.8",
    "vite-plugin-linaria": "^1.0.0",
    "vue-tsc": "^2.0.11"
  }
}


================================================
FILE: splashscreen.html
================================================
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CyberAPI</title>
  <style>
    body {
      margin: 0;
      padding: 0;
      background-color: rgb(24, 24, 28);
    }
    img {
      display: block;
      margin: -30px 0 0 0;
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    }
    h1 {
      color: #f0f0f0;
      margin: 50px 0 0 0;
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    }
  </style>
</head>

<body>
  <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAADAFBMVEVHcEwHBzwTK5UICEMHBzUHBzQIB0owLU1mUzIJCEQyJUkuIksJEG40WZlaSUYOD2vKrihaf3pAc5IwXKJKOzNvkWcLCkKlokANDDoSDUK0hCOKaDbVnhVcjX4NCz/YtRwLCkILCT0HCDQNC0TatR3mthCTp1MHB0jbthwIBzUHB0dwWEEHB0cHB1AHCFUHB0sHB0cICVsHB0AHB0QHBzoHBz0HCWAHCmUHBzcVqP8HC3AHCmoHBzMGCI0FCX0UD00JEaMHC5gGCncGDIUIEJ0HC5MToP8cFU4G2cMlGksUD0Y/MFIH0ssNweMUrPwNCkwLxtoKFH8UEFkPu+kbE0YStPIMEngLE6sD37otIFEQj/8KzNKPbDwTsPcE6KyZcTcNC1UdFlcTJpQMF4cw6Z4NCkd8X0EOHI4aMZYSmP80KVkMH6976Yo2KE4BAZozMmkNC1C/jCXTnRq1gymieDNIN00rP48LEHIHD44PDEJfTElpU0cSue44SoRXRkwUK5kC5bMxSY7Jkx/bqBereyxQQE4iOJQmHVlJPVcZF2gOef+GZTxyWUNSWWlbWl8f4f1CQmMJEpUmIWUMI7n6/7JzY08PDWFkWVMJGMUgye4jQJgEA6UTNaoOH5Qf7P1C6poNgf/ksRNBT3oP6qcFBJJLVG4NJKNn6ZJtc7oNhv8f6aI+WYhP6ZcdOZ6a6oaBbUwpb51c6ZJGWHgYyP8HErUWsf4HB0IRzeUWuv4N+e4pSZ1VTViP6YQIav+16Y0EWf+tiTVfZ2tnYVybiEKv6Xu0nzRQa32If0wU2+4Vdcf1+8P6+JiNmFDGnSdub1/e7bv3/9XJ1s0J5N4Z0fwJ/ODI6Xl0gF+u76cS7fIQObvg634Lcf/Z+9ovS64RWb0G8ssetOAy7rfP8Z0Qlf6sy9eA7agZlNZS9eYQz9pe98D37oJJo/mr7+VxnN+osLiKk6xMcMoSUtGD/NePs96o/MUSqd5ZZJ4Te91k0NxKmtcVYNcOOdQx/9scoegAB0JHcEzBlnsUAAABAHRSTlMA/GD8YPz8AwFLYP5g/mD+/v7+YBP+K/6g/GBgXmBSKc/B/uTJ6F17duHw+/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8AkqH5kAAADR1JREFUaN7sl3lQ02caxxmdWWi7qLDa1e32mtYdZzpcdVYGnYQrAURARUgUEQFFLjm8CAgI8YLgEY7ITUQNWFS0cggB8S5yWFFADkXuU633uW1n9nne9xcS9Rfc2Z39r58E9K/vJ9/neX8voKPzB3/w/0BP979E7z9M/18+2gcdmD79048+fpcpH09R89mUzyh/o/yd8uV0DNCb+BPo6H4x8xs3N7eFyCdASEjIokVbFm0Bdu3atm3r1q3R0Xv3rlu3Z4/n6tWbd+7ctGnDhu3bN27cuGPHjtnfzgGH3oQf/4uvVOkkHvNBMJ5PBd99pxJsBsEmEFDDoUOHZs/R09HVnq8709HRjUAM4Ah5V6GqsOftCttJBVR8+6U2g57O9K8dZ82i+WoD5ocw+bSDakarmQobxiug4NBsLQY4Pl9HzWIaUMHCFQDTYFyBFfaSCp6eKGA16LEOaCbko8HRzd7enjZY8cm44f0KnnQLbxvIlHT19FjyP4qaFRWF+VgCDPa0wYqQIKZDIqMggnHDTrJnNKgU0w7NYRmSnu5XIHBE3Oyxgv3CBGZGQSFBQVSQyByl6OjodeqDNG7YCIpphPeHNF4AsCQjsveBISWQDiuCFhHDlkRgl+aeNQ2g+BkgivcrkA0w+UQACp8Ekh+0IgjBCkRQva2aGNZprIFR/EyYNu2n97YAT8c3VGCJBkti8LFPoFsggqAwRlG9S+N59mQUFzZd2LThAih+Ivz13Rnp6nwKA4py9CPxkG/pgwKfBGgB0Aphi8ISE5PBkFhdvTVjfErEAAoCSQf+8c6McAWAHy1gTyr4+ND8oITUoFQoEBQWFpQclpyMCiAjI2PvAZUBFSoH5l9jE8yK8vMjnx8NPpjPGFJTQRGWGoYQQzIRbEUDKE7hmDwvr768+TLlGsAmmOwHLKYGV0sfVx+qgHyAySeGgmR4V1fvhw7RxICbwBqrVfHXZnzOJoAGfpZ+jIERSBJSJaiQEItKgiWS9+0HxaUDoDgFeJ666nmVcm3GjBksDfywwWKAFHC1dwV8JD4SiSRVIonQ//FtDA1rC6qJIiODOjyJ5OrVtrY2EHzOLsD4xeGW3pauNN6VxItePns68ltrq1Kp7OjokMvlUsKo4b59oLhEDAcOYI22U23IDAMD7YLF4eHhrpbeRID5EongYdfzsb6++OUODk6joxXOMTGVQHllqbS3BA37My7hpK4fuI7v69fbDAzYBX+CfD8iCCf5rpJ0jBcM3mgfOrH2mBcYnFYucV4W477KxcLCzs7OorS3pKAAFfsvIST+uoHBRALM9w739gZDuivkiySCRzfazx5lEQR6eAxUDpeUFJAWjOMiUltrYMgqoPMJp/lmrunpkqkikbih/+R6dkFRkYfLj4ehRAEuY9/+ixf3YXptraGh4V/YBCa0AMRjgfT0qVNFAu5D2TmtgqR8jzfHjx8+TBUM1MAiMNVo4OpthgKRwOalrP+KdkHSnfuPxGBABVJSUlJbcvy4of6EAqxgxhRY09R4awLBnRqPJn3x8djDCMaXQDygr69NQEdkhvlYgPtIdntCwfmkhofZ4thYqgBoPKtgMe7AiAi8083Szc3nC7gBsnsfENyXNbzMFhMFecXGUgNrAxMTI+NwI6aBOdlw44cEz2Sym9lVxEARi8X6Vfp3tQmMjI2Nvc1czeaZL4B8sezexIKapIbGRtkPxVXiCHFEbASJF4urABaBCRF4GxljgXkLUHDzA4L8mkHZ7XuNTRCYHYGIQVRVpVVgZGQU7m1sbGaGBWADL2WNQH9/e/vQ2FhfT09Pc3NzXV1dS0tLKQDXkd39hv7bt2/LHuVWZVMDJZtVACMCgzcWmAcbtuGuaZI1NN1EBgdfvx6Few7J0sD9QdO5K7duNTbdLS5WGbIjsoFibSOCFRirCjzq+qXm/PnzNUVPhGVCDYIJaWmdaY+72k9eAYPsYS4YdgMRu7PhnV2VPYlVgDs2ggmZmy8QcCMG888jNfkDwUKhldBKDUrSwPDg+fqzZ0HR3/AYDMW7VcD/tQmwAinA5b6xgAcVSSp/lQPxPGseAyrAUPbm6dGj61FxrmkwV9OgTcDkg2C+jU1xy0B+EiE/sLQzB/IRPnwRhzBY2DkyduLEaVScbL/xAwgCAgJ2BxBYBaYoMCJnaAGsuLfUI59hoLSXCPh8PofPT0mxTiGC3t/61h48cRoVJ5tu5kKHADWTtJ0iZkI2cdLyAQ+GAQtpdyjPmq/g8DmcSNBACaGwu7XnyLG1qIBBtXc9BkFcnCo/TsuIjBmBjU231CKQYSDQrjTLCgpwOAoOByQwJ9hzRWu815Fjx9YeRMVQ1y/BubkgiPMNwO/aBKoGNtx6qYudmnJ5PVTgMEADq5x6pcPyeC+V4uhzEBTGxcX5+gb4wj9xE5wi3DGXWy8vt1DjUioNDrVWcGwhPpIIguuaHZajwYvO6em4gBA38Q5A0C2vdNEQlEuHQ1UNFHxrq5xhpZOTA1UcAUXf08cgKBzP9524wQKubaG0tNxllQvzdllVKu8MhSVgBwWfl9PZ0bwSDTAmougbeZLGCPyJYpL265o0sOW8kLqv0sBd2huKa7YlK8jp7ViyZOVKpkS8V3xrVmZacGFhXl6ev6+//0QCeBDwMbBV5MpbYtw1yJJ3K/h0RLzQbnmF8xINRY/yCSPwB6BEHvuISAUq4IcOy5e9hbSOZ81nDlGWdNkyZ2dqwEG1vshMA0EeFSCsDUypwAyvIo51cF2dsyYV8nqFNckPrZfHxMRoKJo7ftUo8AEBU4EDMcqKJZrUdRTCkBT80EJpC8yMGIhiVPkqEwW8vLzINQxaBEYqAdeWzxNWdIyu1GBU+UKB94RiWF4JW3dXlRhtroP8NCEI/P0nEMxVNTA2w0eZw8vpVjY7adKs7FRYW8P64QS7qBUVyn9hASteCsYvhRfwZxbBZFMT1YzwoPKtcl60Omji1OqMV3avFB5yF0YBho4YzBdapUSSeMokVoGpes02tpHwOCl7lmvioKxXwBGll4hKkSX/NTOzDPLz/FXpgJYGpuoKNlhhuBXvgnjm5RXf02zFyyrF648qwBAj/x3yYUCRkTRfINAqmAv5pibMFsieg+t64B5QE99aXy+1G4ALfFzRIoUCQlhApC1JF+CXQMAiODN57lxTnBIa5pEhWefUj/StPabx6htpcfEo8hgIDKSG8kr5q8y0MiHkr1lKo0X4ja3B9yqBiXrPvJzXYwffYmykKD+/CH7K0RLlLVmZZSQfPj9XYIP5IpGItcH3k88Qg+rKwyGlhN59MHT0tAZDD+7XJI0rXCrlT8pwwZG2thAP0aL5mC8SsQj+OfnMmbln1AZ8GCJ5oc+6htZrMPT8Qc0d+E0D/j7zCISfdL9jPi/FdinXBrLnI/9uz/x9mgjDOH5NKWXrQhqnppSBQHLX8t5PeweB1jpiXJ1YjQlh8wb/B0I0JISFwQEXFs2FwejehRInoy7oZkw0DiZMfp/nea+U9gqicePbt3dpKd9Pn/d93nuf3gtIKRugFBB9AndSvLpygvLt8LXWISqUn28/MOLBw7Wt3afkH4s/FDbDUlhqlsIsQE6BYDcYQQUSX1Xj54++9I5P36Q6PT3ufUMxtv/i892jrSfkvxq3l1stmJN8PMIwE6AYoGw9EDwMnEp3Dk663Wda3W6v1/v49fba0ct3u79Qs6b+7O77ciz54yKQXrJ1KvFsiDsbK58OhvT97OzH+1d7KCg75N9kW9/f8T1vB0c/C+DkXCUICqKfrK12O97eGNDexp4WCtbV+Nzfgzuax5BsQEqQfE2XhmUg4m1Whw6o61aoAMYC11ki+ybsPRbKTg/N2/EyAY7LUsrmbNIxEAIMouDZbi/FMcpHqiAp+ZdbodgvXlQGIMg5IKApV3cTx0AIMFqgiO63GQdhbB/LtydLfJR/flljAGX4kwjhIJuSJNHXJUJQdsgsahIMODJvhgP2FjXR4rpVGQJMGcX5IBcErhA4CpX01wfqW04TTzJRJD2/2Pcd0Hp5+N5y0bhVKwSBjoIIbsKDXZeF2rLS3qWRFGf93S2TP2Hqk0W/hasjN/hpEAAIHIlCi6adTeuQWU//27LMtCP4SO/XxVyfzM1Nc270/v5UsRoUohoIDrWU0E9aWotIZj31YUtBy/v8qm7eMycXpkcBCCHSIeiOEopMDKbIeqFVv6D0Nf9hsjGXtYuDUSBCLWBRGIFklSMQeVL+shojYnv2r2buOtImUVSAfySMiMM5DwUkhQTm/LITkU28hGMDUa7Ek43Z8phtKGMahEKNGIBEgY6Fu4wPaRK76UWF54s6B0F5e5y/bNRFhCACCaeg5giDw9Hj47qDFFclaUB5LLsz5cu2Ao1KFd8ekMuUG1IeLY8GhJqdKxrFqzZL56OxquERRToNqMvo2qV45uOwMDMxbRjFP9jurUz8hSrlq7d7/2nD+lqb4v9xy/1GN7q2fgPNIVhoREI81AAAAABJRU5ErkJggg==" />
  <h1>Cyber API</h1>
</body>
<script>
</script>
</html>

================================================
FILE: src/App.tsx
================================================
import { defineComponent, onMounted } from "vue";
import { NLayout, NLayoutHeader, useLoadingBar, NModal } from "naive-ui";
import { storeToRefs } from "pinia";

import "./main.css";
import AppHeader from "./views/AppHeader";
import { useDialogStore } from "./stores/dialog";
import AppSetting from "./views/AppSetting";
import CookieSetting from "./views/CookieSetting";
import VariableSetting from "./views/VariableSetting";
import StoreSetting from "./views/StoreSetting";
import { VariableCategory } from "./commands/variable";
import {
  i18nEnvironment,
  i18nCustomizeVariable,
  i18nGlobalReqHeader,
} from "./i18n";

export default defineComponent({
  name: "App",
  setup() {
    const loadingBar = useLoadingBar();
    const dialogStore = useDialogStore();
    const {
      showSetting,
      showCookie,
      showEnvironment,
      showStore,
      showCustomizeVariableStore,
      showReqHeader,
    } = storeToRefs(dialogStore);
    const closeDialog = () => {
      dialogStore.$reset();
    };
    onMounted(() => {
      loadingBar.finish();
    });
    return {
      closeDialog,
      showSetting,
      showCookie,
      showEnvironment,
      showStore,
      showCustomizeVariableStore,
      showReqHeader,
    };
  },
  render() {
    const {
      showSetting,
      showCookie,
      showEnvironment,
      showStore,
      showCustomizeVariableStore,
      showReqHeader,
      closeDialog,
    } = this;
    const settingModal = (
      <NModal
        autoFocus={false}
        show={showSetting}
        closeOnEsc
        onEsc={() => {
          closeDialog();
        }}
        onMaskClick={() => {
          closeDialog();
        }}
      >
        <AppSetting />
      </NModal>
    );
    const cookieModal = (
      <NModal
        autoFocus={false}
        show={showCookie}
        closeOnEsc
        onEsc={() => {
          closeDialog();
        }}
        onMaskClick={() => {
          closeDialog();
        }}
      >
        <CookieSetting />
      </NModal>
    );
    const environmentModal = (
      <NModal
        autoFocus={false}
        show={showEnvironment}
        closeOnEsc
        onEsc={() => {
          closeDialog();
        }}
        onMaskClick={() => {
          closeDialog();
        }}
      >
        <VariableSetting
          category={VariableCategory.Environment}
          title={i18nEnvironment("title")}
          tips={i18nEnvironment("tips")}
        />
      </NModal>
    );
    const customizeVariableModal = (
      <NModal
        autoFocus={false}
        show={showCustomizeVariableStore}
        closeOnEsc
        onEsc={() => {
          closeDialog();
        }}
        onMaskClick={() => {
          closeDialog();
        }}
      >
        <VariableSetting
          maxWidth={1000}
          category={VariableCategory.Customize}
          title={i18nCustomizeVariable("title")}
          tips={i18nCustomizeVariable("tips")}
        />
      </NModal>
    );
    const reqHeaderModal = (
      <NModal
        autoFocus={false}
        show={showReqHeader}
        closeOnEsc
        onEsc={() => {
          closeDialog();
        }}
        onMaskClick={() => {
          closeDialog();
        }}
      >
        <VariableSetting
          category={VariableCategory.GlobalReqHeaders}
          title={i18nGlobalReqHeader("title")}
          tips={i18nGlobalReqHeader("tips")}
        />
      </NModal>
    );
    const storeModal = (
      <NModal
        autoFocus={false}
        show={showStore}
        onEsc={() => {
          closeDialog();
        }}
        onMaskClick={() => {
          closeDialog();
        }}
      >
        <StoreSetting />
      </NModal>
    );
    return (
      <NLayout>
        {settingModal}
        {cookieModal}
        {environmentModal}
        {storeModal}
        {customizeVariableModal}
        {reqHeaderModal}
        <NLayoutHeader bordered>
          <AppHeader />
        </NLayoutHeader>
        <div>
          <router-view />
        </div>
      </NLayout>
    );
  },
});


================================================
FILE: src/commands/api_collection.ts
================================================
import dayjs from "dayjs";
import { ulid } from "ulid";

import { isWebMode } from "../helpers/util";
import { fakeAdd, fakeDeleteAPICollection, fakeList, fakeUpdate } from "./fake";
import {
  run,
  cmdAddAPICollection,
  cmdListAPICollection,
  cmdUpdateAPICollection,
  cmdDeleteAPICollection,
} from "./invoke";

const store = "apiCollections";

export interface APICollection {
  [key: string]: unknown;
  id: string;
  // 名称
  name: string;
  // 描述
  description: string;
  // 创建时间
  createdAt: string;
  // 更新时间
  updatedAt: string;
}

export function newDefaultAPICollection(): APICollection {
  const id = ulid();
  return {
    id,
    name: "",
    description: "",
    createdAt: dayjs().format(),
    updatedAt: dayjs().format(),
  };
}

export async function createAPICollection(
  collection: APICollection,
): Promise<void> {
  if (isWebMode()) {
    await fakeAdd<APICollection>(store, collection);
    return;
  }
  await run(cmdAddAPICollection, {
    collection,
  });
}

export async function listAPICollection(): Promise<APICollection[]> {
  if (isWebMode()) {
    return await fakeList<APICollection>(store);
  }
  return await run<APICollection[]>(cmdListAPICollection);
}

export async function updateAPICollection(collection: APICollection) {
  if (isWebMode()) {
    await fakeUpdate(store, collection);
    return;
  }
  await run(cmdUpdateAPICollection, {
    collection,
  });
}

export async function deleteAPICollection(id: string) {
  if (isWebMode()) {
    await fakeDeleteAPICollection(store, id);
  }
  await run(cmdDeleteAPICollection, {
    id,
  });
}


================================================
FILE: src/commands/api_folder.ts
================================================
import dayjs from "dayjs";
import { ulid } from "ulid";

import { isWebMode } from "../helpers/util";
import {
  run,
  cmdAddAPIFolder,
  cmdListAPIFolder,
  cmdUpdateAPIFolder,
  cmdDeleteAPIFolder,
} from "./invoke";
import { fakeList, fakeAdd, fakeUpdate, fakeDeleteItems } from "./fake";

const store = "apiFolders";

export interface APIFolder {
  [key: string]: unknown;
  id: string;
  collection: string;
  children: string;
  // 名称
  name: string;
  // 创建时间
  createdAt: string;
  // 更新时间
  updatedAt: string;
}

export function newDefaultAPIFolder(): APIFolder {
  const id = ulid();
  return {
    id,
    collection: "",
    children: "",
    name: "",
    createdAt: dayjs().format(),
    updatedAt: dayjs().format(),
  };
}

export async function createAPIFolder(folder: APIFolder): Promise<void> {
  if (isWebMode()) {
    await fakeAdd<APIFolder>(store, folder);
    return;
  }
  await run(cmdAddAPIFolder, {
    folder,
  });
}

export async function listAPIFolder(collection: string): Promise<APIFolder[]> {
  if (isWebMode()) {
    const folders = await fakeList<APIFolder>(store);
    return folders.filter((item) => item.collection === collection);
  }
  return await run<APIFolder[]>(cmdListAPIFolder, {
    collection,
  });
}

export async function updateAPIFolder(folder: APIFolder) {
  if (isWebMode()) {
    return await fakeUpdate(store, folder);
  }
  await run(cmdUpdateAPIFolder, {
    folder,
  });
}

export async function deleteAPIFolder(id: string): Promise<{
  folders: string[];
  settings: string[];
}> {
  const result = {
    folders: [] as string[],
    settings: [] as string[],
  };
  if (isWebMode()) {
    // 查询folders
    const folders = await listAPIFolder("");
    const folderDict: Map<string, APIFolder> = new Map();
    folders.forEach((item) => {
      folderDict.set(item.id, item);
    });
    if (!folderDict.has(id)) {
      return Promise.resolve(result);
    }
    const folderIds = [id];
    const settingIds: string[] = [];
    let children = folderDict.get(id)?.children;
    while (children) {
      const subChildren: string[] = [];
      const arr = children.split(",");
      arr.forEach((id) => {
        if (!id) {
          return;
        }
        if (settingIds.includes(id) || folderIds.includes(id)) {
          return;
        }
        const folder = folderDict.get(id);
        if (folder) {
          folderIds.push(id);
          if (folder.children) {
            subChildren.push(folder.children);
          }
        } else {
          settingIds.push(id);
        }
      });
      children = subChildren.join(",");
    }
    await fakeDeleteItems("apiSettings", settingIds);
    await fakeDeleteItems(store, folderIds);
    result.folders = folderIds;
    result.settings = settingIds;
    return result;
  }
  return await run(cmdDeleteAPIFolder, {
    id,
  });
}


================================================
FILE: src/commands/api_setting.ts
================================================
import dayjs from "dayjs";
import { ulid } from "ulid";

import { isWebMode } from "../helpers/util";
import {
  run,
  cmdAddAPISetting,
  cmdListAPISetting,
  cmdUpdateAPISetting,
  cmdDeleteAPISettings,
} from "./invoke";
import { fakeList, fakeAdd, fakeUpdate, fakeUpdateStore } from "./fake";

const store = "apiSettings";

export interface APISetting {
  [key: string]: unknown;
  id: string;
  collection: string;
  // 名称
  name: string;
  // 类型(http, graphQL)
  category: string;
  // 配置信息
  setting: string;
  // 创建时间
  createdAt: string;
  // 更新时间
  updatedAt: string;
}

export function newDefaultAPISetting(): APISetting {
  const id = ulid();
  return {
    id,
    collection: "",
    name: "",
    category: "",
    setting: "",
    createdAt: dayjs().format(),
    updatedAt: dayjs().format(),
  };
}

export async function createAPISetting(setting: APISetting): Promise<void> {
  if (isWebMode()) {
    await fakeAdd<APISetting>(store, setting);
    return;
  }
  if (!setting.id) {
    setting.id = ulid();
  }
  try {
    await run(cmdAddAPISetting, {
      setting,
    });
  } catch (err) {
    let catchError = false;
    if (err instanceof Error) {
      const message = err.message;
      if (message.includes("seaOrm") && message.includes("UNIQUE constraint failed")) {
        catchError = true;
        setting.id = ulid();
        await run(cmdAddAPISetting, {
          setting,
        }); 
      }
    }
    if (!catchError) {
      throw err;
    }
  }
}

export async function listAPISetting(
  collection: string,
): Promise<APISetting[]> {
  if (isWebMode()) {
    const settings = await fakeList<APISetting>(store);
    return settings.filter((item) => item.collection === collection);
  }
  return await run<APISetting[]>(cmdListAPISetting, {
    collection,
  });
}

export async function updateAPISetting(setting: APISetting) {
  if (isWebMode()) {
    await fakeUpdate(store, setting);
    return;
  }
  await run(cmdUpdateAPISetting, {
    setting,
  });
}

export async function deleteAPISettings(ids: string[]) {
  if (isWebMode()) {
    const arr = await fakeList<APISetting>(store);
    const result = arr.filter((item) => {
      return !ids.includes(item.id);
    });
    await fakeUpdateStore(store, result);
  }
  await run(cmdDeleteAPISettings, {
    ids,
  });
}


================================================
FILE: src/commands/cookies.ts
================================================
import { get, values } from "lodash-es";
import { isWebMode } from "../helpers/util";
import {
  cmdAddCookie,
  cmdClearCookie,
  cmdDeleteCookie,
  cmdListCookie,
  run,
} from "./invoke";

export interface Cookie {
  [key: string]: unknown;
  name: string;
  value: string;
  path: string;
  domain: string;
  expires: string;
}

export async function listCookie(): Promise<Cookie[]> {
  if (isWebMode()) {
    return Promise.resolve([
      {
        name: "cybertect",
        value: "CBBVJIUT8Q9EEFDKF9H0",
        path: "/",
        domain: "cybertect.npmtrend.com",
        expires: "Wed, 31 Jan 2024 16:00:00 GMT",
      },
      {
        name: "cybertect.sig",
        value: "iIoKqqpgXc-Ao-ilTf4XdaNyblsdKauy0fVqISbikoU",
        path: "/",
        domain: "",
        expires: "Wed, 31 Jan 2024 16:00:00 GMT",
      },
    ]);
  }
  const arr = await run<string[]>(cmdListCookie, {});
  if (!arr || !arr.length) {
    return [];
  }
  const cookies: Cookie[] = [];
  arr.forEach((data) => {
    const item = JSON.parse(data);
    const cookie = get(item, "raw_cookie");
    if (!cookie) {
      return;
    }
    const cookieValues = (cookie as string).split(";");
    const [name, ...value] = cookieValues[0].split("=");
    const path = (get(item, "path.0") || "/") as string;

    const domainValues = values(get(item, "domain"));
    let domain = "";
    if (domainValues && domainValues.length) {
      domain = domainValues[0];
    }
    const expires = get(item, "expires.AtUtc") as string;
    cookies.push({
      name,
      value: value.join("="),
      path: path || "",
      domain: domain || "",
      expires: expires || "",
    });
  });
  return cookies;
}

export async function deleteCookie(c: Cookie) {
  if (isWebMode()) {
    return;
  }
  await run(cmdDeleteCookie, {
    c,
  });
}

export async function clearCookie() {
  if (isWebMode()) {
    return;
  }
  await run(cmdClearCookie, {});
}

export async function addOrUpdate(c: Cookie) {
  if (isWebMode()) {
    return;
  }
  await run(cmdAddCookie, {
    c,
  });
}


================================================
FILE: src/commands/database.ts
================================================
import { getVersion } from "@tauri-apps/api/app";
import { message } from "@tauri-apps/api/dialog";
import dayjs from "dayjs";
import { ulid } from "ulid";

import { isWebMode } from "../helpers/util";
import {
  cmdAddVersion,
  cmdExportTables,
  cmdGetLatestVersion,
  cmdImportTables,
  cmdInitTables,
  run,
} from "./invoke";

export interface Version {
  [key: string]: unknown;
  id: string;
  version: string;
  createdAt: string;
  updatedAt: string;
}

async function getDatabaseLatestVersion() {
  if (isWebMode()) {
    return {} as Version;
  }
  return await run<Version>(cmdGetLatestVersion, {});
}

// handleDatabaseCompatible 处理数据库兼容
export async function handleDatabaseCompatible() {
  if (isWebMode()) {
    return;
  }
  try {
    await run(cmdInitTables);
    const version = await getVersion();
    const latestVersion = await getDatabaseLatestVersion();
    if (!latestVersion || latestVersion.version !== version) {
      await run(cmdAddVersion, {
        version: {
          id: ulid(),
          version,
          createdAt: dayjs().format(),
          updatedAt: dayjs().format(),
        },
      });
    }
    // TODO 后续针对数据库做更新
  } catch (err) {
    if (err instanceof Error) {
      message(err.message);
    }
    console.error(err);
  }
}

export async function exportTables(): Promise<string> {
  return await run(cmdExportTables);
}

export async function importTables(file: string) {
  return await run(cmdImportTables, {
    file,
  });
}


================================================
FILE: src/commands/fake.ts
================================================
import localforage from "localforage";

const stores = new Map<string, LocalForage>();

interface WithID {
  id: string;
}

function getStore(name: string): LocalForage {
  let store = stores.get(name);
  if (store) {
    return store;
  }
  store = localforage.createInstance({
    name,
  });
  stores.set(name, store);
  return store;
}

export async function fakeList<T>(storeName: string): Promise<T[]> {
  const store = getStore(storeName);

  const result = await store.getItem<T[]>("fake");
  if (result != null) {
    return result;
  }
  return [];
}
export async function fakeAdd<T>(storeName: string, data: T) {
  const result = await fakeList<T>(storeName);
  result.push(Object.assign({}, data));
  await fakeUpdateStore(storeName, result);
}

export async function fakeUpdate<T extends WithID>(storeName: string, data: T) {
  const result = await fakeList<T>(storeName);
  let found = -1;
  result.forEach((item, index) => {
    if (item.id == data.id) {
      found = index;
    }
  });
  if (found !== -1) {
    result[found] = Object.assign({}, data);
  }
  await fakeUpdateStore(storeName, result);
}

export async function fakeDeleteAPICollection<T extends WithID>(
  storeName: string,
  id: string,
) {
  // 暂时简单删除collection
  const result = await fakeList<T>(storeName);
  let found = -1;
  result.forEach((item, index) => {
    if (item.id == id) {
      found = index;
    }
  });
  if (found !== -1) {
    result.splice(found, 1);
  }
  await fakeUpdateStore(storeName, result);
}

export async function fakeDeleteItems<T extends WithID>(
  storeName: string,
  ids: string[],
) {
  const result = await fakeList<T>(storeName);
  const arr = [] as unknown[];
  result.forEach((item) => {
    if (!ids.includes(item.id)) {
      arr.push(item);
    }
  });
  await fakeUpdateStore(storeName, arr);
}

export async function fakeUpdateStore(storeName: string, data: unknown) {
  const store = getStore(storeName);
  await store.setItem("fake", data);
}


================================================
FILE: src/commands/fn.ts
================================================
import {
  BaseDirectory,
  FsOptions,
  readBinaryFile,
  readTextFile,
} from "@tauri-apps/api/fs";
import { open } from "@tauri-apps/api/dialog";
import { toString, get, trim } from "lodash-es";
import { fromUint8Array } from "js-base64";
import sha256 from "crypto-js/sha256";
import md5 from "crypto-js/md5";

import { i18nCommon } from "../i18n";
import { getLatestResponse, getResponseBody } from "./http_response";
import { listVariable, VariableCategory, VariableStatus } from "./variable";
interface FnHandler {
  collection: string;
  // 原始字符
  text: string;
  // 函数列表
  fnList: string[];
  // 初始参数
  param: string | string[];
}

enum Fn {
  readTextFile = "readTextFile",
  rtf = "rtf",
  readFile = "readFile",
  rf = "rf",
  base64 = "base64",
  b64 = "b64",
  openFile = "openFile",
  of = "of",
  get = "get",
  g = "g",
  timestamp = "timestamp",
  ts = "ts",
  md5 = "md5",
  sha256 = "sha256",
  value = "value",
}

function trimParam(param: string): string | string[] {
  const arr = param.split(",").map((item) => {
    item = item.trim();
    item = trim(item, "'");
    item = trim(item, '"');
    return item;
  });

  // 单引号替换为双引号
  // const str = `[${param.replaceAll("'", '"')}]`;
  // const arr = JSON.parse(str);
  if (arr.length < 2) {
    return arr[0];
  }
  return arr;
}

export function parseFunctions(collection: string, value: string): FnHandler[] {
  const reg = /\{\{([\s\S]+?)\}\}/g;
  const parmaReg = /\(([\s\S]*?)\)/;
  let result: RegExpExecArray | null;
  const handlers: FnHandler[] = [];
  while ((result = reg.exec(value)) !== null) {
    if (result.length !== 2) {
      break;
    }
    const paramResult = parmaReg.exec(result[1]);
    if (paramResult?.length !== 2) {
      break;
    }
    const fnList = result[1].replace(paramResult[0], "").split(".");
    handlers.push({
      collection,
      text: result[0],
      fnList: fnList,
      param: trimParam(paramResult[1]),
    });
  }
  return handlers;
}

interface FsParams {
  file: string;
  option: FsOptions;
}

function getDir(dir: string): BaseDirectory {
  switch (dir.toLowerCase()) {
    case "document":
      return BaseDirectory.Document;
      break;
    case "desktop":
      return BaseDirectory.Desktop;
      break;
    default:
      return BaseDirectory.Download;
      break;
  }
}

function convertToFsParams(p: unknown): FsParams {
  const option = {
    dir: BaseDirectory.Download,
  };
  let file = toString(p);
  if (Array.isArray(p)) {
    file = toString(p[0]);
    if (p[1]) {
      option.dir = getDir(p[1]);
    }
  }
  return {
    file,
    option,
  };
}

export async function doFnHandler(handler: FnHandler): Promise<string> {
  const { param, fnList, collection } = handler;
  let p: unknown = param;
  const size = fnList.length;
  //   函数处理从后往前
  for (let index = size - 1; index >= 0; index--) {
    const fn = fnList[index];
    switch (fn) {
      case Fn.readTextFile:
      case Fn.rtf:
        {
          const params = convertToFsParams(p);
          p = await readTextFile(params.file, params.option);
        }
        break;
      case Fn.md5:
        p = md5(toString(p)).toString();
        break;
      case Fn.sha256:
        p = sha256(toString(p)).toString();
        break;
      case Fn.readFile:
      case Fn.rf:
        {
          const params = convertToFsParams(p);
          p = await readBinaryFile(params.file, params.option);
        }
        break;
      case Fn.base64:
      case Fn.b64:
        {
          p = fromUint8Array(p as Uint8Array);
        }
        break;
      case Fn.openFile:
      case Fn.of:
        {
          const selected = await open({
            title: i18nCommon("selectFile"),
          });
          if (selected) {
            p = selected as string;
          }
        }
        break;
      case Fn.timestamp:
      case Fn.ts:
        {
          p = `${Math.round(Date.now() / 1000)}`;
        }
        break;
      case Fn.get:
      case Fn.g:
        {
          const arr = toString(p).split(",");
          if (arr.length !== 2) {
            throw new Error("params of get from response is invalid");
          }
          const resp = await getLatestResponse(arr[0].trim());
          if (resp) {
            const result = getResponseBody(resp);
            p = get(result.json, arr[1].trim());
          }
        }
        break;
      case Fn.value:
        {
          const name = toString(p);
          const arr = await listVariable(
            collection,
            VariableCategory.Customize,
          );
          const found = arr.find(
            (item) =>
              item.enabled === VariableStatus.Enabled && item.name === name,
          );
          if (found) {
            p = found.value;
          }
        }
        break;
      default:
        break;
    }
  }
  return toString(p);
}


================================================
FILE: src/commands/http_request.ts
================================================
import { forEach, isArray } from "lodash-es";
import { encode } from "js-base64";
import { ulid } from "ulid";
import { getVersion, getTauriVersion } from "@tauri-apps/api/app";
import { arch, type, version } from "@tauri-apps/api/os";
import { FormDataEncoder } from "form-data-encoder";
import { fromUint8Array } from "js-base64";
import { readBinaryFile } from "@tauri-apps/api/fs";

import { run, cmdDoHTTPRequest } from "./invoke";
import { KVParam } from "./interface";
import { isWebMode, delay, formatError } from "../helpers/util";
import { doFnHandler, parseFunctions } from "./fn";
import { HTTPResponse, addLatestResponse } from "./http_response";
import { Cookie } from "./cookies";
import mime from "mime";

export enum HTTPMethod {
  GET = "GET",
  POST = "POST",
  PUT = "PUT",
  PATCH = "PATCH",
  DELETE = "DELETE",
  OPTIONS = "OPTIONS",
  HEAD = "HEAD",
}

export enum ContentType {
  JSON = "application/json",
  Form = "application/x-www-form-urlencoded",
  Multipart = "multipart/form-data",
  XML = "application/xml",
  Plain = "text/plain",
}

export interface RequestTimeout {
  [key: string]: unknown;
  connect: number;
  write: number;
  read: number;
}

export interface HTTPRequest {
  [key: string]: unknown;
  method: string;
  uri: string;
  body: string;
  contentType: string;
  headers: KVParam[];
  query: KVParam[];
  auth: KVParam[];
}

function convertKVListToURLValues(kvList: KVParam[]) {
  if (!kvList || kvList.length === 0) {
    return [];
  }
  const arr: string[] = [];
  kvList.forEach((kv) => {
    if (!kv.enabled) {
      return;
    }
    arr.push(`${kv.key}=${encodeURIComponent(kv.value)}`);
  });
  return arr;
}

export async function convertRequestToCURL(
  collection: string,
  req: HTTPRequest,
  cookies: Cookie[],
) {
  await convertKVParams(collection, req.query);
  await convertKVParams(collection, req.headers);
  const queryList = convertKVListToURLValues(req.query);

  let uri = req.uri;
  if (queryList.length !== 0) {
    if (uri.includes("?")) {
      uri += `&${queryList.join("&")}`;
    } else {
      uri += `?${queryList.join("&")}`;
    }
  }

  const headerList: string[] = [];
  const host = new URL(uri).host;
  const cookieValues: string[] = [];
  cookies.forEach((item) => {
    if (host.includes(item.domain)) {
      cookieValues.push(`${item.name}=${item.value}`);
    }
  });
  if (cookieValues.length) {
    headerList.push(`-H 'Cookie: ${cookieValues.join("; ")}'`);
  }

  let includeContentType = false;
  req.headers?.forEach((kv) => {
    if (!kv.enabled) {
      return;
    }
    if (kv.key.toLowerCase() === "content-type") {
      includeContentType = true;
    }
    headerList.push(`-H '${kv.key}: ${kv.value}'`);
  });
  if (!includeContentType && req.contentType) {
    headerList.push(`-H 'Content-Type: ${req.contentType}'`);
  }
  let body = " ";
  if (req.body) {
    body = await convertBody(collection, req.body);
    switch (req.contentType) {
      case ContentType.JSON:
        body = JSON.stringify(JSON.parse(body));
        break;
      case ContentType.Form:
        {
          const arr: KVParam[] = JSON.parse(body);
          body = convertKVListToURLValues(arr).join("&");
        }
        break;
      default:
        break;
    }
    body = ` -d '${body}' `;
  }
  const method = req.method || "GET";
  return `curl -v -X${method.toUpperCase()}${body}${headerList.join(
    " ",
  )} '${uri}'`;
}

function is_json(str: string) {
  const value = str.trim();
  if (value.length < 2) {
    return false;
  }
  const key = value[0] + value[value.length - 1];
  return key === "{}" || key === "[]";
}

async function convertBody(collection: string, data: string) {
  let body = data;
  // 注释的处理
  if (is_json(body)) {
    const arr = body.split("\n").filter((item) => {
      if (item.trim().startsWith("//")) {
        return false;
      }
      return true;
    });
    body = arr.join("\n");
  }

  const handlers = parseFunctions(collection, body);
  if (handlers.length === 0) {
    return body;
  }
  for (let i = 0; i < handlers.length; i++) {
    const handler = handlers[i];
    const result = await doFnHandler(handler);
    // 替换result的内容
    body = body.replace(handler.text, result);
  }
  return body;
}

export async function convertKVParams(collection: string, params: KVParam[]) {
  if (!params || params.length === 0) {
    return;
  }
  for (let i = 0; i < params.length; i++) {
    const param = params[i];
    const handlers = parseFunctions(collection, param.value);
    if (handlers.length === 0) {
      continue;
    }
    let { value } = param;
    for (let j = 0; j < handlers.length; j++) {
      const handler = handlers[j];
      const result = await doFnHandler(handler);
      // 替换result的内容
      value = value.replace(handler.text, result);
    }
    param.value = value;
  }
}

export const abortRequestID = ulid();

interface MultipartFormData {
  headers: {
    "Content-Type": string;
    "Content-Length"?: string;
  };
  body: string;
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async function convertMultipartForm(body: string): Promise<MultipartFormData> {
  const arr = JSON.parse(body) as KVParam[];
  const form = new FormData();
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    if (!item.enabled || !item.key) {
      continue;
    }
    const fileProtocol = "file://";
    if (item.value.startsWith(fileProtocol)) {
      const file = item.value.substring(fileProtocol.length);
      const fileData = await readBinaryFile(file);
      form.append(
        item.key,
        new Blob([fileData], {
          type: mime.getType(file) || "",
        }),
        file,
      );
      continue;
    }

    form.append(item.key, item.value);
  }
  // eslint-disable-next-line
  // @ts-ignore
  const encoder = new FormDataEncoder(form);
  // eslint-disable-next-line
  // @ts-ignore
  const b = new Blob(encoder, {
    type: encoder.contentType,
  });
  const buf = await b.arrayBuffer();
  return {
    headers: encoder.headers,
    body: fromUint8Array(new Uint8Array(buf)),
  };
}

export async function getUserAgent() {
  const appVersion = await getVersion();
  const appOS = await type();
  const appOSVersion = await version();
  const appArch = await arch();
  const tauriVersion = await getTauriVersion();
  return `CyberAPI/${appVersion} (${appOS}; tauri/${tauriVersion}; ${appOSVersion}; ${appArch})`;
}

// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)
let userAgent = "";

export async function doHTTPRequest(options: {
  id: string;
  collection: string;
  req: HTTPRequest;
  originalReq: HTTPRequest;
  timeout: RequestTimeout;
}): Promise<HTTPResponse> {
  const { id, collection, req, originalReq, timeout } = options;
  if (!req.headers) {
    req.headers = [];
  }
  if (!req.query) {
    req.query = [];
  }
  if (!req.auth) {
    req.auth = [];
  }
  const method = req.method || HTTPMethod.GET;
  let body = req.body || "";
  let contentType = req.contentType || "";
  // 非此类请求,将body设置为空
  if (
    ![HTTPMethod.POST, HTTPMethod.PATCH, HTTPMethod.PUT].includes(
      method as HTTPMethod,
    )
  ) {
    body = "";
    contentType = "";
  }
  body = await convertBody(collection, body);
  // 如果是form
  if (body && contentType === ContentType.Form) {
    const arr = JSON.parse(body) as KVParam[];
    const result: string[] = [];
    arr.forEach((item) => {
      if (!item.enabled) {
        return;
      }
      result.push(
        `${window.encodeURIComponent(item.key)}=${window.encodeURIComponent(
          item.value,
        )}`,
      );
    });
    body = result.join("&");
  }
  if (body && contentType === ContentType.Multipart) {
    const data = await convertMultipartForm(body);
    contentType = data.headers["Content-Type"];
    body = data.body;
  }
  const params = {
    method: method,
    uri: req.uri,
    body,
    contentType,
    headers: req.headers,
    query: req.query,
  };
  await convertKVParams(collection, params.query);
  await convertKVParams(collection, params.headers);
  if (isWebMode()) {
    const ms = Math.random() * 2000;
    await delay(ms);
    const headers = new Map<string, string[]>();
    headers.set("content-type", ["application/json"]);
    headers.set("set-cookie", ["uid=ZHGG9VYP; path=/; httponly"]);
    const resp = {
      api: id,
      req: req,
      latency: Math.ceil(ms),
      status: 200,
      bodySize: 0,
      headers,
      body: encode(JSON.stringify(params)),
      stats: {
        isHttps: false,
        cipher: "",
        remoteAddr: "127.0.0.1:80",
        dnsLookup: 1,
        tcp: 2,
        tls: 3,
        send: 0,
        serverProcessing: 4,
        contentTransfer: 5,
        total: 20,
      },
    };

    addLatestResponse(resp);
    return Promise.resolve(resp);
  }

  if (!userAgent) {
    userAgent = await getUserAgent();
  }

  params.headers.push({
    key: "User-Agent",
    value: userAgent,
    enabled: true,
  });

  const auth = req.auth.filter((item) => item.enabled);
  if (auth.length) {
    const value = encode(`${auth[0].key}:${auth[0].value}`);
    params.headers.push({
      key: "Authorization",
      value: `Basic ${value}`,
      enabled: true,
    });
  }

  const requestTimeout = {
    connect: 10,
    write: 120,
    read: 300,
  };
  if (timeout.connect && timeout.connect > 0) {
    requestTimeout.connect = timeout.connect;
  }
  if (timeout.write && timeout.write > 0) {
    requestTimeout.write = timeout.write;
  }
  if (timeout.read && timeout.read > 0) {
    requestTimeout.read = timeout.read;
  }
  // eslint-disable-next-line
  // @ts-ignore
  let resp: HTTPResponse = {};

  const startedAt = Date.now();
  try {
    resp = await run<HTTPResponse>(cmdDoHTTPRequest, {
      req: params,
      api: id,
      timeout: requestTimeout,
    });
  } catch (err) {
    resp.body = formatError(err);
    resp.latency = Date.now() - startedAt;
  }
  if (resp.latency <= 0) {
    resp.latency = 1;
  }
  // 转换为Map<string, string[]>
  const headers = new Map<string, string[]>();
  forEach(resp.headers, (value, key) => {
    const k = key.toString();
    if (isArray(value)) {
      headers.set(k, value);
    } else {
      headers.set(k, [value as string]);
    }
  });

  resp.req = originalReq;
  resp.headers = headers;
  addLatestResponse(resp);
  return resp;
}


================================================
FILE: src/commands/http_response.ts
================================================
import { decode } from "js-base64";
import dayjs from "dayjs";
import { forEach } from "lodash-es";
import mitt, { Emitter } from "mitt";

import { getLatestResponseStore } from "../stores/local";
import { HTTPRequest } from "./http_request";
import { ulid } from "ulid";

const applicationJSON = "application/json";

export interface HTTPStats {
  remoteAddr: string;
  isHttps: boolean;
  cipher: string;
  dnsLookup: number;
  tcp: number;
  tls: number;
  send: number;
  serverProcessing: number;
  contentTransfer: number;
  total: number;
}

export interface HTTPResponse {
  [key: string]: unknown;
  // response id
  id?: string;
  // api id
  api: string;
  req: HTTPRequest;
  // 原始body的大小(未解压)
  bodySize: number;
  // 耗时(ms)
  latency: number;
  status: number;
  headers: Map<string, string[]>;
  body: string;
  stats: HTTPStats;
}

const selectEvent = "select";
type Events = {
  [selectEvent]: HTTPResponse;
};
const emitter: Emitter<Events> = mitt<Events>();

export enum ResponseBodyCategory {
  JSON = "json",
  Binary = "binary",
  Text = "text",
}
export interface ResponseBodyResult {
  category: ResponseBodyCategory;
  data: string;
  size: number;
  json?: Map<string, unknown>;
}

const statusTextMap = new Map<string, string>();
(() => {
  const dict = {
    100: "Continue",
    101: "Switching Protocols",
    102: "Processing",
    103: "Early Hints",
    200: "OK",
    201: "Created",
    202: "Accepted",
    203: "Non-Authoritative Information",
    204: "No Content",
    205: "Reset Content",
    206: "Partial Content",
    207: "Multi-Status",
    208: "Already Reported",
    226: "IM Used",
    300: "Multiple Choices",
    301: "Moved Permanently",
    302: "Found",
    303: "See Other",
    304: "Not Modified",
    305: "Use Proxy",
    307: "Temporary Redirect",
    308: "Permanent Redirect",
    400: "Bad Request",
    401: "Unauthorized",
    402: "Payment Required",
    403: "Forbidden",
    404: "Not Found",
    405: "Method Not Allowed",
    406: "Not Acceptable",
    407: "Proxy Authentication Required",
    408: "Request Timeout",
    409: "Conflict",
    410: "Gone",
    411: "Length Required",
    412: "Precondition Failed",
    413: "Request Entity Too Large",
    414: "Request URI Too Long",
    415: "Unsupported Media Type",
    416: "Requested Range Not Satisfiable",
    417: "Expectation Failed",
    418: "I'm a teapot",
    421: "Misdirected Request",
    422: "Unprocessable Entity",
    423: "Locked",
    424: "Failed Dependency",
    425: "Too Early",
    426: "Upgrade Required",
    428: "Precondition Required",
    429: "Too Many Requests",
    431: "Request Header Fields Too Large",
    451: "Unavailable For Legal Reasons",
    500: "Internal Server Error",
    501: "Not Implemented",
    502: "Bad Gateway",
    503: "Service Unavailable",
    504: "Gateway Timeout",
    505: "HTTP Version Not Supported",
    506: "Variant Also Negotiates",
    507: "Insufficient Storage",
    508: "Loop Detected",
    510: "Not Extended",
    511: "Network Authentication Required",
  };
  forEach(dict, (value, key) => {
    statusTextMap.set(key.toString(), value);
  });
})();

export function getStatusText(code: number) {
  return statusTextMap.get(code.toString()) || "";
}

export function getResponseBody(resp: HTTPResponse): ResponseBodyResult {
  const { headers, body } = resp;
  let category = ResponseBodyCategory.Binary;
  let data = body;
  let size = -1;
  let json: Map<string, unknown> = new Map<string, unknown>();
  let isJSON = false;
  headers.forEach((values, key) => {
    const k = key.toLowerCase();
    switch (k) {
      case "content-type":
        {
          const mimeTextReg = /text|javascript/gi;
          const value = values.join(" ");
          if (value.includes(applicationJSON)) {
            category = ResponseBodyCategory.JSON;
            data = decode(data);
            json = JSON.parse(data);
            isJSON = true;
            // format
            data = JSON.stringify(json, null, 4);
          } else if (mimeTextReg.test(value)) {
            category = ResponseBodyCategory.Text;
            data = decode(data);
          }
        }
        break;
      case "content-length":
        {
          const v = Number.parseInt(values[0]);
          if (!Number.isNaN(v)) {
            size = v;
          }
        }
        break;
    }
  });
  if (size < 0) {
    size = Math.ceil((body.length / 4) * 3);
  }

  const result: ResponseBodyResult = {
    category,
    data,
    size,
  };
  if (isJSON) {
    result.json = json;
  }
  return result;
}

// 缓存的response数据
export interface Response {
  resp: HTTPResponse;
  createdAt: string;
}

// 每个响应保存的记录限制数
const limit = 10;

export async function addLatestResponse(resp: HTTPResponse) {
  resp.id = ulid();
  const id = resp.api;
  const store = getLatestResponseStore();
  if (!id || !store) {
    return;
  }
  const arr = (await store.getItem<Response[]>(id)) || [];
  if (arr.length >= limit) {
    arr.pop();
  }
  // 添加至顶部
  arr.unshift({
    resp,
    createdAt: dayjs().format(),
  });
  await store.setItem(id, arr);
}

export async function getLatestResponseList(id: string) {
  const arr = (await getLatestResponseStore().getItem<Response[]>(id)) || [];
  return arr;
}

export async function clearLatestResponseList(id: string) {
  const store = getLatestResponseStore();
  if (!id || !store) {
    return;
  }
  await store.setItem(id, []);
}

export async function getLatestResponse(id: string) {
  const arr = await getLatestResponseList(id);
  if (arr && arr.length) {
    return arr[0].resp;
  }
}

export function onSelectResponse(ln: (resp: HTTPResponse) => void) {
  const fn = (resp: HTTPResponse) => {
    ln(resp);
  };

  emitter.on(selectEvent, fn);
  return () => {
    emitter.off(selectEvent, fn);
  };
}

export function selectResponse(resp: HTTPResponse) {
  emitter.emit(selectEvent, resp);
}


================================================
FILE: src/commands/import_api.ts
================================================
import { Promise } from "bluebird";
import { get, uniq, forEach, has } from "lodash-es";
import dayjs from "dayjs";
import { ulid } from "ulid";
import { SettingType } from "../stores/api_setting";
import { APIFolder, createAPIFolder, newDefaultAPIFolder } from "./api_folder";
import {
  APISetting,
  createAPISetting,
  newDefaultAPISetting,
} from "./api_setting";
import { ContentType, HTTPRequest } from "./http_request";
import { KVParam } from "./interface";
import {
  createVariable,
  newDefaultVariable,
  Variable,
  VariableCategory,
} from "./variable";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import parseCurl from "../helpers/curl";
import { i18nCommon } from "../i18n";

interface PostManSetting {
  name: string;
  item?: PostManSetting[];
  request?: {
    method: string;
    url: {
      raw: string;
    };
    query: {
      key: string;
      value: string;
    }[];
    body: {
      mode: string;
      raw: string;
    };
  };
}

interface InsomniaSetting {
  parentId: string;
  url: string;
  name: string;
  method: string;
  sort: number;
  data: Map<string, string>;
  body: {
    mimeType: string;
    text: string;
  };
  headers: {
    name: string;
    value: string;
  }[];
  parameters: {
    name: string;
    value: string;
  }[];
  _id: string;
  _type: string;
}

interface ImportData {
  settings: APISetting[];
  folders: APIFolder[];
}

export enum ImportCategory {
  PostMan = "postMan",
  Insomnia = "insomnia",
  Swagger = "swagger",
  File = "file",
  Text = "text",
}

function convertPostManAPISetting(item: PostManSetting, collection: string) {
  if (!item.request) {
    return;
  }
  const setting = newDefaultAPISetting();
  setting.category = SettingType.HTTP;
  setting.collection = collection;
  setting.name = item.name;
  let contentType = "";
  const body = item.request.body?.raw;
  if (body && body.startsWith("{") && body.endsWith("}")) {
    contentType = ContentType.JSON;
  }
  const query: KVParam[] = [];
  item.request.query?.forEach((q) => {
    query.push({
      key: q.key,
      value: q.value,
      enabled: true,
    });
  });
  // TODO headers的处理
  let uri = item.request.url?.raw || "";
  if (uri && uri.includes("?")) {
    const arr = uri.split("?");
    uri = arr[0];
    // 去除前面host+path部分
    arr.shift();
    const url = new URL(`http://localhost/?${arr.join("?")}`);
    url.searchParams.forEach((value, key) => {
      query.push({
        key,
        value,
        enabled: true,
      });
    });
  }

  const req: HTTPRequest = {
    headers: [],
    method: item.request.method,
    uri,
    contentType,
    query,
    body: body,
    auth: [],
  };
  setting.setting = JSON.stringify(req);
  return setting;
}

function convertSwaggerSetting(params: {
  result: ImportData;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
  json: any;
  collection: string;
  environments: Variable[];
}) {
  const { result, json, collection, environments } = params;
  const name = get(json, "info.title") || "basePath";
  const basePathENV = newDefaultVariable();
  basePathENV.category = VariableCategory.Environment;
  basePathENV.name = name.replace(/ /g, "");
  basePathENV.value = `${get(json, "schemes.0")}://${get(json, "host")}${get(
    json,
    "basePath",
  )}`;
  environments.push(basePathENV);
  const folderDict = new Map<string, APIFolder>();
  forEach(json.paths, (value, uri) => {
    forEach(value, (data, method) => {
      const setting = newDefaultAPISetting();
      setting.collection = collection;
      setting.category = SettingType.HTTP;
      setting.name = get(data, "summary") || get(data, "operationId");
      const query: KVParam[] = [];
      const headers: KVParam[] = [];
      let contentType = "";
      let body = "";

      forEach(get(data, "parameters"), (param) => {
        if (param.in === "query") {
          query.push({
            key: param.name,
            value: get(param, "example") || "",
            enabled: true,
          });
        } else if (param.in === "body") {
          contentType = ContentType.JSON;
          const defineKey: string = param.schema?.$ref?.substring(2);
          const bodyData: Record<string, unknown> = {};
          forEach(
            get(json, defineKey.replace(/\//g, ".") + ".properties"),
            (value, key) => {
              const v = value.example;
              if (value.type === "boolean") {
                bodyData[key] = v === "true";
              } else if (value.type !== "string") {
                bodyData[key] = Number(v || 0);
              } else {
                bodyData[key] = v || "";
              }
            },
          );
          body = JSON.stringify(bodyData, null, 4);
        }
      });
      const req: HTTPRequest = {
        headers,
        method: method.toUpperCase(),
        uri: `{{${basePathENV.name}}}${uri}`,
        contentType,
        query,
        body,
        auth: [],
      };
      setting.setting = JSON.stringify(req);
      result.settings.push(setting);

      const tag = get(data, "tags.0");
      if (tag) {
        let folder = folderDict.get(tag);
        if (!folder) {
          folder = newDefaultAPIFolder();
          folder.collection = collection;
          folder.name = tag;
          folderDict.set(tag, folder);
          result.folders.push(folder);
        }
        const children = folder.children.split(",");
        children.push(setting.id);
        folder.children = children.join(",");
      }
    });
  });
}

function convertPostManSetting(params: {
  result: ImportData;
  items: PostManSetting[];
  collection: string;
  parentChildren: string[];
}) {
  const { result, items, collection, parentChildren } = params;
  if (!items || items.length === 0) {
    return;
  }
  items.forEach((item) => {
    // api 接口
    if (item.request) {
      const setting = convertPostManAPISetting(item, collection);
      if (setting) {
        result.settings.push(setting);
        parentChildren.push(setting.id);
      }
    } else {
      // folder
      const folder = newDefaultAPIFolder();
      result.folders.push(folder);
      parentChildren.push(folder.id);
      folder.collection = collection;
      folder.name = item.name;
      const subChildren: string[] = [];
      convertPostManSetting({
        result,
        items: item.item || [],
        collection,
        parentChildren: subChildren,
      });
      folder.children = subChildren.join(",");
    }
  });
}

function convertInsomniaSetting(params: {
  result: ImportData;
  items: InsomniaSetting[];
  collection: string;
}) {
  const { result, collection, items } = params;
  const subChildrenMap: Map<string, string[]> = new Map();
  const parentIDMap: Map<string, string> = new Map();
  const addToParent = (id: string, parentID: string) => {
    const pid = parentIDMap.get(parentID);
    if (pid) {
      const arr = subChildrenMap.get(pid) || [];
      arr.push(id);
      subChildrenMap.set(pid, arr);
    }
  };
  items.forEach((item) => {
    if (item._type === "request_group") {
      // folder
      const folder = newDefaultAPIFolder();
      result.folders.push(folder);
      folder.collection = collection;
      folder.name = item.name;
      subChildrenMap.set(folder.id, []);
      parentIDMap.set(item._id, folder.id);
      addToParent(folder.id, item.parentId);
      return;
    }
    const setting = newDefaultAPISetting();
    setting.category = SettingType.HTTP;
    setting.collection = collection;
    setting.name = item.name;
    const body = item.body.text;
    let contentType = "";
    if (body && body.startsWith("{") && body.endsWith("}")) {
      contentType = ContentType.JSON;
    }
    const query: KVParam[] = [];
    item.parameters?.forEach((q) => {
      if (!q.value && !q.name) {
        return;
      }
      query.push({
        key: q.name,
        value: q.value,
        enabled: true,
      });
    });
    const headers: KVParam[] = [];
    item.headers?.forEach((q) => {
      if (!q.value && !q.name) {
        return;
      }
      headers.push({
        key: q.name,
        value: q.value,
        enabled: true,
      });
    });
    let uri = item.url || "";
    if (uri && uri.includes("?")) {
      const arr = uri.split("?");
      uri = arr[0];
      // 去除前面host+path部分
      arr.shift();
      const url = new URL(`http://localhost/?${arr.join("?")}`);
      url.searchParams.forEach((value, key) => {
        query.push({
          key,
          value,
          enabled: true,
        });
      });
    }
    const req: HTTPRequest = {
      headers: headers,
      method: item.method,
      uri,
      contentType,
      query,
      body: body,
      auth: [],
    };
    setting.setting = JSON.stringify(req);
    addToParent(setting.id, item.parentId);
    result.settings.push(setting);
  });

  result.folders.forEach((folder) => {
    const arr = subChildrenMap.get(folder.id);
    if (arr) {
      folder.children = arr.join(",");
    }
  });
}

export async function importAPI(params: {
  category: ImportCategory;
  collection: string;
  fileData: string;
}): Promise<string[]> {
  let category = params.category;
  const { collection } = params;
  const result: ImportData = {
    settings: [],
    folders: [],
  };
  if (params.fileData.startsWith("curl")) {
    const req = parseCurl(params.fileData);
    const id = ulid();

    await createAPISetting({
      category: SettingType.HTTP,
      collection: params.collection,
      name: i18nCommon("untitled"),
      id,
      setting: JSON.stringify(req),
      createdAt: dayjs().format(),
      updatedAt: dayjs().format(),
    });
    return [id];
  }
  const json = JSON.parse(params.fileData);
  const environments: Variable[] = [];
  if (has(json, "swagger")) {
    category = ImportCategory.Swagger;
  } else if (has(json, "item")) {
    category = ImportCategory.PostMan;
  } else if (has(json, "resources")) {
    category = ImportCategory.Insomnia;
  }

  switch (category) {
    case ImportCategory.Swagger: {
      convertSwaggerSetting({
        result,
        json,
        collection,
        environments,
      });
      break;
    }
    case ImportCategory.PostMan:
      {
        if (!Array.isArray(json.item)) {
          return [];
        }
        const arr = json.item as PostManSetting[];
        convertPostManSetting({
          result,
          items: arr,
          collection,
          parentChildren: [],
        });

        forEach(json.variable as [], (item: { key: string; value: string }) => {
          const env = newDefaultVariable();
          env.category = VariableCategory.Environment;
          env.name = item.key;
          env.value = item.value;
          environments.push(env);
        });
      }
      break;
    case ImportCategory.Insomnia:
      {
        const items = get(json, "resources");
        if (!Array.isArray(items)) {
          return [];
        }
        let arr = items as InsomniaSetting[];
        arr.forEach((item) => {
          if (item._type === "environment") {
            forEach(item.data, (value, key) => {
              const env = newDefaultVariable();
              env.category = VariableCategory.Environment;
              env.name = key;
              env.value = value as string;
              environments.push(env);
            });
            return;
          }
          if (item._type === "request_group") {
            if (item.parentId.startsWith("wrk_")) {
              item.sort = 0;
            } else {
              item.sort = 1;
            }
          } else {
            item.sort = 2;
          }
        });
        arr = arr.filter((item) =>
          ["request", "request_group"].includes(item._type),
        );

        arr.sort((item1, item2) => {
          return item1.sort - item2.sort;
        });
        convertInsomniaSetting({
          result,
          items: arr,
          collection,
        });
      }
      break;
    case ImportCategory.Text:
    case ImportCategory.File:
      {
        const arr = Array.isArray(json) ? json : [json];
        arr.forEach((item) => {
          item.collection = collection;
          if (item.category === SettingType.HTTP) {
            result.settings.push(item);
          } else {
            result.folders.push(item);
          }
        });
      }
      break;
    default:
      throw new Error(`${category} is not supported`);
      break;
  }

  const topIDList: string[] = [];
  const childrenIDMap: Map<string, boolean> = new Map();
  result.folders.forEach((item) => {
    const children = item.children?.split(",") || [];
    children.forEach((id) => {
      childrenIDMap.set(id, true);
    });
  });

  await Promise.each(result.folders, async (item) => {
    if (!childrenIDMap.has(item.id)) {
      topIDList.push(item.id);
    }
    await createAPIFolder(item);
  });
  await Promise.each(result.settings, async (item) => {
    if (!childrenIDMap.has(item.id)) {
      topIDList.push(item.id);
    }
    await createAPISetting(item);
  });
  await Promise.each(environments, async (item) => {
    if (!item.name && !item.value) {
      return;
    }
    item.collection = collection;
    await createVariable(item);
  });
  return uniq(topIDList);
}


================================================
FILE: src/commands/interface.ts
================================================
export interface KVParam {
  [key: string]: unknown;
  key: string;
  value: string;
  enabled: boolean;
}


================================================
FILE: src/commands/invoke.ts
================================================
import { invoke, InvokeArgs } from "@tauri-apps/api/tauri";
import Debug from "debug";
import { isWebMode } from "../helpers/util";

export const cmdInitTables = "init_tables";
export const cmdExportTables = "export_tables";
export const cmdImportTables = "import_tables";

export const cmdAddAPISetting = "add_api_setting";
export const cmdUpdateAPISetting = "update_api_setting";
export const cmdListAPISetting = "list_api_setting";
export const cmdDeleteAPISettings = "delete_api_settings";

export const cmdAddAPIFolder = "add_api_folder";
export const cmdListAPIFolder = "list_api_folder";
export const cmdUpdateAPIFolder = "update_api_folder";
export const cmdDeleteAPIFolder = "delete_api_folder";

export const cmdAddAPICollection = "add_api_collection";
export const cmdUpdateAPICollection = "update_api_collection";
export const cmdListAPICollection = "list_api_collection";
export const cmdDeleteAPICollection = "delete_api_collection";

export const cmdDoHTTPRequest = "do_http_request";

export const cmdListCookie = "list_cookie";
export const cmdDeleteCookie = "delete_cookie";
export const cmdAddCookie = "add_cookie";
export const cmdClearCookie = "clear_cookie";

export const cmdAddVariable = "add_variable";
export const cmdUpdateVariable = "update_variable";
export const cmdDeleteVariable = "delete_variable";
export const cmdListVariable = "list_variable";

export const cmdGetLatestVersion = "get_latest_version";
export const cmdAddVersion = "add_version";

const debug = Debug("invoke");
export async function run<T>(cmd: string, args?: InvokeArgs): Promise<T> {
  if (isWebMode()) {
    debug("invoke, cmd:%s, args:%o", cmd, args);
    // eslint-disable-next-line
    // @ts-ignore: mock
    return Promise.resolve(null);
  }
  try {
    const result = await invoke<T>(cmd, args);
    debug("invoke, result:%o", result);
    return result;
  } catch (err) {
    // eslint-disable-next-line
    // @ts-ignore: mock
    const message = `[${err.category}]${err.message}`;
    throw new Error(message);
  }
}


================================================
FILE: src/commands/variable.ts
================================================
import { ulid } from "ulid";
import dayjs from "dayjs";
import { isWebMode } from "../helpers/util";
import {
  cmdAddVariable,
  cmdDeleteVariable,
  cmdListVariable,
  cmdUpdateVariable,
  run,
} from "./invoke";
import { fakeList, fakeAdd, fakeUpdate, fakeUpdateStore } from "./fake";

const store = "variables";

export enum VariableStatus {
  Enabled = "1",
  Disabled = "0",
}

export enum VariableCategory {
  // 环境变量
  Environment = "env",
  // 自定义
  Customize = "customize",
  // 全局请求头
  GlobalReqHeaders = "globalReqHeaders",
}

export interface Variable {
  [key: string]: unknown;
  id: string;
  category: string;
  collection: string;
  // 名称
  name: string;
  // 值
  value: string;
  // 是否启用(0:禁用 1:启用)
  enabled: string;
  // 创建时间
  createdAt: string;
  // 更新时间
  updatedAt: string;
}

export function newDefaultVariable(): Variable {
  const id = ulid();
  return {
    id,
    category: "",
    collection: "",
    name: "",
    value: "",
    enabled: VariableStatus.Enabled,
    createdAt: dayjs().format(),
    updatedAt: dayjs().format(),
  };
}

export async function createVariable(value: Variable) {
  if (isWebMode()) {
    await fakeAdd<Variable>(store, value);
  }
  await run(cmdAddVariable, {
    value,
  });
}

export async function listVariable(
  collection: string,
  category: string,
): Promise<Variable[]> {
  if (isWebMode()) {
    return await fakeList<Variable>(store);
  }
  return await run<Variable[]>(cmdListVariable, {
    collection,
    category,
  });
}

export async function updateVariable(value: Variable) {
  if (isWebMode()) {
    return await fakeUpdate(store, value);
    return;
  }
  await run(cmdUpdateVariable, {
    value,
  });
}

export async function deleteVariable(ids: string[]) {
  if (isWebMode()) {
    const arr = await fakeList<Variable>(store);
    const result = arr.filter((item) => {
      return !ids.includes(item.id);
    });
    await fakeUpdateStore(store, result);
  }
  await run(cmdDeleteVariable, {
    ids,
  });
}


================================================
FILE: src/commands/window.ts
================================================
import { run } from "./invoke";
import { appWindow, LogicalSize, getAll } from "@tauri-apps/api/window";
import { isWebMode } from "../helpers/util";

export function closeSplashscreen() {
  run("close_splashscreen");
}

export function showSplashscreen() {
  if (isWebMode()) {
    return;
  }
  getAll().forEach((item) => {
    if (item.label === "splashscreen") {
      item.show();
    }
  });
}

export async function setWindowSize(width: number, height: number) {
  if (isWebMode()) {
    return;
  }
  // 如果有设置小于0,则最大化
  if (width < 0 || height < 0) {
    await appWindow.maximize();
  } else if (width > 0 && height > 0) {
    await appWindow.setSize(new LogicalSize(width, height));
  }
}


================================================
FILE: src/components/APIResponse/index.tsx
================================================
import {
  defineComponent,
  onBeforeUnmount,
  onMounted,
  PropType,
  ref,
  StyleValue,
  watch,
} from "vue";
import { css } from "@linaria/core";
import prettyBytes from "pretty-bytes";
import {
  BowlingBallOutline,
  InformationCircleOutline,
  LinkOutline,
} from "@vicons/ionicons5";
import { editor } from "monaco-editor/esm/vs/editor/editor.api";

import {
  HTTPResponse,
  ResponseBodyResult,
  getLatestResponse,
  getResponseBody,
  HTTPStats,
} from "../../commands/http_response";
import { useSettingStore } from "../../stores/setting";
import {
  NDescriptions,
  NDescriptionsItem,
  NDivider,
  NIcon,
  NPopover,
  NSpace,
  useMessage,
} from "naive-ui";
import { useRoute } from "vue-router";
import { padding } from "../../constants/style";
import { replaceContent, createEditor } from "../../helpers/editor";
import { i18nCollection, i18nCommon } from "../../i18n";
import { convertRequestToCURL, HTTPRequest } from "../../commands/http_request";
import {
  convertHTTPHeaderName,
  showError,
  writeFileToDownload,
  writeTextToClipboard,
  formatLatency,
} from "../../helpers/util";
import ExPreview, { isSupportPreview } from "../ExPreview";
import ExTimer from "../ExTimer";
import APIResponseList from "./list";
import APIResponseStatusText from "./status_text";
import { toUint8Array } from "js-base64";
import { useCookieStore } from "../../stores/cookie";
import { useAPISettingStore } from "../../stores/api_setting";

const responseClass = css`
  margin-left: 5px;
  margin-right: 2px;
  .infos {
    height: 48px;
    line-height: 48px;
    padding: 0 ${padding}px;
  }
  .codeEditor,
  .previewWrapper {
    position: absolute;
    top: 50px;
    left: 5px;
    right: 2px;
    bottom: 0;
    overflow: auto;
  }
  .previewWrapper {
    z-index: 99;
    background-color: rgb(255, 255, 255);
  }
  .previewWrapper.isDark {
    background-color: rgb(30, 30, 30);
  }

  .n-divider {
    margin: 0;
  }
  .info {
    cursor: pointer;
    float: left;
    margin-top: 15px;
    padding: 0 2px;
    font-size: 20px;
    font-weight: 600;
  }
  .cookie {
    margin-top: 16px;
  }
  .header {
    padding: 0 5px;
    cursor: pointer;
    font-size: 16px;
  }
  .responseList {
    float: right;
  }
`;

const showCurlLimitSize = 2 * 1024;
export default defineComponent({
  name: "APIResponse",
  props: {
    response: {
      type: Object as PropType<HTTPResponse>,
      default: () => {
        return {
          status: -1,
          headers: new Map<string, string[]>(),
          body: "",
        };
      },
    },
  },
  setup(props) {
    const route = useRoute();
    const message = useMessage();
    const settingStore = useSettingStore();
    const cookieStore = useCookieStore();
    const apiSettingStore = useAPISettingStore();
    let editorIns: editor.IStandaloneCodeEditor | null;
    const destroy = () => {
      if (editorIns) {
        editorIns = null;
      }
    };
    const statusCode = ref(0);
    const size = ref(-1);
    const originalSize = ref(-1);
    const latency = ref(0);
    const apiID = ref("");
    const stats = ref({} as HTTPStats);
    const headers = ref(new Map<string, string[]>());
    const collection = route.query.collection as string;

    const previewMode = ref(false);
    const previewData = ref({
      contentType: "",
      data: "",
    });

    let req: HTTPRequest;

    const reqExists = ref(false);
    const curl = ref("");

    const fillValues = async (resp: HTTPResponse) => {
      // 初始加载时,读取最近的响应
      let isFromCache = false;
      if (!resp.status) {
        const tmp = await getLatestResponse(resp.api);
        isFromCache = true;
        if (tmp) {
          resp = tmp;
        }
      }
      statusCode.value = resp.status;
      let body = {
        size: -1,
      } as ResponseBodyResult;
      if (resp.body) {
        body = getResponseBody(resp);
      }

      let contentType = "";
      let filename = "";
      resp.headers?.forEach((values, key) => {
        const k = key.toLowerCase();
        switch (k) {
          case "content-type":
            contentType = values[0];
            break;
          case "content-disposition":
            {
              const reg = /filename="([\s\S]*?)"/;
              const result = reg.exec(values[0]);
              if (result?.length === 2) {
                filename = result[1];
              }
            }
            break;
        }
      });

      if (isSupportPreview(contentType)) {
        previewMode.value = true;
        previewData.value = {
          contentType,
          data: body.data,
        };
        editorIns?.setValue("");
      } else {
        previewMode.value = false;
      }
      originalSize.value = resp.bodySize;
      size.value = body.size;
      latency.value = resp.latency;
      apiID.value = resp.api;
      headers.value = resp.headers;
      req = resp.req;
      if (!req) {
        reqExists.value = false;
      } else {
        reqExists.value = true;
      }
      curl.value = "";
      stats.value = resp.stats;
      if (filename) {
        if (!isFromCache) {
          writeFileToDownload(filename, toUint8Array(body.data))
            .then(() => {
              message.info(i18nCommon("saveToDownloadSuccess"));
            })
            .catch((err) => {
              showError(message, err);
            });
        }
      } else if (!previewMode.value) {
        replaceContent(editorIns, body.data);
      }
    };

    const handleToCURL = async () => {
      if (!req || curl.value) {
        return;
      }
      try {
        // 由于cookie会一直更新,因此此时再拉取
        await cookieStore.fetch();
        // req 对象为未调用uri部分,需要先调整
        apiSettingStore.fillValues(req);
        const value = await convertRequestToCURL(
          collection,
          req,
          cookieStore.cookies,
        );
        if (value.length > showCurlLimitSize) {
          await writeTextToClipboard(value);
        }
        curl.value = value;
      } catch (err) {
        showError(message, err);
      }
    };

    const stop = watch(
      () => props.response,
      (resp) => {
        fillValues(resp);
        editorIns?.setScrollTop(0);
      },
    );

    const codeEditor = ref<HTMLElement>();
    const initEditor = () => {
      if (codeEditor.value) {
        editorIns = createEditor({
          readonly: true,
          dom: codeEditor.value,
          isDark: settingStore.isDark,
        });
      }
    };

    onMounted(() => {
      initEditor();
      if (props.response.api) {
        fillValues(props.response);
      }
    });
    onBeforeUnmount(() => {
      destroy();
      stop();
    });

    return {
      curl,
      reqExists,
      headers,
      size,
      originalSize,
      stats,
      latency,
      statusCode,
      apiID,
      previewMode,
      previewData,
      codeEditor,
      handleToCURL,
      isDark: settingStore.isDark,
    };
  },
  render() {
    const {
      statusCode,
      size,
      originalSize,
      latency,
      apiID,
      curl,
      headers,
      reqExists,
      stats,
      previewMode,
      previewData,
    } = this;
    let statusCodeInfo = <span></span>;
    if (statusCode === -1) {
      statusCodeInfo = (
        <span>
          {i18nCollection("requesting")} <ExTimer />
        </span>
      );
    } else if (statusCode) {
      statusCodeInfo = <APIResponseStatusText statusCode={statusCode} />;
    }

    const apiIDSlots = {
      trigger: () => (
        <NIcon class="info" size={20}>
          <InformationCircleOutline />
        </NIcon>
      ),
    };

    const headerSlots = {
      trigger: () => <span class="header">H</span>,
    };

    const curlSlots = {
      trigger: () => (
        <NIcon class="info" size={20}>
          <LinkOutline />
        </NIcon>
      ),
    };

    const cookieSlots = {
      trigger: () => (
        <NIcon class="info cookie" size={17}>
          <BowlingBallOutline />
        </NIcon>
      ),
    };
    const isTooLarge = curl.length > showCurlLimitSize;
    let curlText = i18nCollection("curlTooLargeTips");
    if (!isTooLarge) {
      curlText = curl;
    }
    if (curl.length === 0) {
      curlText = i18nCollection("curlGenerateFail");
    }
    const curlStyle: StyleValue = isTooLarge
      ? {}
      : {
          width: "400px",
          "word-break": "break-all",
          "word-wrap": "break-word",
        };

    const descriptionItemOptions = [
      {
        label: i18nCollection("apiID"),
        key: "apiID",
        value: apiID,
      },
    ];
    if (stats?.remoteAddr) {
      descriptionItemOptions.push({
        label: i18nCollection("remoteAddr"),
        key: "remoteAddr",
        value: stats.remoteAddr,
      });
    }
    if (stats?.cipher) {
      descriptionItemOptions.push({
        label: i18nCollection("cipher"),
        key: "cipher",
        value: `${stats.cipher}`,
      });
    }

    if (stats?.dnsLookup) {
      descriptionItemOptions.push({
        label: i18nCollection("dns"),
        key: "dns",
        value: formatLatency(stats.dnsLookup),
      });
    }
    if (stats) {
      descriptionItemOptions.push({
        label: i18nCollection("tcp"),
        key: "tcp",
        value: formatLatency(stats.tcp),
      });
      if (stats.isHttps) {
        descriptionItemOptions.push({
          label: i18nCollection("tls"),
          key: "tls",
          value: formatLatency(stats.tls),
        });
      }
      descriptionItemOptions.push(
        {
          label: i18nCollection("send"),
          key: "send",
          value: formatLatency(stats.send),
        },
        {
          label: i18nCollection("serverProcessing"),
          key: "serverProcessing",
          value: formatLatency(stats.serverProcessing),
        },
        {
          label: i18nCollection("contentTransfer"),
          key: "contentTransfer",
          value: formatLatency(stats.contentTransfer),
        },
      );
    }

    const headerDescriptionItems: JSX.Element[] = [];
    const cookieDescriptionItems: JSX.Element[] = [];
    const setCookieKey = "set-cookie";
    if (headers && headers.size !== 0) {
      headers.forEach((values, key) => {
        values.forEach((value, index) => {
          if (key === setCookieKey) {
            const cookie = value.split(";")[0];
            if (!cookie) {
              return;
            }
            const arr = cookie.split("=");
            cookieDescriptionItems.push(
              <NDescriptionsItem label={arr[0]} key={`${arr[0]}`} span={3}>
                {arr.slice(1).join("=")}
              </NDescriptionsItem>,
            );
            return;
          }
          headerDescriptionItems.push(
            <NDescriptionsItem
              label={convertHTTPHeaderName(key)}
              key={`${key}-${index}`}
              span={3}
            >
              {value}
            </NDescriptionsItem>,
          );
        });
      });
    }

    const descriptionItems = descriptionItemOptions.map((item) => {
      return (
        <NDescriptionsItem label={item.label} key={item.key} span={3}>
          {item.value}
        </NDescriptionsItem>
      );
    });

    const codeEditorCls = {
      hidden: false,
    };
    if (previewMode) {
      codeEditorCls.hidden = true;
    }
    const previewWrapperCls = {
      previewWrapper: true,
      isDark: false,
    };
    if (this.isDark) {
      previewWrapperCls.isDark = true;
    }

    const popupContentStyle: StyleValue = {
      maxWidth: "600px",
      wordBreak: "break-all",
      wordWrap: "break-word",
    };

    const sizeDesc: string[] = [];
    if (size > 0) {
      sizeDesc.push(prettyBytes(size));
      if (originalSize > 0 && Math.abs(size - originalSize) > 100) {
        const percent = `${Math.ceil((originalSize * 100) / size)}%`;
        sizeDesc.push(`${prettyBytes(originalSize)}(${percent})`);
      }
    }

    return (
      <div class={responseClass}>
        {apiID && (
          <div class="responseList">
            {" "}
            <APIResponseList id={apiID} />{" "}
          </div>
        )}
        <NSpace class="infos">
          {apiID && (
            <NPopover v-slots={apiIDSlots} trigger="click" placement="bottom">
              <NDescriptions labelPlacement="left" size="small">
                {descriptionItems}
              </NDescriptions>
            </NPopover>
          )}
          {cookieDescriptionItems.length !== 0 && (
            <NPopover
              v-slots={cookieSlots}
              trigger="click"
              placement="bottom"
              contentStyle={popupContentStyle}
            >
              <NDescriptions labelPlacement="left" size="small">
                {cookieDescriptionItems}
              </NDescriptions>
            </NPopover>
          )}
          {headerDescriptionItems.length !== 0 && (
            <NPopover v-slots={headerSlots} trigger="click" placement="bottom">
              <NDescriptions
                contentStyle={popupContentStyle}
                labelPlacement="left"
                size="small"
              >
                {headerDescriptionItems}
              </NDescriptions>
            </NPopover>
          )}
          {reqExists && (
            <NPopover
              v-slots={curlSlots}
              trigger="click"
              placement="bottom"
              onUpdateShow={(value) => {
                if (value) {
                  this.handleToCURL();
                }
              }}
            >
              <div style={curlStyle}>{curlText}</div>
            </NPopover>
          )}
          {statusCodeInfo}
          {/* 占位 */}
          <span> </span>
          {sizeDesc.length !== 0 && sizeDesc.join(" / ")}
          {/* 占位 */}
          <span> </span>
          {latency > 0 && formatLatency(latency)}
        </NSpace>
        <NDivider />
        {codeEditorCls.hidden && (
          <div class={previewWrapperCls}>
            <ExPreview
              contentType={previewData.contentType}
              data={previewData.data}
            />
          </div>
        )}
        <div ref="codeEditor" class="codeEditor"></div>
      </div>
    );
  },
});


================================================
FILE: src/components/APIResponse/list.tsx
================================================
import {
  NButton,
  NDivider,
  NDropdown,
  NIcon,
  NP,
  NSpace,
  useMessage,
} from "naive-ui";
import { css } from "@linaria/core";
import prettyBytes from "pretty-bytes";
import { isNumber } from "lodash-es";

import { defineComponent, ref } from "vue";
import {
  clearLatestResponseList,
  getLatestResponseList,
  Response,
  selectResponse,
} from "../../commands/http_response";
import { formatSimpleDate, showError } from "../../helpers/util";
import { ListOutline, TrashOutline } from "@vicons/ionicons5";
import { DropdownMixedOption } from "naive-ui/es/dropdown/src/interface";
import { i18nStore } from "../../i18n";

const showMoreClass = css`
  cursor: pointer;
  margin: 15px 10px 0 0;
  padding: 0 5px;
`;

export default defineComponent({
  name: "APIResponseList",
  props: {
    id: {
      type: String,
      required: true,
    },
  },
  setup(props) {
    const message = useMessage();
    const responseList = ref([] as Response[]);

    const handleFetch = async () => {
      responseList.value.length = 0;
      try {
        const arr = await getLatestResponseList(props.id);
        responseList.value = arr;
      } catch (err) {
        showError(message, err);
      }
    };
    const handleSelect = (index: number) => {
      const resp = responseList.value[index];
      if (resp) {
        selectResponse(resp.resp);
      }
    };
    const handleClearHistory = async () => {
      try {
        await clearLatestResponseList(props.id);
        message.info(i18nStore("clearHistorySuccess"));
        responseList.value.length = 0;
      } catch (err) {
        showError(message, err);
      }
    };
    return {
      responseList,
      handleFetch,
      handleSelect,
      handleClearHistory,
    };
  },
  render() {
    const { responseList } = this;
    const options: DropdownMixedOption[] = responseList.map((item, index) => {
      let bodySize = "--";
      if (item.resp && isNumber(item.resp.bodySize)) {
        bodySize = prettyBytes(item.resp.bodySize);
      }
      return {
        label: () => (
          <NSpace>
            {item.resp.status}
            {bodySize}
            {formatSimpleDate(item.createdAt)}
          </NSpace>
        ),
        key: index,
      };
    });
    const clearHistorySlots = {
      icon: () => (
        <NIcon>
          <TrashOutline />
        </NIcon>
      ),
    };

    const clearBtn = responseList?.length !== 0 && (
      <NButton
        class="widthFull"
        v-slots={clearHistorySlots}
        quaternary
        onClick={() => {
          this.handleClearHistory();
        }}
      >
        {i18nStore("clearHistory")}
      </NButton>
    );

    const tips = responseList?.length === 0 && (
      <NP
        style={{
          margin: "5px 15px",
        }}
      >
        {i18nStore("noHistory")}
      </NP>
    );
    options.unshift({
      key: "header",
      type: "render",
      render: () => (
        <div>
          {clearBtn}
          <NDivider
            titlePlacement="left"
            style={{
              margin: "5px 0",
              "font-size": "12px",
            }}
          >
            {i18nStore("responseList")}
          </NDivider>
          {tips}
        </div>
      ),
    });
    return (
      <NDropdown
        trigger="click"
        placement="bottom-end"
        onSelect={(value) => {
          this.handleSelect(value);
        }}
        onUpdateShow={(value) => {
          if (value) {
            this.handleFetch();
          }
        }}
        options={options}
        showArrow={true}
      >
        <NIcon class={showMoreClass} size={20}>
          <ListOutline />
        </NIcon>
      </NDropdown>
    );
  },
});


================================================
FILE: src/components/APIResponse/status_text.tsx
================================================
import { NGradientText } from "naive-ui";
import { defineComponent } from "vue";
import { getStatusText } from "../../commands/http_response";

function getStatusType(statusCode: number) {
  if (statusCode >= 500) {
    return "error";
  }
  if (statusCode >= 400) {
    return "warning";
  }
  return "success";
}

export default defineComponent({
  name: "APIResponseStatusText",
  props: {
    statusCode: {
      type: Number,
      required: true,
    },
  },
  render() {
    const { statusCode } = this.$props;
    return (
      <NGradientText type={getStatusType(statusCode)}>
        {statusCode} {getStatusText(statusCode)}
      </NGradientText>
    );
  },
});


================================================
FILE: src/components/APISettingParams/index.tsx
================================================
import {
  defineComponent,
  watch,
  ref,
  onBeforeUnmount,
  PropType,
  VNode,
} from "vue";
import { css } from "@linaria/core";
import { NDivider, useMessage } from "naive-ui";
import { storeToRefs } from "pinia";
import { cloneDeep, debounce } from "lodash-es";

import { useAPISettingStore } from "../../stores/api_setting";
import { abortRequestID, HTTPRequest } from "../../commands/http_request";
import { showError } from "../../helpers/util";
import { i18nCollection } from "../../i18n";
import APISettingParamsURI, { RequestURI } from "./uri";
import APISettingParamsReqParams from "./req_params";
import { KVParam } from "../../commands/interface";
import { onSelectResponse } from "../../commands/http_response";

const wrapperClass = css`
  height: 100%;
  position: relative;
  margin-left: 5px;
  .n-divider {
    margin: 0;
  }
`;

export default defineComponent({
  name: "APISettingParams",
  props: {
    onSend: {
      type: Function as PropType<(id: string) => Promise<void>>,
      required: true,
    },
  },
  setup() {
    const message = useMessage();
    const apiSettingStore = useAPISettingStore();
    const { selectedID } = storeToRefs(apiSettingStore);

    const componentKey = ref(selectedID.value);
    const reqParams = ref({} as HTTPRequest);
    const reqParamsStyle = ref({
      height: "0px",
    });

    const wrapper = ref<Element>();
    let uriNodeHeight = 0;
    const caclWrapperHeight = () => {
      const height = wrapper.value?.clientHeight || 0;
      if (!height) {
        return;
      }
      reqParamsStyle.value.height = `${height - uriNodeHeight}px`;
    };

    const updateURINodeHeight = (node: VNode) => {
      uriNodeHeight = node.el?.clientHeight || 0;
      caclWrapperHeight();
    };
    const updateReqParams = (id: string) => {
      try {
        if (id) {
          reqParams.value = apiSettingStore.getHTTPRequest(id);
        } else {
          reqParams.value = {} as HTTPRequest;
        }
      } catch (err) {
        console.error(err);
      } finally {
        caclWrapperHeight();
      }
    };

    const stop = watch(selectedID, (id) => {
      componentKey.value = id;
      updateReqParams(id);
    });
    if (selectedID.value) {
      updateReqParams(selectedID.value);
    }

    const offListen = onSelectResponse((resp) => {
      reqParams.value = cloneDeep(resp.req);
      caclWrapperHeight();
      const id = resp.id || `${Date.now()}`;
      componentKey.value = `${selectedID.value}-${id}`;
    });

    onBeforeUnmount(() => {
      offListen();
      stop();
    });
    const update = async () => {
      const id = selectedID.value;
      if (!id) {
        message.warning(i18nCollection("shouldSelectAPISettingFirst"));
        return;
      }
      const data = apiSettingStore.findByID(id);
      if (!data) {
        return;
      }
      try {
        let value = "";
        if (reqParams.value) {
          value = JSON.stringify(reqParams.value);
        }
        data.setting = value;
        await apiSettingStore.updateByID(id, data);
      } catch (err) {
        showError(message, err);
      }
    };
    const handleUpdateURI = async (data: RequestURI) => {
      Object.assign(reqParams.value, data);
      await update();
    };

    const handleUpdateBody = async (
      id: string,
      params: {
        body: string;
        contentType: string;
      },
    ) => {
      // 因为是延时执行,如果已经切换,则不更新
      // 避免更新了其它接口的数据
      if (id !== selectedID.value) {
        return;
      }
      reqParams.value.contentType = params.contentType;
      reqParams.value.body = params.body;
      await update();
    };

    const newHandleUpdate = (key: string) => {
      return async (id: string, data: KVParam[]) => {
        // 因为是延时执行,如果已经切换,则不更新
        // 避免更新了其它接口的数据
        if (id !== selectedID.value) {
          return;
        }
        reqParams.value[key] = data;
        await update();
      };
    };

    const handleUpdateAuth = debounce(newHandleUpdate("auth"), 300);

    const handleUpdateQuery = debounce(newHandleUpdate("query"), 300);

    const handleUpdateHeaders = debounce(newHandleUpdate("headers"), 300);

    return {
      componentKey,
      reqParamsStyle,
      updateURINodeHeight,
      wrapper,
      selectedID,
      reqParams,
      // 避免频繁重复触发,不能设置过长
      // 如果设置过长容易导致更新了还没生效
      handleUpdateAuth,
      handleUpdateBody: debounce(handleUpdateBody, 300),
      handleUpdateURI,
      handleUpdateQuery,
      handleUpdateHeaders,
    };
  },
  render() {
    const { reqParams, selectedID, componentKey } = this;

    return (
      <div class={wrapperClass} key={`uri-${componentKey}`} ref="wrapper">
        <APISettingParamsURI
          onVnodeMounted={(node) => {
            this.updateURINodeHeight(node);
          }}
          onVnodeUpdated={(node) => {
            this.updateURINodeHeight(node);
          }}
          params={reqParams}
          onUpdateURI={(data) => {
            this.handleUpdateURI(data);
          }}
          onSubmit={(isAborted: boolean) => {
            if (isAborted) {
              return this.$props.onSend(abortRequestID);
            }
            return this.$props.onSend(selectedID);
          }}
        />
        <NDivider />
        <APISettingParamsReqParams
          key={`params-${componentKey}`}
          style={this.reqParamsStyle}
          id={selectedID}
          params={reqParams}
          onUpdateBody={(value) => {
            this.handleUpdateBody(selectedID, value);
          }}
          onUpdateAuth={(value) => {
            this.handleUpdateAuth(selectedID, value);
          }}
          onUpdateQuery={(value) => {
            this.handleUpdateQuery(selectedID, value);
          }}
          onUpdateHeaders={(value) => {
            this.handleUpdateHeaders(selectedID, value);
          }}
        />
      </div>
    );
  },
});


================================================
FILE: src/components/APISettingParams/req_params.tsx
================================================
import {
  NBadge,
  NButton,
  NButtonGroup,
  NDropdown,
  NIcon,
  NTab,
  NTabs,
  useDialog,
  useMessage,
} from "naive-ui";
import { css } from "@linaria/core";
import {
  defineComponent,
  onBeforeUnmount,
  onMounted,
  PropType,
  ref,
  watch,
} from "vue";
import { editor } from "monaco-editor/esm/vs/editor/editor.api";

import {
  HTTPMethod,
  HTTPRequest,
  ContentType,
} from "../../commands/http_request";
import { useSettingStore } from "../../stores/setting";
import { i18nCollection, i18nCommon } from "../../i18n";
import { CaretDownOutline, CodeSlashOutline } from "@vicons/ionicons5";
import { showError, tryToParseArray } from "../../helpers/util";
import ExKeyValue, { HandleOption } from "../ExKeyValue";
import { KVParam } from "../../commands/interface";
import { padding } from "../../constants/style";
import { useAPICollectionStore } from "../../stores/api_collection";
import { replaceContent, createEditor } from "../../helpers/editor";

enum TabItem {
  Body = "Body",
  Query = "Query",
  Auth = "Auth",
  Header = "Header",
}

const tabClass = css`
  position: relative;
  .expandSelect {
    visibility: hidden;
  }
  .n-tabs:hover .expandSelect {
    visibility: visible;
  }
  .n-tabs-tab__label {
    .n-icon {
      margin-left: 5px;
    }
    .contentType {
      width: 60px;
      text-align: center;
    }
  }
  .badgeTab {
    position: relative;
    .badge {
      position: absolute;
      right: -15px;
      top: 8px;
      .n-badge-sup {
        padding: 0 3px !important;
        border-radius: 3px !important;
      }
    }
  }
  .hidden {
    display: none;
  }
  .format {
    position: fixed;
    bottom: 2px;
    .n-icon {
      font-size: 16px;
      font-weight: 900;
      margin-right: 5px;
    }
  }
  .content {
    position: absolute;
    top: 40px;
    left: 0;
    right: 0;
    bottom: 0;
    overflow: hidden;
  }
  .keyValue {
    margin: ${padding}px;
  }
`;

function shouldHaveBody(method: string) {
  return [HTTPMethod.POST, HTTPMethod.PUT, HTTPMethod.PATCH].includes(
    method as HTTPMethod,
  );
}

function shouldShowEditor(contentType: string) {
  return [ContentType.JSON, ContentType.XML, ContentType.Plain].includes(
    contentType as ContentType,
  );
}

function createBadgeTab(params: {
  tab: string;
  value: number;
  activeTab: string;
}) {
  const { value, tab, activeTab } = params;
  const badge =
    value && tab !== activeTab ? (
      <NBadge class="badge" color="grey" value={value} />
    ) : null;
  return (
    <NTab class="badgeTab" name={tab}>
      {tab}
      {badge}
    </NTab>
  );
}

function createBodyBadge(params: { contentType: string; body: string }) {
  const { contentType, body } = params;
  if (
    ![ContentType.Multipart, ContentType.Form].includes(
      contentType as ContentType,
    )
  ) {
    return;
  }
  const arr = tryToParseArray(body);
  if (arr.length === 0) {
    return;
  }
  return <NBadge class="badge" color="grey" value={arr.length} />;
}

export default defineComponent({
  name: "APISettingParamsReqParams",
  props: {
    id: {
      type: String,
      default: () => "",
    },
    params: {
      type: Object as PropType<HTTPRequest>,
      required: true,
    },
    onUpdateBody: {
      type: Function as PropType<
        (params: { body: string; contentType: string }) => void
      >,
      required: true,
    },
    onUpdateQuery: {
      type: Function as PropType<(query: KVParam[]) => void>,
      required: true,
    },
    onUpdateHeaders: {
      type: Function as PropType<(headers: KVParam[]) => void>,
      required: true,
    },
    onUpdateAuth: {
      type: Function as PropType<(auth: KVParam[]) => void>,
      required: true,
    },
  },
  setup(props) {
    const settingStore = useSettingStore();
    const message = useMessage();
    const dialog = useDialog();
    const collectionStore = useAPICollectionStore();
    const codeEditor = ref<HTMLElement>();
    const contentType = ref(props.params.contentType || ContentType.JSON);

    let tab = collectionStore.getActiveTab(props.id);
    if (!tab) {
      if (shouldHaveBody(props.params.method)) {
        tab = TabItem.Body;
      } else {
        tab = TabItem.Query;
      }
    }
    const activeTab = ref(tab as TabItem);

    let editorIns: editor.IStandaloneCodeEditor | null;
    const destroy = () => {
      if (editorIns) {
        editorIns = null;
      }
    };
    const handleEditorUpdate = () => {
      if (props.onUpdateBody && editorIns) {
        props.onUpdateBody({
          body: editorIns.getValue().trim(),
          contentType: contentType.value,
        });
      }
    };
    const initEditor = () => {
      if (editorIns) {
        editorIns.setValue(props.params.body);
        return;
      }
      if (codeEditor.value) {
        editorIns = createEditor({
          dom: codeEditor.value,
          isDark: settingStore.isDark,
        });
        editorIns.setValue(props.params.body || "");
        editorIns.onDidChangeModelContent(handleEditorUpdate);
      }
    };

    const handleChangeContentType = (newContentType: string) => {
      // 如果无数据,直接切换
      const changeContentType = () => {
        // 清空
        replaceContent(editorIns, "");
        if (props.onUpdateBody) {
          props.onUpdateBody({
            body: "",
            contentType: newContentType,
          });
        }
        contentType.value = newContentType;
      };
      if (!props.params.body) {
        changeContentType();
        return;
      }
      dialog.warning({
        title: i18nCollection("changeContentType"),
        content: i18nCollection("changeContentTypeContent"),
        positiveText: i18nCommon("confirm"),
        onPositiveClick: async () => {
          changeContentType();
        },
      });
    };

    const getParamsFromHandleOption = (opt: HandleOption) => {
      const arr = [] as KVParam[];
      opt.params.forEach((item) => {
        const { key, value } = item;
        if (!key && !value) {
          return;
        }
        arr.push({
          key,
          value,
          enabled: item.enabled,
        });
      });
      return arr;
    };

    const handleBodyParams = (opt: HandleOption) => {
      const arr = getParamsFromHandleOption(opt);
      if (props.onUpdateBody) {
        props.onUpdateBody({
          body: JSON.stringify(arr),
          contentType: contentType.value,
        });
      }
    };
    const handleQueryParams = (opt: HandleOption) => {
      const arr = getParamsFromHandleOption(opt);
      if (props.onUpdateQuery) {
        props.onUpdateQuery(arr);
      }
    };

    const handleHeaders = (opt: HandleOption) => {
      const arr = getParamsFromHandleOption(opt);
      if (props.onUpdateHeaders) {
        props.onUpdateHeaders(arr);
      }
    };

    const handleAuth = (opt: HandleOption) => {
      const arr = getParamsFromHandleOption(opt);
      if (props.onUpdateAuth) {
        props.onUpdateAuth(arr);
      }
    };
    const updateParamsColumnWidth = (width: number) => {
      settingStore.updateParamsColumnWidth(width);
    };

    // method变化时要选定对应的tab
    const stop = watch(
      () => props.params.method,
      (method) => {
        if (shouldHaveBody(method)) {
          activeTab.value = TabItem.Body;
        } else {
          activeTab.value = TabItem.Query;
        }
      },
    );
    const handleUpdateActiveTab = async (activeTab: string) => {
      try {
        await collectionStore.updateActiveTab({
          id: props.id,
          activeTab,
        });
      } catch (err) {
        showError(message, err);
      }
    };
    const handleFormat = () => {
      if (editorIns) {
        editorIns.getAction("editor.action.formatDocument")?.run();
      }
    };

    onMounted(() => {
      initEditor();
    });
    onBeforeUnmount(() => {
      stop();
      destroy();
    });
    return {
      contentType,
      handleBodyParams,
      handleQueryParams,
      handleHeaders,
      handleAuth,
      handleChangeContentType,
      handleUpdateActiveTab,
      handleFormat,
      activeTab,
      codeEditor,
      updateParamsColumnWidth,
    };
  },
  render() {
    const { params } = this.$props;
    const { method } = params;
    const { activeTab, contentType } = this;
    const tabs = [TabItem.Query, TabItem.Header, TabItem.Auth];
    if (shouldHaveBody(method)) {
      tabs.unshift(TabItem.Body);
    }
    let activeIndex = tabs.indexOf(activeTab);
    if (activeIndex < 0) {
      activeIndex = 0;
    }

    const contentTypeOptions = [
      {
        label: "JSON",
        key: ContentType.JSON,
      },
      {
        label: "Form",
        key: ContentType.Form,
      },
      {
        label: "Multipart",
        key: ContentType.Multipart,
      },
      {
        label: "XML",
        key: ContentType.XML,
      },
      {
        label: "Plain",
        key: ContentType.Plain,
      },
    ];
    const list = tabs.map((item) => {
      switch (item) {
        case TabItem.Body:
          {
            const label = contentTypeOptions.find(
              (opt) => opt.key === contentType,
            );
            if (activeTab !== TabItem.Body) {
              const badge = createBodyBadge({
                contentType,
                body: params.body,
              });
              return (
                <NTab name={item} class="badgeTab">
                  <div class="contentType">
                    {label?.label}
                    <NIcon>
                      <CaretDownOutline />
                    </NIcon>
                  </div>
                  {badge}
                </NTab>
              );
            }
            return (
              <NTab name={item}>
                <NDropdown
                  options={contentTypeOptions}
                  trigger="click"
                  value={contentType}
                  onSelect={(value) => {
                    this.handleChangeContentType(value);
                  }}
                >
                  <div class="contentType">
                    {label?.label}
                    <NIcon>
                      <CaretDownOutline />
                    </NIcon>
                  </div>
                </NDropdown>
              </NTab>
            );
          }
          break;
        case TabItem.Query:
          return createBadgeTab({
            activeTab,
            tab: item,
            value: params.query?.length,
          });
          break;
        case TabItem.Header:
          {
            return createBadgeTab({
              activeTab,
              tab: item,
              value: params.headers?.length,
            });
          }
          break;
        case TabItem.Auth:
          {
            return createBadgeTab({
              activeTab,
              tab: item,
              value: params.auth?.length,
            });
          }
          break;
        default:
          return <NTab name={item}>{item}</NTab>;
          break;
      }
    });

    let codeEditorClass = "";
    if (activeTab !== TabItem.Body || !shouldShowEditor(contentType)) {
      codeEditorClass = "hidden";
    }
    let showBodyKeyValue = false;
    let keyValues = [];

    switch (activeTab) {
      case TabItem.Body:
        {
          if (!shouldShowEditor(contentType)) {
            showBodyKeyValue = true;
            try {
              keyValues = tryToParseArray(this.params.body);
            } catch (err) {
              // 忽略parse出错
              console.error(err);
            }
          }
        }
        break;
      case TabItem.Query:
        {
          keyValues = this.params.query || [];
        }
        break;
      case TabItem.Header:
        {
          keyValues = this.params.headers || [];
        }
        break;
      case TabItem.Auth:
        {
          keyValues = this.params.auth || [];
        }
        break;
    }

    const keyValueSpans = [8, 16];

    const tabSlots = {
      suffix: () => (
        <NButtonGroup class="expandSelect">
          <NButton
            onClick={() => {
              this.updateParamsColumnWidth(0.3);
            }}
          >
            30%
          </NButton>
          <NButton
            onClick={() => {
              this.updateParamsColumnWidth(0.5);
            }}
          >
            50%
          </NButton>
          <NButton
            onClick={() => {
              this.updateParamsColumnWidth(0.7);
            }}
          >
            70%
          </NButton>
        </NButtonGroup>
      ),
    };

    return (
      <div class={tabClass}>
        <NTabs
          v-slots={tabSlots}
          tabsPadding={15}
          key={method}
          type="line"
          defaultValue={tabs[activeIndex]}
          onUpdateValue={(value) => {
            let activeTab = value as string;
            if (shouldHaveBody(method)) {
              if (value === TabItem.Body) {
                activeTab = "";
              }
            } else {
              if (value === TabItem.Query) {
                activeTab = "";
              }
            }
            this.handleUpdateActiveTab(activeTab);
            this.activeTab = value;
          }}
        >
          {list}
        </NTabs>
        <div class="content">
          {/* json, xml, text */}
          <div
            style="height: 100vh"
            ref="codeEditor"
            class={codeEditorClass}
          ></div>
          {activeTab === TabItem.Body && contentType === ContentType.JSON && (
            <NButton
              class="format"
              quaternary
              onClick={() => {
                this.handleFormat();
              }}
            >
              <NIcon>
                <CodeSlashOutline />
              </NIcon>
              {i18nCollection("format")}
            </NButton>
          )}
          {/* body form/multipart */}
          {showBodyKeyValue && (
            <ExKeyValue
              key="form/multipart"
              class="keyValue"
              spans={keyValueSpans}
              params={keyValues}
              supportFileSelect={contentType === ContentType.Multipart}
              onHandleParam={(opt) => {
                this.handleBodyParams(opt);
              }}
            />
          )}
          {activeTab === TabItem.Query && (
            <ExKeyValue
              key="query"
              class="keyValue"
              spans={keyValueSpans}
              params={keyValues}
              onHandleParam={(opt) => {
                this.handleQueryParams(opt);
              }}
            />
          )}
          {activeTab === TabItem.Header && (
            <ExKeyValue
              key="header"
              class="keyValue"
              spans={[12, 12]}
              params={keyValues}
              onHandleParam={(opt) => {
                this.handleHeaders(opt);
              }}
            />
          )}
          {activeTab === TabItem.Auth && (
            <ExKeyValue
              key="auth"
              class="keyValue"
              typeList={["textarea", "password"]}
              spans={keyValueSpans}
              params={keyValues}
              onHandleParam={(opt) => {
                this.handleAuth(opt);
              }}
            />
          )}
        </div>
      </div>
    );
  },
});


================================================
FILE: src/components/APISettingParams/uri.tsx
================================================
import { defineComponent, PropType, ref } from "vue";
import { css } from "@linaria/core";
import {
  NButton,
  NInput,
  NInputGroup,
  NSelect,
  NIcon,
  NDropdown,
  NGradientText,
} from "naive-ui";
import { ulid } from "ulid";
import { storeToRefs } from "pinia";
import { CodeSlashOutline } from "@vicons/ionicons5";

import { i18nCollection, i18nEnvironment } from "../../i18n";
import { HTTPRequest, HTTPMethod } from "../../commands/http_request";
import { useEnvironmentStore, ENVRegexp } from "../../stores/environment";
import { useDialogStore } from "../../stores/dialog";
import { VariableStatus } from "../../commands/variable";

const environmentSelectWidth = 50;
const wrapperClass = css`
  padding: 7px 4px 5px 0;
  overflow: hidden;
  .environmentSelect {
    width: ${environmentSelectWidth}px;
    float: left;
    .n-icon {
      font-size: 16px;
      font-weight: 900;
    }
  }
  .url {
    margin-left: ${environmentSelectWidth}px;
    .method {
      width: 120px;
      .n-base-selection {
        height: 36px;
      }
    }
    .submit {
      width: 80px;
      height: 36px;
    }
  }
  .n-input,
  .n-base-selection-label {
    background-color: transparent !important;
    line-break: anywhere;
  }
`;

const envLabelClass = css`
  padding: 0 5px;
  span {
    margin-left: 10px;
  }
  .n-icon {
    font-weight: 900;
    font-size: 16px;
  }
`;

interface CuttingURIResult {
  env: string;
  uri: string;
}

function cuttingURI(uri: string): CuttingURIResult {
  const result = {
    env: "",
    uri,
  };
  const arr = ENVRegexp.exec(uri);
  if (arr?.length === 2) {
    result.env = arr[1].trim();
    result.uri = uri.substring(arr[0].length);
  }
  return result;
}

export interface RequestURI {
  method: string;
  uri: string;
}

const addNewENVKey = ulid();
const clearENVKey = ulid();

export default defineComponent({
  name: "APISettingParamsURI",
  props: {
    params: {
      type: Object as PropType<HTTPRequest>,
      required: true,
    },
    onUpdateURI: {
      type: Function as PropType<(value: RequestURI) => void>,
      required: true,
    },
    onSubmit: {
      type: Function as PropType<(isAborted: boolean) => Promise<void>>,
      required: true,
    },
  },
  setup(props) {
    const dialogStore = useDialogStore();
    const environmentStore = useEnvironmentStore();
    const environments = storeToRefs(environmentStore).variables;
    const uriResult = cuttingURI(props.params.uri);

    const currentURI = ref(uriResult.uri);
    const env = ref(uriResult.env);
    const method = ref(props.params.method);
    const sending = ref(false);

    const showEnvironment = () => {
      dialogStore.toggleEnvironmentDialog(true);
    };

    const handleUpdate = () => {
      let uri = currentURI.value || "";
      if (env.value) {
        uri = `{{${env.value}}}${uri}`;
      }
      const changed =
        uri !== props.params.uri || method.value !== props.params.metod;

      if (changed && props.onUpdateURI) {
        props.onUpdateURI({
          method: method.value,
          uri,
        });
      }
    };
    let currentID = "";
    const isCurrent = (id: string) => {
      return id === currentID;
    };
    let lastHandleSendAt = 0;
    const handleSend = async () => {
      if (!props.onSubmit) {
        return;
      }
      const now = Date.now();
      // 如果快速点击
      // 直接忽略第二次点击
      if (now - lastHandleSendAt < 200) {
        return;
      }
      lastHandleSendAt = now;

      // 如果发送中,则中止请求
      if (sending.value) {
        sending.value = false;
        currentID = "";
        await props.onSubmit(true);
        return;
      }
      const id = ulid();
      currentID = id;
      sending.value = true;
      try {
        await props.onSubmit(false);
      } finally {
        // 只有当前id才重置状态
        if (isCurrent(id)) {
          sending.value = false;
        }
      }
    };

    return {
      sending,
      handleSend,
      showEnvironment,
      handleUpdate,
      environments,
      method,
      env,
      currentURI,
    };
  },
  render() {
    const { environments, currentURI, env, method } = this;
    const options = [
      HTTPMethod.GET,
      HTTPMethod.POST,
      HTTPMethod.PUT,
      HTTPMethod.PATCH,
      HTTPMethod.DELETE,
      HTTPMethod.OPTIONS,
      HTTPMethod.HEAD,
    ].map((item) => {
      return {
        label: item,
        value: item,
      };
    });
    // 只过滤启用的
    const envOptions = environments
      .filter((item) => item.enabled === VariableStatus.Enabled)
      .map((item) => {
        return {
          label: `${item.name} | ${item.value}`,
          key: item.name,
        };
      });
    let envPrefix = "";
    if (env) {
      envPrefix = env.substring(0, 2).toUpperCase();
    }
    envOptions.push({
      label: i18nEnvironment("addNew"),
      key: addNewENVKey,
    });
    if (this.env) {
      envOptions.push({
        label: i18nEnvironment("clearCurrent"),
        key: clearENVKey,
      });
    }

    const autoSizeOption = { minRows: 1, maxRows: 3 };

    return (
      <div class={wrapperClass}>
        <div class="environmentSelect">
          <NDropdown
            trigger="click"
            options={envOptions}
            renderLabel={(option) => {
              const label = (option.label as string) || "";
              const arr = label.split(" | ");
              return (
                <span class={envLabelClass}>
                  {arr[0]}
                  {arr[1] && <span class="font12">{arr[1]}</span>}
                </span>
              );
            }}
            value={env}
            onSelect={(value) => {
              if (value === addNewENVKey) {
                this.showEnvironment();
                return;
              }
              if (value === clearENVKey) {
                this.env = "";
              } else {
                this.env = value;
              }
              this.handleUpdate();
            }}
          >
            <NButton quaternary>
              {!envPrefix && (
                <NIcon>
                  <CodeSlashOutline />
                </NIcon>
              )}
              {envPrefix && <NGradientText>{envPrefix}</NGradientText>}
            </NButton>
          </NDropdown>
        </div>
        <div class="url">
          <NInputGroup>
            <NSelect
              class="method"
              consistentMenuWidth={false}
              options={options}
              placeholder={""}
              defaultValue={method || HTTPMethod.GET}
              onUpdateValue={(value) => {
                this.method = value;
                this.handleUpdate();
              }}
            />

            <NInput
              defaultValue={currentURI}
              type="textarea"
              autosize={autoSizeOption}
              placeholder={"http://test.com/users/v1/me"}
              clearable
              onBlur={() => {
                this.handleUpdate();
              }}
              onUpdateValue={(value) => {
                this.currentURI = value?.trim();
              }}
              onKeydown={(e) => {
                if (e.key.toLowerCase() === "enter" && this.currentURI) {
                  this.handleSend();
                  e.preventDefault();
                }
              }}
            />
            <NButton
              type="primary"
              class="submit"
              // loading={this.sending}
              onClick={() => {
                this.handleSend();
              }}
            >
              {this.sending ? i18nCollection("abort") : i18nCollection("send")}
            </NButton>
          </NInputGroup>
        </div>
      </div>
    );
  },
});


================================================
FILE: src/components/APISettingTree/header.tsx
================================================
// API栏目的顶部功能栏
import { defineComponent, inject, onBeforeUnmount, PropType } from "vue";
import { css } from "@linaria/core";
import {
  NDropdown,
  NButton,
  NGi,
  NGrid,
  NInput,
  NIcon,
  useMessage,
  useDialog,
} from "naive-ui";
import { DropdownMixedOption } from "naive-ui/es/dropdown/src/interface";

import { i18nCollection, i18nCommon } from "../../i18n";
import { SettingType, useAPISettingStore } from "../../stores/api_setting";
import {
  AnalyticsOutline,
  DownloadOutline,
  FolderOpenOutline,
} from "@vicons/ionicons5";
import {
  hotKeyCreateFolder,
  hotKeyCreateHTTPSetting,
  hotKeyMatchCreateFolder,
  hotKeyMatchCreateHTTPSetting,
} from "../../helpers/hot_key";
import {
  addFolderDefaultValue,
  addFolderKey,
  addHTTPSettingDefaultValue,
  addHTTPSettingKey,
} from "../../constants/provide";
import {
  readTextFromClipboard,
  showError,
  writeSettingToDownload,
} from "../../helpers/util";
import { useRoute } from "vue-router";
import { useAPIFolderStore } from "../../stores/api_folder";
import { useAPICollectionStore } from "../../stores/api_collection";
import { HandleKey } from "../../constants/handle_key";
import { newImportDialog } from "../ExDialog";

const collapseWidth = 50;

const headerClass = css`
  margin-right: ${collapseWidth}px;
  position: relative;
  .collapse {
    position: absolute;
    top: 0;
    right: ${-collapseWidth}px;
    bottom: 0;
    width: ${collapseWidth}px;
    .n-button {
      margin-left: 10px;
    }
  }
`;

const addDropdownClass = css`
  .label {
    min-width: 180px;
  }
  .hotKey {
    float: right;
  }
`;

export default defineComponent({
  name: "APISettingTreeHeader",
  props: {
    onFilter: {
      type: Function as PropType<(value: string) => void>,
      required: true,
    },
  },
  setup() {
    const route = useRoute();
    const message = useMessage();
    const dialog = useDialog();
    const apiFolderStore = useAPIFolderStore();
    const apiSettingStore = useAPISettingStore();
    const collectionStore = useAPICollectionStore();

    const collection = route.query.collection as string;
    const addHTTPSetting = inject(
      addHTTPSettingKey,
      addHTTPSettingDefaultValue,
    );
    const addFolder = inject(addFolderKey, addFolderDefaultValue);

    const handleKeydown = (e: KeyboardEvent) => {
      if (hotKeyMatchCreateFolder(e)) {
        addFolder("");
        return;
      }
      if (hotKeyMatchCreateHTTPSetting(e)) {
        addHTTPSetting("");
        return;
      }
    };
    document.addEventListener("keydown", handleKeydown);
    onBeforeUnmount(() => {
      document.removeEventListener("keydown", handleKeydown);
    });

    const handleCloseAllFolders = async () => {
      try {
        await collectionStore.closeAllFolders(collection);
      } catch (err) {
        showError(message, err);
      }
    };

    const handleImport = async () => {
      let data = "";
      try {
        data = (await readTextFromClipboard()) || "";
      } catch (err) {
        showError(message, err);
      } finally {
        newImportDialog({
          dialog,
          collection,
          data,
        });
      }
    };

    const handleExport = async () => {
      const arr: unknown[] = [];
      apiFolderStore.apiFolders.forEach((folder) => arr.push(folder));
      apiSettingStore.apiSettings.forEach((apiSetting) => arr.push(apiSetting));
      try {
        let name = "unknown";
        const result = collectionStore.findByID(collection);
        if (result) {
          name = result.name;
        }
        await writeSettingToDownload(arr, name);
        message.info(i18nCollection("exportSettingsSuccess"));
      } catch (err) {
        showError(message, err);
      }
    };

    return {
      handleImport,
      handleExport,
      addHTTPSetting,
      addFolder,
      handleCloseAllFolders,
      text: {
        add: i18nCommon("add"),
        placeholder: i18nCollection("filterPlaceholder"),
      },
    };
  },
  render() {
    const options: DropdownMixedOption[] = [
      {
        label: `${i18nCollection(
          "newHTTPRequest",
        )} | ${hotKeyCreateHTTPSetting()}`,
        key: SettingType.HTTP,
        icon: () => (
          <NIcon>
            <AnalyticsOutline />
          </NIcon>
        ),
      },
      {
        label: `${i18nCollection("newFolder")} | ${hotKeyCreateFolder()}`,
        key: SettingType.Folder,
        icon: () => (
          <NIcon>
            <FolderOpenOutline />
          </NIcon>
        ),
      },
      {
        type: "divider",
        key: "divider",
      },
      {
        label: i18nCollection("exportSettings"),
        key: HandleKey.ExportSettings,
        icon: () => (
          <NIcon>
            <DownloadOutline class="rotate90" />
          </NIcon>
        ),
      },
      {
        label: i18nCollection("importSettings"),
        key: HandleKey.ImportSettings,
        icon: () => (
          <NIcon>
            <DownloadOutline class="rotate270" />
          </NIcon>
        ),
      },
    ];
    const { text } = this;
    const inputProps = {
      spellcheck: false,
    };
    return (
      <div class={headerClass}>
        <NGrid xGap={8}>
          <NGi span={16}>
            <NInput
              type="text"
              clearable
              inputProps={inputProps}
              placeholder={text.placeholder}
              onInput={(value: string) => {
                this.$props.onFilter(value.toLowerCase());
              }}
            />
          </NGi>
          <NGi span={8}>
            <NDropdown
              class={addDropdownClass}
              trigger="click"
              options={options}
              renderLabel={(option) => {
                const arr = (option.label as string).split(" | ");
                const hotkey =
                  arr.length === 2 ? (
                    <span class="hotKey">{arr[1]}</span>
                  ) : undefined;

                return (
                  <div class="label">
                    {arr[0]}
                    {hotkey}
                  </div>
                );
              }}
              onSelect={(key: string) => {
                switch (key) {
                  case SettingType.HTTP:
                    this.addHTTPSetting("");
                    break;
                  case SettingType.Folder:
                    this.addFolder("");
                    break;
                  case HandleKey.ImportSettings:
                    this.handleImport();
                    break;
                  case HandleKey.ExportSettings:
                    this.handleExport();
                    break;
                }
              }}
            >
              <NButton class="widthFull">{text.add}</NButton>
            </NDropdown>
          </NGi>
        </NGrid>
        <div class="collapse">
          <NButton
            onClick={() => {
              this.handleCloseAllFolders();
            }}
          >
            <NIcon size={18}>
              <svg
                viewBox="0 0 24 24"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="M22 18h-6M15 7l-1.8-2.4c-.589-.785-.883-1.178-1.306-1.389C11.472 3 10.981 3 10 3H7c-1.886 0-2.828 0-3.414.586C3 4.172 3 5.114 3 7v10c0 1.886 0 2.828.586 3.414C4.172 21 5.114 21 7 21h4m4-14h2c1.886 0 2.828 0 3.414.586C21 8.172 21 9.114 21 11v.5M15 7H3"
                  stroke="currentColor"
                  stroke-width="1.5"
                  stroke-linecap="round"
                />
              </svg>
            </NIcon>
          </NButton>
        </div>
      </div>
    );
  },
});


================================================
FILE: src/components/APISettingTree/index.tsx
================================================
// API应用配置列表
import { css } from "@linaria/core";
import { defineComponent, provide, ref } from "vue";
import { useDialog, useMessage } from "naive-ui";

import { SettingType, useAPISettingStore } from "../../stores/api_setting";
import APISettingTreeHeader from "./header";
import APISettingTreeItems from "./items";
import ExDialog from "../ExDialog";
import { i18nCollection, i18nCommon } from "../../i18n";
import { ExFormItem } from "../ExForm";
import { newDefaultAPISetting } from "../../commands/api_setting";
import { useRoute } from "vue-router";
import { showError } from "../../helpers/util";
import { useAPIFolderStore } from "../../stores/api_folder";
import { newDefaultAPIFolder } from "../../commands/api_folder";
import { addFolderKey, addHTTPSettingKey } from "../../constants/provide";

const treesClass = css`
  padding: 10px;
`;

const getSettingFormItems = (): ExFormItem[] => {
  return [
    {
      key: "name",
      label: i18nCommon("name"),
      placeholer: i18nCommon("namePlaceholder"),
      rule: {
        required: true,
        message: i18nCommon("nameRequireError"),
        trigger: "blur",
      },
    },
  ];
};

const getFolderFormItems = (): ExFormItem[] => {
  return [
    {
      key: "name",
      label: i18nCommon("name"),
      placeholer: i18nCommon("namePlaceholder"),
      rule: {
        required: true,
        message: i18nCommon("nameRequireError"),
        trigger: "blur",
      },
    },
  ];
};

export default defineComponent({
  name: "APISettingTree",
  setup() {
    const keyword = ref("");
    const apiSettingStore = useAPISettingStore();
    const apiFolderStore = useAPIFolderStore();
    const dialog = useDialog();
    const route = useRoute();
    const message = useMessage();
    const collection = route.query.collection as string;

    provide(addHTTPSettingKey, (folder: string) => {
      ExDialog({
        dialog,
        title: i18nCollection("newHTTPRequest"),
        formItems: getSettingFormItems(),
        onConfirm: async (data) => {
          const setting = newDefaultAPISetting();
          setting.category = SettingType.HTTP;
          setting.collection = collection;
          setting.name = data.name as string;
          try {
            await apiSettingStore.add(setting);
            if (folder) {
              await apiFolderStore.addChild({
                id: folder,
                children: [setting.id],
              });
            }
            apiSettingStore.select(setting.id);
          } catch (err) {
            showError(message, err);
          }
        },
      });
    });
    provide(addFolderKey, (parentFolder: string) => {
      ExDialog({
        dialog,
        title: i18nCollection("newFolder"),
        formItems: getFolderFormItems(),
        onConfirm: async (data) => {
          const folder = newDefaultAPIFolder();
          folder.collection = collection;
          folder.name = data.name as string;
          try {
            await apiFolderStore.add(folder);
            if (folder) {
              await apiFolderStore.addChild({
                id: parentFolder,
                children: [folder.id],
              });
            }
          } catch (err) {
            showError(message, err);
          }
        },
      });
    });
    return {
      keyword,
    };
  },
  render() {
    return (
      <div class={treesClass}>
        <APISettingTreeHeader
          onFilter={(value: string) => {
            this.keyword = value;
          }}
        />
        <APISettingTreeItems keyword={this.keyword} />
      </div>
    );
  },
});


================================================
FILE: src/components/APISettingTree/item_dropdown.tsx
================================================
// API功能下拉选项框
import {
  AddOutline,
  ChevronDownOutline,
  CopyOutline,
  CreateOutline,
  DownloadOutline,
  FolderOpenOutline,
  LinkOutline,
  PinOutline,
  TrashOutline,
} from "@vicons/ionicons5";
import { NDropdown, NIcon, useDialog, useMessage } from "naive-ui";
import { defineComponent, inject } from "vue";
import { useRoute } from "vue-router";
import { css } from "@linaria/core";
import { DropdownMixedOption } from "naive-ui/es/dropdown/src/interface";

import {
  readTextFromClipboard,
  showError,
  writeSettingToDownload,
  writeTextToClipboard,
} from "../../helpers/util";
import { i18nCollection, i18nCommon } from "../../i18n";
import { useAPIFolderStore } from "../../stores/api_folder";
import { SettingType, useAPISettingStore } from "../../stores/api_setting";
import { HandleKey } from "../../constants/handle_key";
import ExDialog, { newImportDialog } from "../ExDialog";
import {
  addFolderDefaultValue,
  addFolderKey,
  addHTTPSettingDefaultValue,
  addHTTPSettingKey,
} from "../../constants/provide";
import { usePinRequestStore } from "../../stores/pin_request";
import { convertRequestToCURL } from "../../commands/http_request";
import { APISetting } from "../../commands/api_setting";
import { APIFolder } from "../../commands/api_folder";
import { useCookieStore } from "../../stores/cookie";

const dropdownClass = css`
  .n-dropdown-option {
    margin: 2px 0;
  }
`;

export default defineComponent({
  name: "APISettingTreeItemDropdown",
  props: {
    id: {
      type: String,
      required: true,
    },
    apiSettingType: {
      type: String,
      required: true,
    },
  },
  setup(props) {
    const dialog = useDialog();
    const message = useMessage();
    const cookieStore = useCookieStore();
    const apiFolderStore = useAPIFolderStore();
    const pinRequestStore = usePinRequestStore();
    const apiSettingStore = useAPISettingStore();
    const route = useRoute();
    const collection = route.query.collection as string;

    const addHTTPSetting = inject(
      addHTTPSettingKey,
      addHTTPSettingDefaultValue,
    );
    const addFolder = inject(addFolderKey, addFolderDefaultValue);

    const hanldeImport = async (id: string) => {
      try {
        const data = (await readTextFromClipboard()) || "";
        newImportDialog({
          dialog,
          collection,
          folder: id,
          data,
        });
      } catch (err) {
        showError(message, err);
      }
    };
    const handleExport = async (id: string) => {
      try {
        const apiSettings: APISetting[] = [];
        const folders: APIFolder[] = [];
        const name = apiFolderStore.findByID(id).name;
        const folderIdList:string[] = [];
        const appendChildren = (folderId: string) => {
          if (folderIdList.includes(folderId)) {
            return;
          }
          folderIdList.push(folderId);
          const folder = apiFolderStore.findByID(folderId);
          if (!folder) {
            return;
          }
          folder.children.split(",").forEach((child) => {
            const apiSetting = apiSettingStore.findByID(child);
            if (apiSetting) {
              apiSettings.push(apiSetting);
            } else {
              // folder
              appendChildren(child);
            }
          });
        };
        appendChildren(id);
        const arr: unknown[] = [];
        folders.forEach((folder) => arr.push(folder));
        apiSettings.forEach((apiSetting) => arr.push(apiSetting));
        await writeSettingToDownload(arr, name);
        message.info(i18nCollection("exportSettingsSuccess"));
      } catch (err) {
        showError(message, err);
      }
    };
    const handleSelect = (key: string) => {
      const { id, apiSettingType } = props;
      let name = "";
      let isFolder = false;
      if (apiSettingType === SettingType.Folder) {
        isFolder = true;
        name = apiFolderStore.findByID(id).name;
      } else {
        name = apiSettingStore.findByID(id).name;
      }

      switch (key) {
        case HandleKey.Delete:
          {
            const content = i18nCollection("deleteSettingContent").replace(
              "%s",
              name,
            );
            const d = dialog.warning({
              title: i18nCollection("deleteSetting"),
              content: content,
              positiveText: i18nCommon("confirm"),
              onPositiveClick: async () => {
                d.loading = true;
                try {
                  if (isFolder) {
                    await apiFolderStore.remove(id);
                  } else {
                    await apiSettingStore.remove(id);
                  }
                } catch (err) {
                  showError(message, err);
                }
              },
            });
          }
          break;
        case HandleKey.Modify:
          {
            ExDialog({
              dialog,
              title: i18nCollection("modifySetting"),
              formItems: [
                {
                  key: "name",
                  label: i18nCommon("name"),
                  defaultValue: name,
                  placeholer: i18nCommon("namePlaceholder"),
                  rule: {
                    required: true,
                    message: i18nCommon("nameRequireError"),
                    trigger: "blur",
                  },
                },
              ],
              onConfirm: async (data) => {
                try {
                  if (isFolder) {
                    await apiFolderStore.updateByID(id, data);
                  } else {
                    await apiSettingStore.updateByID(id, data);
                  }
                } catch (err) {
                  showError(message, err);
                }
              },
            });
          }
          break;
        case HandleKey.Create:
          {
            addHTTPSetting(id);
          }
          break;
        case HandleKey.CreateFolder:
          {
            addFolder(id);
          }
          break;
        case HandleKey.CopyAsCURL:
          {
            const { req } = apiSettingStore.getHTTPRequestFillValues(id);
            cookieStore
              .fetch()
              .then(() => {
                return convertRequestToCURL(
                  collection,
                  req,
                  cookieStore.cookies,
                );
              })
              .then(writeTextToClipboard)
              .then(() => {
                message.success(i18nCollection("copyAsCURLSuccess"));
              })
              .catch((err) => {
                showError(message, err);
              });
          }
          break;
        case HandleKey.Pin:
          {
            pinRequestStore.add(collection, {
              id,
            });
          }
          break;
        case HandleKey.ImportSettings:
          {
            hanldeImport(id);
          }
          break;
        case HandleKey.ExportSettings:
          {
            handleExport(id);
          }
          break;
        case HandleKey.Copy:
          {
            const setting = apiSettingStore.findByID(id);
            writeTextToClipboard(JSON.stringify(setting, null, 2))
              .then(() => {
                message.success(i18nCollection("copySettingSuccess"));
              })
              .catch((err) => {
                showError(message, err);
              });
          }
          break;
        default:
          break;
      }
    };
    return {
      handleSelect,
    };
  },
  render() {
    const { apiSettingType } = this.$props;
    const options: DropdownMixedOption[] = [
      {
        label: i18nCollection("modifySetting"),
        key: HandleKey.Modify,
        icon: () => (
          <NIcon>
            <CreateOutline />
          </NIcon>
        ),
      },
    ];
    if (apiSettingType === SettingType.Folder) {
      options.unshift(
        {
          label: i18nCollection("newHTTPRequest"),
          key: HandleKey.Create,
          icon: () => (
            <NIcon>
              <AddOutline />
            </NIcon>
          ),
        },
        {
          label: i18nCollection("newFolder"),
          key: HandleKey.CreateFolder,
          icon: () => (
            <NIcon>
              <FolderOpenOutline />
            </NIcon>
          ),
        },
      );
      options.push(
        {
          label: i18nCollection("exportSettings"),
          key: HandleKey.ExportSettings,
          icon: () => (
            <NIcon>
              <DownloadOutline class="rotate90" />
            </NIcon>
          ),
        },
        {
          label: i18nCollection("importSettings"),
          key: HandleKey.ImportSettings,
          icon: () => (
            <NIcon>
              <DownloadOutline class="rotate270" />
            </NIcon>
          ),
        },
      );
    } else {
      options.push(
        {
          label: i18nCollection("copyAsCURL"),
          key: HandleKey.CopyAsCURL,
          icon: () => (
            <NIcon>
              <LinkOutline />
            </NIcon>
          ),
        },

        {
          label: i18nCollection("pinRequest"),
          key: HandleKey.Pin,
          icon: () => (
            <NIcon>
              <PinOutline />
            </NIcon>
          ),
        },
        {
          label: i18nCollection("copySetting"),
          key: HandleKey.Copy,
          icon: () => (
            <NIcon>
              <CopyOutline />
            </NIcon>
          ),
        },
      );
    }
    options.push(
      {
        type: "divider",
        key: HandleKey.Divider,
      },
      {
        label: i18nCollection("deleteSetting"),
        key: HandleKey.Delete,
        icon: () => (
          <NIcon>
            <TrashOutline />
          </NIcon>
        ),
      },
    );
    return (
      <NDropdown
        class={dropdownClass}
        options={options}
        trigger="click"
        onSelect={this.handleSelect}
        renderLabel={(option) => {
          return <span class="option">{option.label}</span>;
        }}
      >
        <span
          class="itemDropitem"
          onClick={(e) => {
            e.stopPropagation();
          }}
        >
          <NIcon>
            <ChevronDownOutline />
          </NIcon>
        </span>
      </NDropdown>
    );
  },
});


================================================
FILE: src/components/APISettingTree/items.tsx
================================================
// API列表,实现拖动
import { defineComponent, ref, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { css } from "@linaria/core";
import { NGradientText, NInput, useMessage } from "naive-ui";
import { sortBy, uniq } from "lodash-es";
import { useRoute } from "vue-router";

import { useAPIFolderStore } from "../../stores/api_folder";
import { isMatchTextOrPinYin, showError } from "../../helpers/util";
import { useAPISettingStore, SettingType } from "../../stores/api_setting";
import { APIFolder } from "../../commands/api_folder";
import { APISetting } from "../../commands/api_setting";
import { useSettingStore } from "../../stores/setting";
import { useAPICollectionStore } from "../../stores/api_collection";
import {
  nodeAddClass,
  nodeGetDataValue,
  nodeGetOffset,
  nodeGetOffsetHeightWidth,
  nodGetScrollTop,
  nodeInsertAt,
  nodeRemove,
  nodeRemoveClass,
  nodeSetStyle,
  nodeHasClass,
} from "../../helpers/html";
import APISettingTreeItemDropdown from "./item_dropdown";
import { HTTPMethod } from "../../commands/http_request";
import { openFolderIcon, closeFolderIcon } from "../../icons";

const itemsWrapperClass = css`
  user-select: none;
  position: absolute;
  top: 50px;
  left: 5px;
  right: 5px;
  bottom: 5px;
  overflow-y: auto;
  overflow-x: hidden;
  &.dragging {
    li:hover {
      background-color: rgba(255, 255, 255, 0) !important;
    }
    .dragItem {
      &:hover {
        background-color: rgba(255, 255, 255, 0.3) !important;
      }
      &.light:hover {
        background-color: rgba(0, 0, 0, 0.3) !important;
      }
    }
  }

  ul {
    margin: 0;
    padding: 0;
  }
  li {
    list-style: none;
    padding: 3px 10px;
    line-height: 34px;
    height: 34px;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;

    .method {
      margin: 0 8px 0 5px;
      font-size: 12px;
    }
    &.renameItem {
      // 避免出现...
      text-overflow: initial;
      &:hover .itemDropitem {
        display: none;
      }
    }
    .renameInput {
      width: 86% !important;
    }
    &.insertBefore {
      padding-top: 2px;
      border-top: 1px dashed;
    }
    &:hover {
      cursor: pointer;
      background-color: rgba(255, 255, 255, 0.1);
      .itemDropitem {
        display: inline;
      }
    }
    &.selected {
      background-color: rgba(255, 255, 255, 0.1);
      color: #fff;
    }
    &.light:hover {
      background-color: rgba(30, 30, 30, 0.1);
    }
    &.light.selected {
      background-color: rgba(30, 30, 30, 0.1);
      color: #000;
    }
    .itemDropitem {
      float: right;
      display: none;
    }
    .folder {
      display: block;
      float: left;
      width: 30px;
      height: 100%;
      background-size: 20px;
      background-repeat: no-repeat;
      background-position: center;
      &.close {
        background-image: url(${closeFolderIcon});
      }
      &.open {
        background-image: url(${openFolderIcon});
      }
    }
  }
  .n-icon {
    float: left;
    font-size: 16px;
    line-height: 29px;
    margin: 5px 5px 0 10px;
    font-weight: 900;
  }
`;

enum OverType {
  Over = 0,
  Top = 1,
  Bottom = 2,
}

interface TreeItem {
  id: string;
  name: string;
  settingType: string;
  method: string;
  uri: string;
  children: TreeItem[];
  expanded: boolean;
  parent: string;
  childIndex: number;
  hidden: boolean;
  isLastChild: boolean;
}

function getMethodColorType(method: string) {
  switch (method) {
    case HTTPMethod.DELETE:
      return "error";
      break;
    case HTTPMethod.PATCH:
    case HTTPMethod.PUT:
    case HTTPMethod.POST:
      return "success";
      break;
    default:
      return "info";
      break;
  }
}

function convertToTreeItems(params: {
  apiFolders: APIFolder[];
  apiSettings: APISetting[];
  expandedFolders: string[];
  topTreeItems: string[];
  keyword: string;
}): TreeItem[] {
  const { apiFolders, apiSettings, expandedFolders, topTreeItems } = params;
  const map = new Map<string, TreeItem>();

  const keyword = params.keyword.toLowerCase();

  const methodReg = /"method":"(\S+?)"/;
  const uriReg = /"uri":"(\S+?)"/;
  apiSettings.forEach((item) => {
    let method = "";
    let uri = "";
    if (item.setting) {
      let result = methodReg.exec(item.setting);
      if (result?.length === 2) {
        method = result[1];
      }
      result = uriReg.exec(item.setting);
      if (result?.length === 2) {
        uri = result[1].replace(/{{\S+}}/, "");
      }
    }
    map.set(item.id, {
      method,
      uri,
      id: item.id,
      name: item.name,
      settingType: SettingType.HTTP,
      children: [],
      expanded: false,
      parent: "",
      childIndex: -1,
      hidden: false,
      isLastChild: false,
    });
  });

  apiFolders.forEach((item) => {
    map.set(item.id, {
      id: item.id,
      uri: "",
      name: item.name,
      settingType: SettingType.Folder,
      children: [],
      expanded: expandedFolders.includes(item.id),
      parent: "",
      childIndex: -1,
      hidden: false,
      method: "",
      isLastChild: false,
    });
  });

  // 记录已经设置为子元素的id
  const children = [] as string[];
  apiFolders.forEach((item) => {
    if (!item.children) {
      return;
    }
    const treeItem = map.get(item.id);
    if (!treeItem) {
      return;
    }
    const arr: string[] = [];
    item.children.split(",").forEach((child) => {
      if (!child || child === treeItem.id) {
        return;
      }
      arr.push(child);
    });
    const childCount = arr.length;
    arr.forEach((child, index) => {
      const subItem = map.get(child);
      if (!subItem) {
        return;
      }
      subItem.parent = treeItem.id;
      subItem.childIndex = treeItem.children.length;
      subItem.isLastChild = index === childCount - 1;
      treeItem.children.push(subItem);
      children.push(child);
    });
  });
  let result = [] as TreeItem[];
  map.forEach((item, key) => {
    if (children.includes(key)) {
      return;
    }
    result.push(item);
  });
  if (keyword) {
    const methodKeyword = keyword.toUpperCase();
    const shouldBeHide = (item: TreeItem) => {
      // 匹配method
      // 匹配url
      // 如果当前元素匹配,则其子元素展示
      if (
        item.method === methodKeyword ||
        item.uri.toLowerCase().includes(keyword) ||
        isMatchTextOrPinYin(item.name, keyword)
      ) {
        return;
      }
      let hidden = true;
      item.children.forEach((item) => {
        shouldBeHide(item);
        // 子元素有一个非hidden,则父元素非hidden
        if (!item.hidden) {
          hidden = false;
        }
      });

      item.hidden = hidden;
    };
    result.forEach(shouldBeHide);
  }
  const filterVisible = (item: TreeItem) => {
    if (item.hidden) {
      return false;
    }
    item.children = item.children.filter(filterVisible);
    return true;
  };
  result = result.filter(filterVisible);
  return sortBy(result, (item) => {
    return topTreeItems.indexOf(item.id);
  });
}

export default defineComponent({
  name: "APISettingTreeItems",
  props: {
    keyword: {
      type: String,
      default: () => "",
    },
  },
  setup() {
    const message = useMessage();
    const route = useRoute();
    const wrapper = ref(null);
    const collection = route.query.collection as string;

    const collectionStore = useAPICollectionStore();
    const apiFolderStore = useAPIFolderStore();
    const apiSettingStore = useAPISettingStore();
    const { apiFolders } = storeToRefs(apiFolderStore);
    const { expandedFolders, topTreeItems } = storeToRefs(collectionStore);
    const { isDark } = storeToRefs(useSettingStore());
    const { apiSettings, selectedID } = storeToRefs(apiSettingStore);

    let currentTreeItems = [] as TreeItem[];
    let topTreeItemIDList = [] as string[];
    const renameItem = ref({
      name: "",
      id: "",
    });
    const renameValue = ref("");

    const setTreeItems = (items: TreeItem[], topItems: string[]) => {
      currentTreeItems = items;
      topTreeItemIDList = topItems;
    };

    const handleClick = async (item: TreeItem) => {
      try {
        // folder的处理
        if (item.settingType === SettingType.Folder) {
          let fn = collectionStore.openFolder;
          if (item.expanded) {
            fn = collectionStore.closeFolder;
          }
          await fn(collection, item.id);
        } else {
          apiSettingStore.select(item.id);
        }
      } catch (err) {
        showError(message, err);
      }
    };

    const handleMove = async (
      moveIndex: number,
      targetIndex: number,
      overType: OverType,
    ) => {
      // TODO 如果是最后一个元素的处理
      // isOver move 与 target 是否重叠
      const moveItem = currentTreeItems[moveIndex];
      const targetItem = currentTreeItems[targetIndex];
      // 如果元素不存在,则忽略不处理
      if (!moveItem || !targetItem) {
        return;
      }
      // 同一个元素不处理
      if (moveItem.id === targetItem.id) {
        return;
      }
      let parentID = targetItem.parent;
      let insertBefore = targetItem.id;
      // 如果是最后一个元素,而且bottom
      if (
        targetIndex === currentTreeItems.length - 1 &&
        overType === OverType.Bottom
      ) {
        insertBefore = "";
      }

      if (targetItem.settingType === SettingType.Folder) {
        // 拖动至文件上面,则add child
        if (overType === OverType.Over) {
          parentID = targetItem.id;
          insertBefore = "";
        } else {
          // 如果folder前面是元素,而且有parent
          // 且是该folder的最后元素
          // 则添加至该元素所有在folder
          const newTarget = currentTreeItems[targetIndex - 1];
          if (newTarget && newTarget.parent && newTarget.isLastChild) {
            parentID = newTarget.parent;
            insertBefore = "";
          }
        }
      }
      try {
        await apiFolderStore.addChild({
          id: parentID,
          children: [moveItem.id],
          before: insertBefore,
        });
        if (!parentID) {
          // 设置至top items
          const moveItemIndex = topTreeItemIDList.indexOf(moveItem.id);
          if (moveItemIndex !== -1) {
            topTreeItemIDList.splice(moveItemIndex, 1);
          }
          const index = topTreeItemIDList.indexOf(insertBefore);
          if (index === -1) {
            topTreeItemIDList.push(moveItem.id);
          } else {
            topTreeItemIDList.splice(index, 0, moveItem.id);
          }
          await collectionStore.updateTopTreeItems(
            collection,
            uniq(topTreeItemIDList),
          );
        }
      } catch (err) {
        showError(message, err);
      }
    };

    let target: EventTarget;
    let moveTarget: EventTarget;
    let originClientY = 0;
    let originOffset = 0;
    let targetHeight = 0;
    let currentInsertIndex = -1;
    let isDragging = false;
    const draggingClass = "dragging";
    let listItems = [] as HTMLCollection[];
    let mousedownFiredAt = 0;
    let maxMoveOffsetX = 0;
    const handleMousemove = (e: MouseEvent) => {
      // 每移动两个点再处理
      if (isDragging && e.clientY % 2 !== 0) {
        e.preventDefault();
        return;
      }
      const offset = e.clientY - originClientY;
      if (
        !isDragging &&
        Math.abs(offset) > 5 &&
        Date.now() - mousedownFiredAt < 500
      ) {
        isDragging = true;
        nodeAddClass(wrapper.value, draggingClass);
        nodeAddClass(document.body, "disableUserSelect");

        // 提交公共方法至html无法复制(TODO确认原因)
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        moveTarget = target.cloneNode(true);
        nodeSetStyle(moveTarget, {
          position: "absolute",
          left: "0px",
          right: "0px",
        });
        nodeAddClass(moveTarget, "dragItem");

        nodeInsertAt(wrapper.value, moveTarget, 0);
      }

      if (isDragging) {
        const top = offset + originOffset + nodGetScrollTop(wrapper.value);
        const index = Math.round(top / targetHeight);
        if (currentInsertIndex !== index) {
          if (currentInsertIndex !== -1) {
            nodeRemoveClass(listItems[currentInsertIndex], "insertBefore");
          }
          if (listItems.length > index) {
            nodeAddClass(listItems[index], "insertBefore");
            currentInsertIndex = index;
          }
        }
        nodeSetStyle(moveTarget, {
          top: `${top}px`,
        });
        e.preventDefault();
      }
    };

    const handleMouseup = (e: MouseEvent) => {
      document.removeEventListener("mousemove", handleMousemove);
      document.removeEventListener("mouseup", handleMouseup);
      nodeRemoveClass(document.body, "disableUserSelect");
      if (!isDragging) {
        return;
      }

      let overType = OverType.Bottom;
      const overOffset = 5;
      const offset = Math.abs(e.clientY - originClientY) % targetHeight;
      // 覆盖
      if (offset <= overOffset || targetHeight - offset <= overOffset) {
        overType = OverType.Over;
      } else if (offset < targetHeight * 0.4) {
        overType = OverType.Top;
      }

      isDragging = false;

      const moveItemIndex = Number.parseInt(
        nodeGetDataValue(moveTarget, "index"),
      );
      const targetItemIndex = Number.parseInt(
        nodeGetDataValue(listItems[currentInsertIndex], "index"),
      );

      nodeRemove(moveTarget);
      nodeRemoveClass(listItems[currentInsertIndex], "insertBefore");
      nodeRemoveClass(wrapper.value, draggingClass);

      if (maxMoveOffsetX && e.clientX > maxMoveOffsetX) {
        return;
      }

      handleMove(moveItemIndex, targetItemIndex, overType);
    };
    const handleMousedown = (e: MouseEvent) => {
      isDragging = false;
      // 无target或者点击非左键
      if (!e.currentTarget || e.button > 1) {
        return;
      }
      mousedownFiredAt = Date.now();
      // TODO 此处导致无法复制,后续研究
      // e.preventDefault();
      currentInsertIndex = -1;
      target = e.currentTarget;
      originOffset =
        nodeGetOffset(target).top - nodeGetOffset(wrapper.value).top;
      targetHeight = nodeGetOffsetHeightWidth(target).height;
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      listItems = wrapper.value.children[0].children;
      originClientY = e.clientY;
      document.addEventListener("mousemove", handleMousemove);
      document.addEventListener("mouseup", handleMouseup);
      if (wrapper.value) {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        maxMoveOffsetX = wrapper.value.clientWidth as number;
      }
    };

    const resetRename = () => {
      renameValue.value = "";
      renameItem.value = {
        name: "",
        id: "",
      };
    };

    const handelRename = async () => {
      // 无变化,无需修改
      const name = renameValue.value;
      const id = renameItem.value.id;
      if (!name || !id) {
        resetRename();
        return;
      }
      try {
        const folder = apiFolderStore.findByID(id);
        if (folder) {
          folder.name = name;
          await apiFolderStore.updateByID(id, folder);
        } else {
          const apiSetting = apiSettingStore.findByID(id);
          apiSetting.name = name;
          await apiSettingStore.updateByID(id, apiSetting);
        }
      } catch (err) {
        showError(message, err);
      } finally {
        resetRename();
      }
    };
    const handleKeydown = (e: KeyboardEvent) => {
      const key = e.key.toLowerCase();
      switch (key) {
        case "escape":
          {
            resetRename();
          }
          break;
        case "enter":
          {
            handelRename();
          }
          break;

        default:
          break;
      }
    };
    document.addEventListener("keydown", handleKeydown);
    onBeforeUnmount(() => {
      document.removeEventListener("keydown", handleKeydown);
    });

    return {
      renameValue,
      renameItem,
      selectedID,
      topTreeItems,
      expandedFolders,
      isDark,
      apiFolders,
      apiSettings,
      handleClick,
      handleMousedown,
      handelRename,
      setTreeItems,
      wrapper,
    };
  },
  render() {
    const { keyword } = this.$props;
    const {
      apiFolders,
      apiSettings,
      isDark,
      expandedFolders,
      topTreeItems,
      setTreeItems,
      selectedID,
      renameItem,
    } = this;
    const treeItems = convertToTreeItems({
      apiFolders,
      apiSettings,
      expandedFolders,
      topTreeItems,
      keyword,
    });
    const showAllChildren = keyword.trim().length !== 0;
    const itemList = [] as JSX.Element[];
    // 当前展示的tree item
    const currentTreeItems = [] as TreeItem[];
    // 顶层元素
    const topTreeItemIDList = [] as string[];
    let treeItemIndex = 0;
    const appendToList = (items: TreeItem[], level: number) => {
      if (!items || items.length === 0) {
        return;
      }
      items.forEach((item) => {
        if (level === 0) {
          topTreeItemIDList.push(item.id);
        }
        let folderClass = "folder";
        if (item.expanded) {
          folderClass += " open";
        } else {
          folderClass += " close";
        }
        let icon = <span class={folderClass}></span>;
        const isFolder = item.settingType === SettingType.Folder;
        if (!isFolder) {
          icon = (
            <NGradientText
              class="method"
              type={getMethodColorType(item.method)}
            >
              {item.method || HTTPMethod.GET}
            </NGradientText>
          );
        }

        const style = {
          "padding-left": `${level * 20}px`,
        };
        let cls = isDark ? "" : "light";
        if (item.id === selectedID) {
          cls += " selected";
        }
        if (item.id === renameItem.id) {
          cls += " renameItem";
        }
        const onClick =
          item.id !== selectedID
            ? () => {
                if (this.renameItem.id) {
                  this.renameItem = {
                    id: "",
                    name: "",
                  };
                }

                this.handleClick(item);
              }
            : undefined;
        const onDblclick = !isFolder
          ? (e: MouseEvent) => {
              if (!nodeHasClass(e.target, "name")) {
                return;
              }
              this.renameItem = {
                id: item.id,
                name: item.name,
              };
            }
          : undefined;
        currentTreeItems.push(item);
        itemList.push(
          <li
            key={`${item.id}-${level}`}
            data-index={treeItemIndex}
            class={cls}
            style={style}
            onDblclick={onDblclick}
            onClick={onClick}
            onMousedown={this.handleMousedown}
          >
            <APISettingTreeItemDropdown
              id={item.id}
              apiSettingType={item.settingType}
            />
            {icon}
            {item.id === renameItem.id && (
              <NInput
                class="renameInput"
                key={item.id}
                bordered={false}
                clearable
                defaultValue={renameItem.name}
                onVnodeMounted={(node) => {
                  node.el?.getElementsByTagName("input")[0]?.focus();
                }}
                onUpdateValue={(value) => {
                  this.renameValue = value;
                }}
                onInputBlur={() => {
                  this.handelRename();
                }}
              />
            )}
            {item.id !== renameItem.id && <span class="name">{item.name}</span>}
          </li>,
        );
        treeItemIndex++;
        // 未展开的则不需要展示子元素
        // 而且非展示所有子元素
        if (!item.expanded && !showAllChildren) {
          return;
        }
        appendToList(item.children, level + 1);
      });
    };
    appendToList(treeItems, 0);
    setTreeItems(currentTreeItems, topTreeItemIDList);
    return (
      <div class={itemsWrapperClass} ref="wrapper">
        <ul>{itemList}</ul>
      </div>
    );
  },
});


================================================
FILE: src/components/ExColumn.tsx
================================================
import { defineComponent, PropType, StyleValue } from "vue";
import { css } from "@linaria/core";
import { NDivider } from "naive-ui";

import { nodeInsertAt, nodeSetStyle, nodeRemove } from "../helpers/html";

const dividerClass = css`
  margin: 0 !important;
  height: 100% !important;
  position: absolute;
`;

export default defineComponent({
  name: "ExColumn",
  props: {
    width: {
      type: Number,
      default: 0,
    },
    left: {
      type: Number,
      default: 0,
    },
    showDivider: {
      type: Boolean,
      default: false,
    },
    onResize: {
      type: Function as PropType<(value: number) => void>,
      default: () => {
        return (value: number) => {
          console.info(value);
        };
      },
    },
  },
  setup(props) {
    let isDragging = false;
    let originClientX = 0;
    let target: EventTarget;
    let moveLeft = 0;
    const onMousemove = (e: MouseEvent) => {
      if (!isDragging) {
        return;
      }
      e.preventDefault();
      moveLeft = e.clientX - originClientX;
      nodeSetStyle(target, {
        left: `${e.clientX}px`,
      });
    };
    const onMouseup = () => {
      if (props.onResize) {
        props.onResize(moveLeft);
      }
      moveLeft = 0;
      isDragging = false;
      document.removeEventListener("mousemove", onMousemove);
      document.removeEventListener("mouseup", onMouseup);
      if (target) {
        nodeRemove(target);
      }
    };
    const onMousedown = (e: MouseEvent) => {
      isDragging = false;
      if (!e.currentTarget) {
        return;
      }

      originClientX = e.clientX;
      isDragging = true;
      document.addEventListener("mousemove", onMousemove);
      document.addEventListener("mouseup", onMouseup);

      // 提交公共方法至html无法复制(TODO确认原因)
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      target = e.currentTarget.cloneNode(true);

      nodeSetStyle(target, {
        left: `${originClientX}px`,
        width: "2px",
        zIndex: "9",
      });
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      nodeInsertAt(e.currentTarget.parentNode.parentNode, target, 0);
    };

    return {
      onMousedown,
    };
  },
  render() {
    const { left, width, showDivider } = this.$props;
    const { $slots } = this;
    const style: StyleValue = {
      position: "absolute",
      top: "0px",
      bottom: "0px",
      left: `${left}px`,
      width: `${width}px`,
      overflow: "hidden",
    };
    if (!width) {
      delete style.width;
      style.right = "0px";
    }
    const divider = showDivider && (
      <div
        style={{
          position: "absolute",
          cursor: "col-resize",
          padding: "0px 2px",
          height: "100%",
        }}
        onMousedown={this.onMousedown}
      >
        <NDivider vertical class={dividerClass} />
      </div>
    );
    return (
      <div style={style}>
        {divider}
        {$slots.default && $slots.default()}
      </div>
    );
  },
});


================================================
FILE: src/components/ExCookieEditor.tsx
================================================
import { defineComponent, PropType, ref } from "vue";
import {
  NForm,
  NFormItem,
  NInput,
  NP,
  NGrid,
  NGi,
  NDatePicker,
  useMessage,
  NButton,
  FormInst,
} from "naive-ui";
import dayjs from "dayjs";

import { i18nCookie, i18nCommon } from "../i18n";
import { Cookie } from "../commands/cookies";
import { useCookieStore } from "../stores/cookie";
import { newRequireRules, showError } from "../helpers/util";

export default defineComponent({
  name: "ExCookieEditor",
  props: {
    cookie: {
      type: Object as PropType<Cookie>,
      required: true,
    },
    onBack: {
      type: Function as PropType<() => void>,
      default: () => {
        console.info("back");
      },
    },
  },
  setup(props) {
    const message = useMessage();
    const cookieStore = useCookieStore();
    const formRef = ref<FormInst | null>(null);

    const updateValues = ref({
      name: props.cookie.name,
      path: props.cookie.path,
      domain: props.cookie.domain,
      value: props.cookie.value,
      expires: props.cookie.expires,
    });
    const cookie = Object.assign({}, props.cookie);
    const update = async () => {
      const { value, expires, name, path, domain } = updateValues.value;
      try {
        await new Promise((resolve, reject) => {
          if (!formRef.value) {
            return reject(new Error("form ref is null"));
          }
          formRef.value.validate((errors) => {
            if (!errors) {
              resolve(null);
              return;
            }
            reject(errors[0][0]);
          });
        });
        cookie.value = value;
        cookie.expires = expires;
        // 新增
        if (!cookie.name) {
          cookie.name = name;
          cookie.path = path || "/";
          cookie.domain = domain || "";
        }
        await cookieStore.addOrUpdate(cookie);
        if (props.onBack) {
          props.onBack();
        }
      } catch (err) {
        showError(message, err);
      }
    };
    return {
      updateValues,
      update,
      formRef,
    };
  },
  render() {
    const { cookie } = this.$props;
    let defaultExpires = null;
    if (cookie.expires) {
      defaultExpires = dayjs(cookie.expires).unix() * 1000;
    }
    const shortcuts = {
      [i18nCookie("neverExpired")]: 32503651200000,
    };

    const isAdd = !cookie.name;
    const rules = newRequireRules([
      "name",
      "value",
      "path",
      "domain",
      "expires",
    ]);
    return (
      <NForm ref="formRef" rules={rules} model={this.updateValues}>
        <NGrid xGap={15}>
          <NGi span={8}>
            <NFormItem label={i18nCookie("name")} path="name">
              {!isAdd && <NP>{cookie.name}</NP>}
              {isAdd && (
                <NInput
                  placeholder={i18nCookie("namePlaceholder")}
                  clearable
                  onUpdateValue={(value) => {
                    this.updateValues.name = value;
                  }}
                />
              )}
            </NFormItem>
          </NGi>
          <NGi span={8}>
            <NFormItem label={i18nCookie("path")} path="path">
              {!isAdd && <NP>{cookie.path}</NP>}
              {isAdd && (
                <NInput
                  placeholder={i18nCookie("pathPlaceholder")}
                  clearable
                  onUpdateValue={(value) => {
                    this.updateValues.path = value;
                  }}
                />
              )}
            </NFormItem>
          </NGi>
          <NGi span={8}>
            <NFormItem label={i18nCookie("domain")} path="domain">
              {!isAdd && <NP>{cookie.domain}</NP>}
              {isAdd && (
                <NInput
                  placeholder={i18nCookie("domainPlaceholder")}
                  clearable
                  onUpdateValue={(value) => {
                    this.updateValues.domain = value;
                  }}
                />
              )}
            </NFormItem>
          </NGi>
          <NGi span={8}>
            <NFormItem label={i18nCookie("value")} path="value">
              <NInput
                defaultValue={cookie.value}
                placeholder={i18nCookie("valuePlaceholder")}
                clearable
                onUpdateValue={(value) => {
                  this.updateValues.value = value;
                }}
              />
            </NFormItem>
          </NGi>
          <NGi span={16}>
            <NFormItem label={i18nCookie("expires")} path="expires">
              <NDatePicker
                class="widthFull"
                type="datetime"
                shortcuts={shortcuts}
                placeholder={i18nCookie("expiresPlaceholder")}
                defaultValue={defaultExpires}
                onUpdateValue={(value) => {
                  this.updateValues.expires = dayjs(value).toString();
                }}
                clearable
              />
            </NFormItem>
          </NGi>
          <NGi span={12}>
            <NButton
              class="widthFull"
              onClick={() => {
                this.update();
              }}
            >
              {i18nCommon("confirm")}
            </NButton>
          </NGi>
          <NGi span={12}>
            <NButton
              class="widthFull"
              onClick={() => {
                this.$props.onBack();
              }}
            >
              {i18nCommon("back")}
            </NButton>
          </NGi>
        </NGrid>
      </NForm>
    );
  },
});


================================================
FILE: src/components/ExDeleteCheck.tsx
================================================
import { defineComponent, ref, PropType } from "vue";
import { NButton, NIcon } from "naive-ui";
import { AlertCircleOutline, TrashOutline } from "@vicons/ionicons5";
import { css } from "@linaria/core";

const checkClass = css`
  color: #f2c97d;
  font-weight: 900;
`;

// 两次短时间点击则清除

export default defineComponent({
  name: "ExDeleteCheck",
  props: {
    onConfirm: {
      type: Function as PropType<() => void>,
      required: true,
    },
  },
  setup(props) {
    const deleting = ref(false);
    const handleClick = () => {
      if (!deleting.value) {
        deleting.value = true;
      } else if (props
Download .txt
gitextract_n8v88nit/

├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .github/
│   └── workflows/
│       ├── publish.yml
│       └── test.yml
├── .gitignore
├── .vscode/
│   └── extensions.json
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── README_uk.md
├── README_zh.md
├── cliff.toml
├── dev.md
├── index.html
├── package.json
├── splashscreen.html
├── src/
│   ├── App.tsx
│   ├── commands/
│   │   ├── api_collection.ts
│   │   ├── api_folder.ts
│   │   ├── api_setting.ts
│   │   ├── cookies.ts
│   │   ├── database.ts
│   │   ├── fake.ts
│   │   ├── fn.ts
│   │   ├── http_request.ts
│   │   ├── http_response.ts
│   │   ├── import_api.ts
│   │   ├── interface.ts
│   │   ├── invoke.ts
│   │   ├── variable.ts
│   │   └── window.ts
│   ├── components/
│   │   ├── APIResponse/
│   │   │   ├── index.tsx
│   │   │   ├── list.tsx
│   │   │   └── status_text.tsx
│   │   ├── APISettingParams/
│   │   │   ├── index.tsx
│   │   │   ├── req_params.tsx
│   │   │   └── uri.tsx
│   │   ├── APISettingTree/
│   │   │   ├── header.tsx
│   │   │   ├── index.tsx
│   │   │   ├── item_dropdown.tsx
│   │   │   └── items.tsx
│   │   ├── ExColumn.tsx
│   │   ├── ExCookieEditor.tsx
│   │   ├── ExDeleteCheck.tsx
│   │   ├── ExDialog.tsx
│   │   ├── ExForm.tsx
│   │   ├── ExKeyValue.tsx
│   │   ├── ExLoading.tsx
│   │   ├── ExPreview.tsx
│   │   └── ExTimer.tsx
│   ├── constants/
│   │   ├── common.ts
│   │   ├── handle_key.ts
│   │   ├── provide.ts
│   │   └── style.ts
│   ├── env.d.ts
│   ├── event.ts
│   ├── helpers/
│   │   ├── curl.js
│   │   ├── editor.ts
│   │   ├── hot_key.ts
│   │   ├── html.ts
│   │   ├── pinyin.ts
│   │   └── util.ts
│   ├── i18n/
│   │   ├── en.ts
│   │   ├── index.ts
│   │   ├── uk.ts
│   │   └── zh.ts
│   ├── icons.ts
│   ├── main.css
│   ├── main.ts
│   ├── root.tsx
│   ├── router/
│   │   ├── index.ts
│   │   └── routes.ts
│   ├── stores/
│   │   ├── api_collection.ts
│   │   ├── api_folder.ts
│   │   ├── api_setting.ts
│   │   ├── app.ts
│   │   ├── cookie.ts
│   │   ├── dialog.ts
│   │   ├── environment.ts
│   │   ├── global_req_header.ts
│   │   ├── header.ts
│   │   ├── local.ts
│   │   ├── pin_request.ts
│   │   ├── setting.ts
│   │   └── variable.ts
│   ├── userWorker.ts
│   └── views/
│       ├── AppHeader.tsx
│       ├── AppSetting.tsx
│       ├── Collection.tsx
│       ├── CookieSetting.tsx
│       ├── Dashboard.tsx
│       ├── StoreSetting.tsx
│       └── VariableSetting.tsx
├── src-tauri/
│   ├── .gitignore
│   ├── Cargo.toml
│   ├── build.rs
│   ├── icons/
│   │   └── icon.icns
│   ├── src/
│   │   ├── commands.rs
│   │   ├── cookies.rs
│   │   ├── entities/
│   │   │   ├── api_collections.rs
│   │   │   ├── api_folders.rs
│   │   │   ├── api_settings.rs
│   │   │   ├── mod.rs
│   │   │   ├── prelude.rs
│   │   │   ├── variables.rs
│   │   │   └── versions.rs
│   │   ├── error.rs
│   │   ├── http_request.rs
│   │   ├── main.rs
│   │   ├── schemas/
│   │   │   ├── api_collection.rs
│   │   │   ├── api_folder.rs
│   │   │   ├── api_setting.rs
│   │   │   ├── database.rs
│   │   │   ├── mod.rs
│   │   │   ├── variable.rs
│   │   │   └── version.rs
│   │   └── util.rs
│   └── tauri.conf.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
Download .txt
SYMBOL INDEX (492 symbols across 84 files)

FILE: src-tauri/build.rs
  function main (line 1) | fn main() {

FILE: src-tauri/src/commands.rs
  type CommandResult (line 7) | pub type CommandResult<T> = Result<T, CyberAPIError>;
  function close_splashscreen (line 11) | pub fn close_splashscreen(window: Window) {
  function add_api_setting (line 22) | pub async fn add_api_setting(setting: APISetting) -> CommandResult<APISe...
  function update_api_setting (line 29) | pub async fn update_api_setting(setting: APISetting) -> CommandResult<AP...
  function init_tables (line 36) | pub async fn init_tables() -> CommandResult<()> {
  function export_tables (line 42) | pub async fn export_tables() -> CommandResult<String> {
  function import_tables (line 48) | pub async fn import_tables(file: String) -> CommandResult<()> {
  function list_api_setting (line 55) | pub async fn list_api_setting(collection: String) -> CommandResult<Vec<A...
  function delete_api_settings (line 62) | pub async fn delete_api_settings(ids: Vec<String>) -> CommandResult<()> {
  function add_api_collection (line 69) | pub async fn add_api_collection(collection: APICollection) -> CommandRes...
  function update_api_collection (line 76) | pub async fn update_api_collection(collection: APICollection) -> Command...
  function list_api_collection (line 83) | pub async fn list_api_collection() -> CommandResult<Vec<APICollection>> {
  function delete_api_collection (line 89) | pub async fn delete_api_collection(id: String) -> CommandResult<u64> {
  function add_api_folder (line 98) | pub async fn add_api_folder(folder: APIFolder) -> CommandResult<APIFolde...
  function update_api_folder (line 105) | pub async fn update_api_folder(folder: APIFolder) -> CommandResult<APIFo...
  function list_api_folder (line 112) | pub async fn list_api_folder(collection: String) -> CommandResult<Vec<AP...
  function delete_api_folder (line 119) | pub async fn delete_api_folder(id: String) -> CommandResult<schemas::API...
  function add_variable (line 129) | pub async fn add_variable(value: Variable) -> CommandResult<Variable> {
  function update_variable (line 136) | pub async fn update_variable(value: Variable) -> CommandResult<Variable> {
  function delete_variable (line 143) | pub async fn delete_variable(ids: Vec<String>) -> CommandResult<u64> {
  function list_variable (line 149) | pub async fn list_variable(collection: String, category: String) -> Comm...
  function do_http_request (line 156) | pub async fn do_http_request(
  function list_cookie (line 166) | pub fn list_cookie() -> CommandResult<Vec<String>> {
  function delete_cookie (line 172) | pub fn delete_cookie(c: cookies::Cookie) -> CommandResult<()> {
  function add_cookie (line 179) | pub fn add_cookie(c: cookies::Cookie) -> CommandResult<()> {
  function clear_cookie (line 186) | pub fn clear_cookie() -> CommandResult<()> {
  function get_latest_version (line 193) | pub async fn get_latest_version() -> CommandResult<schemas::Version> {
  function add_version (line 200) | pub async fn add_version(version: schemas::Version) -> CommandResult<()> {

FILE: src-tauri/src/cookies.rs
  function init_store (line 15) | fn init_store() -> &'static Mutex<CookieStore> {
  constant COOKIE_FILE (line 30) | const COOKIE_FILE: &str = "cookies.json";
  type Cookie (line 33) | pub struct Cookie {
    method to_set_cookie_string (line 42) | fn to_set_cookie_string(&self) -> String {
    method get_url (line 58) | fn get_url(&self) -> String {
  function get_cookie_store (line 68) | pub fn get_cookie_store() -> MutexGuard<'static, CookieStore> {
  function save_store (line 73) | fn save_store(store: MutexGuard<CookieStore>) -> Result<(), CyberAPIErro...
  function delete_cookie_from_store (line 80) | pub fn delete_cookie_from_store(c: Cookie) -> Result<(), CyberAPIError> {
  function clear_cookie_from_store (line 95) | pub fn clear_cookie_from_store() -> Result<(), CyberAPIError> {
  function save_cookie_store (line 102) | pub fn save_cookie_store(set_cookies: Vec<String>, current_url: &Url) ->...
  function list_cookie (line 129) | pub fn list_cookie() -> Result<Vec<String>, CyberAPIError> {
  function add_cookie (line 142) | pub fn add_cookie(c: Cookie) -> Result<(), CyberAPIError> {

FILE: src-tauri/src/entities/api_collections.rs
  type Model (line 8) | pub struct Model {
  type Relation (line 18) | pub enum Relation {}

FILE: src-tauri/src/entities/api_folders.rs
  type Model (line 8) | pub struct Model {
  type Relation (line 19) | pub enum Relation {}

FILE: src-tauri/src/entities/api_settings.rs
  type Model (line 8) | pub struct Model {
  type Relation (line 20) | pub enum Relation {}

FILE: src-tauri/src/entities/variables.rs
  type Model (line 8) | pub struct Model {
  type Relation (line 21) | pub enum Relation {}

FILE: src-tauri/src/entities/versions.rs
  type Model (line 8) | pub struct Model {
  type Relation (line 17) | pub enum Relation {}

FILE: src-tauri/src/error.rs
  type CyberAPIError (line 5) | pub struct CyberAPIError {
    method from (line 11) | fn from(error: sea_orm::DbErr) -> Self {
    method from (line 19) | fn from(error: hyper::http::Error) -> Self {
    method from (line 27) | fn from(error: hyper::Error) -> Self {
    method from (line 35) | fn from(error: tauri::http::InvalidUri) -> Self {
    method from (line 43) | fn from(error: hyper::header::InvalidHeaderValue) -> Self {
    method from (line 52) | fn from(error: hyper::header::InvalidHeaderName) -> Self {
    method from (line 61) | fn from(error: hyper::header::ToStrError) -> Self {
    method from (line 70) | fn from(error: std::io::Error) -> Self {
    method from (line 78) | fn from(error: cookie_store::Error) -> Self {
    method from (line 87) | fn from(error: url::ParseError) -> Self {
    method from (line 96) | fn from(error: cookie_store::CookieError) -> Self {
    method from (line 105) | fn from(error: serde_json::Error) -> Self {
    method from (line 114) | fn from(error: base64::DecodeError) -> Self {
    method from (line 123) | fn from(error: ZipError) -> Self {
    method from (line 131) | fn from(error: cookie::ParseError) -> Self {

FILE: src-tauri/src/http_request.rs
  type HTTPRequestKVParam (line 31) | pub struct HTTPRequestKVParam {
  type HTTPRequest (line 39) | pub struct HTTPRequest {
  type RequestTimeout (line 50) | pub struct RequestTimeout {
  type HTTPStats (line 58) | pub struct HTTPStats {
    method new (line 72) | fn new() -> Self {
    method from (line 80) | fn from(trace: &HTTPTrace) -> Self {
  type HTTPTrace (line 96) | struct HTTPTrace {
    method now (line 126) | fn now(&self) -> u64 {
    method new (line 129) | fn new() -> Self {
    method reset (line 134) | fn reset(&self) {
    method set_cipher (line 151) | fn set_cipher(&self, value: String) {
    method get_cipher (line 160) | fn get_cipher(&self) -> String {
    method is_tls (line 175) | fn is_tls(&self) -> bool {
    method tls (line 178) | fn tls(&self) {
    method get_conn_from_pool (line 181) | fn get_conn_from_pool(&self) {
    method get_conn (line 184) | fn get_conn(&self) {
    method dns_start (line 187) | fn dns_start(&self) {
    method dns_done (line 190) | fn dns_done(&self) {
    method tcp_start (line 193) | fn tcp_start(&self) {
    method tcp_done (line 196) | fn tcp_done(&self) {
    method tls_start (line 199) | fn tls_start(&self) {
    method tls_done (line 202) | fn tls_done(&self) {
    method http_start (line 205) | fn http_start(&self) {
    method got_first_response_byte (line 209) | fn got_first_response_byte(&self) {
    method written (line 213) | fn written(&self) {
    method done (line 216) | fn done(&self) {
    method send_consuming (line 219) | fn send_consuming(&self) -> u32 {
    method dns_consuming (line 227) | fn dns_consuming(&self) -> u32 {
    method tcp_consuming (line 235) | fn tcp_consuming(&self) -> u32 {
    method tls_consuming (line 243) | fn tls_consuming(&self) -> u32 {
    method server_processing_consuming (line 252) | fn server_processing_consuming(&self) -> u32 {
    method content_transfer_consuming (line 262) | fn content_transfer_consuming(&self) -> u32 {
    method consuming (line 271) | fn consuming(&self) -> u32 {
  function get_http_trace (line 283) | fn get_http_trace() -> &'static HTTPTrace {
  type HTTPResponse (line 289) | pub struct HTTPResponse {
  type JsonVisitor (line 299) | struct JsonVisitor<'a>(&'a mut BTreeMap<String, String>);
  function record_debug (line 302) | fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn st...
  type HTTPTraceLayer (line 308) | pub struct HTTPTraceLayer;
    method on_event (line 315) | fn on_event(&self, event: &tracing::Event<'_>, _: tracing_subscriber::...
  function request (line 403) | pub async fn request(

FILE: src-tauri/src/main.rs
  function main (line 16) | fn main() {

FILE: src-tauri/src/schemas/api_collection.rs
  type APICollection (line 13) | pub struct APICollection {
    method from (line 27) | fn from(model: api_collections::Model) -> Self {
    method into_active_model (line 39) | fn into_active_model(self) -> api_collections::ActiveModel {
  function get_api_collections_create_sql (line 52) | pub fn get_api_collections_create_sql() -> String {
  function add_api_collection (line 63) | pub async fn add_api_collection(collection: APICollection) -> Result<API...
  function update_api_collection (line 70) | pub async fn update_api_collection(collection: APICollection) -> Result<...
  function list_api_collection (line 78) | pub async fn list_api_collection() -> Result<Vec<APICollection>, DbErr> {
  function delete_api_collection (line 85) | pub async fn delete_api_collection(id: String) -> Result<u64, DbErr> {
  function get_table_name_api_collection (line 91) | pub fn get_table_name_api_collection() -> String {
  function delete_all_api_collection (line 95) | pub async fn delete_all_api_collection() -> Result<(), DbErr> {
  function export_api_collection (line 101) | pub async fn export_api_collection() -> Result<ExportData, DbErr> {
  function import_api_collection (line 110) | pub async fn import_api_collection(data: Vec<serde_json::Value>) -> Resu...

FILE: src-tauri/src/schemas/api_folder.rs
  type APIFolder (line 14) | pub struct APIFolder {
    method from (line 30) | fn from(model: api_folders::Model) -> Self {
    method into_active_model (line 42) | fn into_active_model(self) -> api_folders::ActiveModel {
  type APIFolderChildren (line 58) | pub struct APIFolderChildren {
  function get_api_folders_create_sql (line 63) | pub fn get_api_folders_create_sql() -> String {
  function add_api_folder (line 75) | pub async fn add_api_folder(folder: APIFolder) -> Result<APIFolder, DbEr...
  function update_api_folder (line 82) | pub async fn update_api_folder(folder: APIFolder) -> Result<APIFolder, D...
  function list_api_folder (line 89) | pub async fn list_api_folder(collection: String) -> Result<Vec<APIFolder...
  function delete_api_folder_by_collection (line 98) | pub async fn delete_api_folder_by_collection(collection: String) -> Resu...
  function delete_api_folders (line 108) | pub async fn delete_api_folders(ids: Vec<String>) -> Result<u64, DbErr> {
  function list_api_folder_all_children (line 120) | pub async fn list_api_folder_all_children(id: String) -> Result<APIFolde...
  function get_table_name_api_folder (line 173) | pub fn get_table_name_api_folder() -> String {
  function delete_all_api_folder (line 177) | pub async fn delete_all_api_folder() -> Result<(), CyberAPIError> {
  function export_api_folder (line 183) | pub async fn export_api_folder() -> Result<ExportData, DbErr> {
  function import_api_folder (line 192) | pub async fn import_api_folder(data: Vec<serde_json::Value>) -> Result<(...

FILE: src-tauri/src/schemas/api_setting.rs
  type APISetting (line 12) | pub struct APISetting {
    method from (line 30) | fn from(model: api_settings::Model) -> Self {
    method into_active_model (line 43) | fn into_active_model(self) -> api_settings::ActiveModel {
  function get_api_settings_create_sql (line 58) | pub fn get_api_settings_create_sql() -> String {
  function add_api_setting (line 71) | pub async fn add_api_setting(setting: APISetting) -> Result<APISetting, ...
  function update_api_setting (line 77) | pub async fn update_api_setting(setting: APISetting) -> Result<APISettin...
  function list_api_setting (line 84) | pub async fn list_api_setting(collection: String) -> Result<Vec<APISetti...
  function delete_api_setting_by_collection (line 94) | pub async fn delete_api_setting_by_collection(collection: String) -> Res...
  function delete_api_settings (line 103) | pub async fn delete_api_settings(ids: Vec<String>) -> Result<u64, DbErr> {
  function get_table_name_api_setting (line 113) | pub fn get_table_name_api_setting() -> String {
  function delete_all_api_setting (line 117) | pub async fn delete_all_api_setting() -> Result<(), CyberAPIError> {
  function export_api_setting (line 124) | pub async fn export_api_setting() -> Result<ExportData, DbErr> {
  function import_api_setting (line 133) | pub async fn import_api_setting(data: Vec<serde_json::Value>) -> Result<...

FILE: src-tauri/src/schemas/database.rs
  type ExportData (line 35) | pub struct ExportData {
  function get_conn (line 40) | async fn get_conn() -> DatabaseConnection {
  function get_database (line 63) | pub async fn get_database() -> DatabaseConnection {
  function init_tables (line 68) | pub async fn init_tables() -> Result<(), DbErr> {
  function export_tables (line 84) | pub async fn export_tables() -> Result<String, CyberAPIError> {
  function import_tables (line 121) | pub async fn import_tables(filename: String) -> Result<(), CyberAPIError> {

FILE: src-tauri/src/schemas/variable.rs
  type Variable (line 13) | pub struct Variable {
    method from (line 33) | fn from(model: variables::Model) -> Self {
    method into_active_model (line 47) | fn into_active_model(self) -> variables::ActiveModel {
  function get_variables_create_sql (line 63) | pub fn get_variables_create_sql() -> String {
  function add_variable (line 77) | pub async fn add_variable(value: Variable) -> Result<Variable, DbErr> {
  function update_variable (line 84) | pub async fn update_variable(value: Variable) -> Result<Variable, DbErr> {
  function list_variable (line 91) | pub async fn list_variable(collection: String, category: String) -> Resu...
  function delete_variable (line 101) | pub async fn delete_variable(ids: Vec<String>) -> Result<u64, DbErr> {
  function get_table_name_variable (line 110) | pub fn get_table_name_variable() -> String {
  function delete_all_variable (line 114) | pub async fn delete_all_variable() -> Result<(), CyberAPIError> {
  function export_variable (line 120) | pub async fn export_variable() -> Result<ExportData, DbErr> {
  function import_variable (line 129) | pub async fn import_variable(data: Vec<serde_json::Value>) -> Result<(),...

FILE: src-tauri/src/schemas/version.rs
  type Version (line 10) | pub struct Version {
    method from (line 22) | fn from(model: versions::Model) -> Self {
  function get_versions_table_create_sql (line 32) | pub fn get_versions_table_create_sql() -> String {
  function add_version (line 42) | pub async fn add_version(version: Version) -> Result<Version, DbErr> {
  function get_latest_version (line 56) | pub async fn get_latest_version() -> Result<Version, DbErr> {

FILE: src-tauri/src/util.rs
  function set_app_dir (line 4) | pub fn set_app_dir(dir: String) {
  function get_app_dir (line 8) | pub fn get_app_dir() -> &'static String {

FILE: src/App.tsx
  method setup (line 21) | setup() {
  method render (line 48) | render() {

FILE: src/commands/api_collection.ts
  type APICollection (line 16) | interface APICollection {
  function newDefaultAPICollection (line 29) | function newDefaultAPICollection(): APICollection {
  function createAPICollection (line 40) | async function createAPICollection(
  function listAPICollection (line 52) | async function listAPICollection(): Promise<APICollection[]> {
  function updateAPICollection (line 59) | async function updateAPICollection(collection: APICollection) {
  function deleteAPICollection (line 69) | async function deleteAPICollection(id: string) {

FILE: src/commands/api_folder.ts
  type APIFolder (line 16) | interface APIFolder {
  function newDefaultAPIFolder (line 29) | function newDefaultAPIFolder(): APIFolder {
  function createAPIFolder (line 41) | async function createAPIFolder(folder: APIFolder): Promise<void> {
  function listAPIFolder (line 51) | async function listAPIFolder(collection: string): Promise<APIFolder[]> {
  function updateAPIFolder (line 61) | async function updateAPIFolder(folder: APIFolder) {
  function deleteAPIFolder (line 70) | async function deleteAPIFolder(id: string): Promise<{

FILE: src/commands/api_setting.ts
  type APISetting (line 16) | interface APISetting {
  function newDefaultAPISetting (line 32) | function newDefaultAPISetting(): APISetting {
  function createAPISetting (line 45) | async function createAPISetting(setting: APISetting): Promise<void> {
  function listAPISetting (line 75) | async function listAPISetting(
  function updateAPISetting (line 87) | async function updateAPISetting(setting: APISetting) {
  function deleteAPISettings (line 97) | async function deleteAPISettings(ids: string[]) {

FILE: src/commands/cookies.ts
  type Cookie (line 11) | interface Cookie {
  function listCookie (line 20) | async function listCookie(): Promise<Cookie[]> {
  function deleteCookie (line 71) | async function deleteCookie(c: Cookie) {
  function clearCookie (line 80) | async function clearCookie() {
  function addOrUpdate (line 87) | async function addOrUpdate(c: Cookie) {

FILE: src/commands/database.ts
  type Version (line 16) | interface Version {
  function getDatabaseLatestVersion (line 24) | async function getDatabaseLatestVersion() {
  function handleDatabaseCompatible (line 32) | async function handleDatabaseCompatible() {
  function exportTables (line 59) | async function exportTables(): Promise<string> {
  function importTables (line 63) | async function importTables(file: string) {

FILE: src/commands/fake.ts
  type WithID (line 5) | interface WithID {
  function getStore (line 9) | function getStore(name: string): LocalForage {
  function fakeList (line 21) | async function fakeList<T>(storeName: string): Promise<T[]> {
  function fakeAdd (line 30) | async function fakeAdd<T>(storeName: string, data: T) {
  function fakeUpdate (line 36) | async function fakeUpdate<T extends WithID>(storeName: string, data: T) {
  function fakeDeleteAPICollection (line 50) | async function fakeDeleteAPICollection<T extends WithID>(
  function fakeDeleteItems (line 68) | async function fakeDeleteItems<T extends WithID>(
  function fakeUpdateStore (line 82) | async function fakeUpdateStore(storeName: string, data: unknown) {

FILE: src/commands/fn.ts
  type FnHandler (line 16) | interface FnHandler {
  type Fn (line 26) | enum Fn {
  function trimParam (line 44) | function trimParam(param: string): string | string[] {
  function parseFunctions (line 61) | function parseFunctions(collection: string, value: string): FnHandler[] {
  type FsParams (line 85) | interface FsParams {
  function getDir (line 90) | function getDir(dir: string): BaseDirectory {
  function convertToFsParams (line 104) | function convertToFsParams(p: unknown): FsParams {
  function doFnHandler (line 121) | async function doFnHandler(handler: FnHandler): Promise<string> {

FILE: src/commands/http_request.ts
  type HTTPMethod (line 18) | enum HTTPMethod {
  type ContentType (line 28) | enum ContentType {
  type RequestTimeout (line 36) | interface RequestTimeout {
  type HTTPRequest (line 43) | interface HTTPRequest {
  function convertKVListToURLValues (line 54) | function convertKVListToURLValues(kvList: KVParam[]) {
  function convertRequestToCURL (line 68) | async function convertRequestToCURL(
  function is_json (line 135) | function is_json(str: string) {
  function convertBody (line 144) | async function convertBody(collection: string, data: string) {
  function convertKVParams (line 170) | async function convertKVParams(collection: string, params: KVParam[]) {
  type MultipartFormData (line 193) | interface MultipartFormData {
  function convertMultipartForm (line 203) | async function convertMultipartForm(body: string): Promise<MultipartForm...
  function getUserAgent (line 242) | async function getUserAgent() {
  function doHTTPRequest (line 254) | async function doHTTPRequest(options: {

FILE: src/commands/http_response.ts
  type HTTPStats (line 12) | interface HTTPStats {
  type HTTPResponse (line 25) | interface HTTPResponse {
  type Events (line 43) | type Events = {
  type ResponseBodyCategory (line 48) | enum ResponseBodyCategory {
  type ResponseBodyResult (line 53) | interface ResponseBodyResult {
  function getStatusText (line 131) | function getStatusText(code: number) {
  function getResponseBody (line 135) | function getResponseBody(resp: HTTPResponse): ResponseBodyResult {
  type Response (line 188) | interface Response {
  function addLatestResponse (line 196) | async function addLatestResponse(resp: HTTPResponse) {
  function getLatestResponseList (line 215) | async function getLatestResponseList(id: string) {
  function clearLatestResponseList (line 220) | async function clearLatestResponseList(id: string) {
  function getLatestResponse (line 228) | async function getLatestResponse(id: string) {
  function onSelectResponse (line 235) | function onSelectResponse(ln: (resp: HTTPResponse) => void) {
  function selectResponse (line 246) | function selectResponse(resp: HTTPResponse) {

FILE: src/commands/import_api.ts
  type PostManSetting (line 25) | interface PostManSetting {
  type InsomniaSetting (line 44) | interface InsomniaSetting {
  type ImportData (line 67) | interface ImportData {
  type ImportCategory (line 72) | enum ImportCategory {
  function convertPostManAPISetting (line 80) | function convertPostManAPISetting(item: PostManSetting, collection: stri...
  function convertSwaggerSetting (line 131) | function convertSwaggerSetting(params: {
  function convertPostManSetting (line 217) | function convertPostManSetting(params: {
  function convertInsomniaSetting (line 254) | function convertInsomniaSetting(params: {
  function importAPI (line 350) | async function importAPI(params: {

FILE: src/commands/interface.ts
  type KVParam (line 1) | interface KVParam {

FILE: src/commands/invoke.ts
  function run (line 40) | async function run<T>(cmd: string, args?: InvokeArgs): Promise<T> {

FILE: src/commands/variable.ts
  type VariableStatus (line 15) | enum VariableStatus {
  type VariableCategory (line 20) | enum VariableCategory {
  type Variable (line 29) | interface Variable {
  function newDefaultVariable (line 46) | function newDefaultVariable(): Variable {
  function createVariable (line 60) | async function createVariable(value: Variable) {
  function listVariable (line 69) | async function listVariable(
  function updateVariable (line 82) | async function updateVariable(value: Variable) {
  function deleteVariable (line 92) | async function deleteVariable(ids: string[]) {

FILE: src/commands/window.ts
  function closeSplashscreen (line 5) | function closeSplashscreen() {
  function showSplashscreen (line 9) | function showSplashscreen() {
  function setWindowSize (line 20) | async function setWindowSize(width: number, height: number) {

FILE: src/components/APIResponse/index.tsx
  method setup (line 120) | setup(props) {
  method render (line 298) | render() {

FILE: src/components/APIResponse/list.tsx
  method setup (line 40) | setup(props) {
  method render (line 75) | render() {

FILE: src/components/APIResponse/status_text.tsx
  function getStatusType (line 5) | function getStatusType(statusCode: number) {
  method render (line 23) | render() {

FILE: src/components/APISettingParams/index.tsx
  method setup (line 40) | setup() {
  method render (line 175) | render() {

FILE: src/components/APISettingParams/req_params.tsx
  type TabItem (line 38) | enum TabItem {
  function shouldHaveBody (line 99) | function shouldHaveBody(method: string) {
  function shouldShowEditor (line 105) | function shouldShowEditor(contentType: string) {
  function createBadgeTab (line 111) | function createBadgeTab(params: {
  function createBodyBadge (line 129) | function createBodyBadge(params: { contentType: string; body: string }) {
  method setup (line 175) | setup(props) {
  method render (line 346) | render() {

FILE: src/components/APISettingParams/uri.tsx
  type CuttingURIResult (line 65) | interface CuttingURIResult {
  function cuttingURI (line 70) | function cuttingURI(uri: string): CuttingURIResult {
  type RequestURI (line 83) | interface RequestURI {
  method setup (line 107) | setup(props) {
  method render (line 185) | render() {

FILE: src/components/APISettingTree/header.tsx
  method setup (line 80) | setup() {
  method render (line 162) | render() {

FILE: src/components/APISettingTree/index.tsx
  method setup (line 55) | setup() {
  method render (line 116) | render() {

FILE: src/components/APISettingTree/item_dropdown.tsx
  method setup (line 60) | setup(props) {
  method render (line 261) | render() {

FILE: src/components/APISettingTree/items.tsx
  type OverType (line 133) | enum OverType {
  type TreeItem (line 139) | interface TreeItem {
  function getMethodColorType (line 153) | function getMethodColorType(method: string) {
  function convertToTreeItems (line 169) | function convertToTreeItems(params: {
  method setup (line 311) | setup() {
  method render (line 621) | render() {

FILE: src/components/ExColumn.tsx
  method setup (line 37) | setup(props) {
  method render (line 94) | render() {

FILE: src/components/ExCookieEditor.tsx
  method setup (line 35) | setup(props) {
  method render (line 85) | render() {

FILE: src/components/ExDeleteCheck.tsx
  method setup (line 21) | setup(props) {
  method render (line 36) | render() {

FILE: src/components/ExDialog.tsx
  type OnConfirm (line 33) | interface OnConfirm {
  type DialogOption (line 36) | interface DialogOption {
  type ImportDialogOption (line 44) | interface ImportDialogOption {
  function newDialog (line 50) | function newDialog(option: DialogOption) {
  method setup (line 99) | setup(props) {
  method render (line 205) | render() {
  function newImportDialog (line 278) | function newImportDialog(option: ImportDialogOption) {

FILE: src/components/ExForm.tsx
  type ExUpdateData (line 17) | interface ExUpdateData {
  type ExOnSubmit (line 20) | interface ExOnSubmit {
  type ExFormItem (line 24) | interface ExFormItem {
  method setup (line 49) | setup(props) {
  method render (line 92) | render() {

FILE: src/components/ExKeyValue.tsx
  type HandleOptionCategory (line 46) | enum HandleOptionCategory {
  type HandleOption (line 51) | interface HandleOption {
  type KVItem (line 58) | type KVItem = {
  method setup (line 87) | setup(props) {
  method render (line 183) | render() {

FILE: src/components/ExLoading.tsx
  method setup (line 30) | setup() {
  method render (line 40) | render() {

FILE: src/components/ExPreview.tsx
  function isSupportPreview (line 15) | function isSupportPreview(contentType: string) {
  method render (line 32) | render() {

FILE: src/components/ExTimer.tsx
  method setup (line 12) | setup() {
  method render (line 23) | render() {

FILE: src/constants/handle_key.ts
  type HandleKey (line 1) | enum HandleKey {

FILE: src/event.ts
  function initWindowEvent (line 5) | async function initWindowEvent() {

FILE: src/helpers/curl.js
  function parse (line 46) | function parse(s) {
  function rewrite (line 133) | function rewrite(args) {
  function parseField (line 150) | function parseField(s) {
  function isURL (line 158) | function isURL(s) {

FILE: src/helpers/editor.ts
  function createEditor (line 3) | function createEditor(params: {
  function replaceContent (line 24) | function replaceContent(

FILE: src/helpers/hot_key.ts
  function match (line 4) | function match(hotKey: string, e: KeyboardEvent) {
  function hotKeyCreateHTTPSetting (line 31) | function hotKeyCreateHTTPSetting() {
  function hotKeyMatchCreateHTTPSetting (line 35) | function hotKeyMatchCreateHTTPSetting(e: KeyboardEvent) {
  function hotKeyCreateFolder (line 39) | function hotKeyCreateFolder() {
  function hotKeyMatchCreateFolder (line 43) | function hotKeyMatchCreateFolder(e: KeyboardEvent) {

FILE: src/helpers/html.ts
  function nodeRemove (line 4) | function nodeRemove(node) {
  function nodeInsertAt (line 12) | function nodeInsertAt(fatherNode, node, position) {
  function nodeInsertBefore (line 22) | function nodeInsertBefore(node, beforeNode) {
  function nodeSetStyle (line 40) | function nodeSetStyle(node, style) {
  function nodeGetOffset (line 46) | function nodeGetOffset(node) {
  function nodGetScrollTop (line 62) | function nodGetScrollTop(node) {
  function nodeAddClass (line 71) | function nodeAddClass(node, cls) {
  function nodeHasClass (line 80) | function nodeHasClass(node, cls) {
  function nodeRemoveClass (line 89) | function nodeRemoveClass(node, cls) {
  function nodeGetOffsetHeightWidth (line 98) | function nodeGetOffsetHeightWidth(node) {
  function nodeGetDataValue (line 113) | function nodeGetDataValue(node, key) {
  function nodeGetTagName (line 122) | function nodeGetTagName(node) {

FILE: src/helpers/pinyin.ts
  function newPinYinMap (line 1) | function newPinYinMap() {
  function getPinYin (line 423) | function getPinYin(str: string): string[] {

FILE: src/helpers/util.ts
  function isWebMode (line 17) | function isWebMode() {
  function setAppTitle (line 21) | async function setAppTitle(title: string) {
  function formatError (line 31) | function formatError(err: Error | unknown): string {
  function showError (line 43) | function showError(message: MessageApi, err: Error | unknown): void {
  function formatDate (line 50) | function formatDate(str: string): string {
  function formatSimpleDate (line 57) | function formatSimpleDate(str: string): string {
  function getBodyWidth (line 69) | function getBodyWidth(): number {
  function getNormalDialogStyle (line 73) | function getNormalDialogStyle(percent = 0.7) {
  function newRequireRules (line 82) | function newRequireRules(keys: string[]) {
  function tryToParseArray (line 93) | function tryToParseArray(data: string) {
  function writeTextToClipboard (line 104) | async function writeTextToClipboard(text: string) {
  function readTextFromClipboard (line 112) | async function readTextFromClipboard() {
  function reload (line 119) | async function reload() {
  function delay (line 127) | async function delay(ms: number) {
  function formatLatency (line 131) | function formatLatency(ms: number) {
  function isJSON (line 141) | function isJSON(data: string) {
  function jsonFormat (line 149) | function jsonFormat(data: string) {
  function convertHTTPHeaderName (line 170) | function convertHTTPHeaderName(name: string) {
  function stringToArrayBuffer (line 177) | function stringToArrayBuffer(data: string): Promise<ArrayBuffer> {
  function isMatchTextOrPinYin (line 188) | function isMatchTextOrPinYin(content: string, keyword: string) {
  function writeFileToDownload (line 205) | async function writeFileToDownload(file: string, data: ArrayBuffer) {
  function writeSettingToDownload (line 230) | async function writeSettingToDownload(arr: unknown, name: string) {
  function isMacOS (line 236) | async function isMacOS() {

FILE: src/i18n/index.ts
  type LANG (line 8) | enum LANG {
  function getLocale (line 25) | function getLocale() {
  function getCurrentLang (line 35) | function getCurrentLang() {
  function changeI18nLocale (line 39) | function changeI18nLocale(locale: string) {
  function i18nGet (line 45) | function i18nGet(
  function newI18nGet (line 52) | function newI18nGet(prefix: string) {

FILE: src/main.ts
  function init (line 25) | async function init() {

FILE: src/root.tsx
  method setup (line 24) | setup() {
  method render (line 56) | render() {

FILE: src/router/index.ts
  function goTo (line 17) | function goTo(
  type loadingEvent (line 31) | type loadingEvent = () => void;
  function setLoadingEvent (line 40) | function setLoadingEvent(

FILE: src/router/routes.ts
  type Router (line 6) | interface Router {

FILE: src/stores/api_collection.ts
  type TabActiveData (line 20) | interface TabActiveData {
  function toggleFolderExpanded (line 24) | async function toggleFolderExpanded(
  method findByID (line 57) | findByID(id: string) {
  method closeAllFolders (line 60) | async closeAllFolders(collection: string) {
  method fetchExpandedFolders (line 65) | async fetchExpandedFolders(collection: string) {
  method fetchTopTreeItems (line 72) | async fetchTopTreeItems(collection: string) {
  method fetchActiveTabs (line 78) | async fetchActiveTabs() {
  method getActiveTab (line 83) | getActiveTab(id: string) {
  method updateActiveTab (line 86) | async updateActiveTab(params: { id: string; activeTab: string }) {
  method updateTopTreeItems (line 102) | async updateTopTreeItems(collection: string, idList: string[]) {
  method openFolder (line 106) | async openFolder(collection: string, folder: string) {
  method closeFolder (line 111) | async closeFolder(collection: string, folder: string) {
  method add (line 116) | async add(data: APICollection): Promise<void> {
  method get (line 130) | async get(id: string): Promise<APICollection | undefined> {
  method fetch (line 136) | async fetch(): Promise<void> {
  method update (line 147) | async update(data: APICollection): Promise<void> {
  method remove (line 165) | async remove(id: string): Promise<void> {

FILE: src/stores/api_folder.ts
  method findByID (line 24) | findByID(id: string): APIFolder {
  method updateByID (line 28) | async updateByID(id: string, data: unknown) {
  method add (line 33) | async add(data: APIFolder): Promise<void> {
  method fetch (line 47) | async fetch(collection: string): Promise<void> {
  method addChild (line 65) | async addChild(params: {
  method update (line 133) | async update(data: APIFolder): Promise<void> {
  method remove (line 151) | async remove(id: string): Promise<void> {

FILE: src/stores/api_setting.ts
  type SettingType (line 20) | enum SettingType {
  method setWindowTitle (line 37) | async setWindowTitle(id: string) {
  method select (line 48) | select(id: string) {
  method getHTTPRequest (line 54) | getHTTPRequest(id: string) {
  method fillValues (line 61) | fillValues(req: HTTPRequest) {
  method getHTTPRequestFillValues (line 83) | getHTTPRequestFillValues(id: string) {
  method findByID (line 98) | findByID(id: string): APISetting {
  method updateByID (line 102) | async updateByID(id: string, data: unknown) {
  method add (line 107) | async add(data: APISetting) {
  method fetch (line 121) | async fetch(collection: string): Promise<void> {
  method update (line 137) | async update(data: APISetting) {
  method remove (line 160) | async remove(id: string) {

FILE: src/stores/app.ts
  method fetch (line 23) | async fetch() {

FILE: src/stores/cookie.ts
  function isSameCookie (line 12) | function isSameCookie(c1: Cookie, c2: Cookie) {
  method fetch (line 26) | async fetch() {
  method remove (line 38) | async remove(c: Cookie) {
  method clear (line 53) | async clear() {
  method addOrUpdate (line 65) | async addOrUpdate(c: Cookie) {

FILE: src/stores/dialog.ts
  method toggleSettingDialog (line 15) | toggleSettingDialog(shown: boolean) {
  method toggleCookieDialog (line 18) | toggleCookieDialog(shown: boolean) {
  method toggleEnvironmentDialog (line 21) | toggleEnvironmentDialog(shown: boolean) {
  method toggleStoreDialog (line 24) | toggleStoreDialog(shown: boolean) {
  method toggleCustomizeVariableDialog (line 27) | toggleCustomizeVariableDialog(shown: boolean) {
  method toggleReqHeaderDialog (line 30) | toggleReqHeaderDialog(shown: boolean) {

FILE: src/stores/header.ts
  type Breadcrumb (line 3) | interface Breadcrumb {
  method add (line 15) | add(breadcrumb: Breadcrumb) {
  method clear (line 27) | clear() {

FILE: src/stores/local.ts
  function createNewStore (line 5) | function createNewStore(name: string) {
  type StoreKey (line 18) | enum StoreKey {
  function getLang (line 54) | async function getLang() {
  function setLang (line 63) | async function setLang(lang: string) {
  function clearStore (line 67) | async function clearStore(name: StoreKey) {

FILE: src/stores/pin_request.ts
  type LatestRequest (line 4) | interface LatestRequest {
  method fetch (line 17) | async fetch(collection: string) {
  method save (line 30) | async save() {
  method add (line 35) | async add(collection: string, req: LatestRequest) {
  method remove (line 47) | async remove(id: string) {

FILE: src/stores/setting.ts
  type Timeout (line 10) | interface Timeout {
  type AppSetting (line 16) | interface AppSetting {
  type ResizeType (line 33) | enum ResizeType {
  function getAppSetting (line 40) | async function getAppSetting(): Promise<AppSetting> {
  function updateAppSetting (line 48) | function updateAppSetting(data: AppSetting): Promise<AppSetting> {
  function updateAppLatestRoute (line 52) | async function updateAppLatestRoute(route: {
  function getAppLatestRoute (line 62) | async function getAppLatestRoute() {
  function isDarkTheme (line 67) | function isDarkTheme(theme: string) {
  method fetch (line 95) | async fetch(): Promise<void> {
  method updateTheme (line 143) | async updateTheme(theme: string) {
  method updateCollectionSortType (line 151) | async updateCollectionSortType(sortType: string) {
  method updateCollectionColumnWidths (line 157) | async updateCollectionColumnWidths(widths: number[]) {
  method updateParamsColumnWidth (line 163) | async updateParamsColumnWidth(width: number) {
  method updateSize (line 172) | async updateSize(width: number, height: number) {
  method updateResizeType (line 184) | async updateResizeType(resizeType: string) {
  method resize (line 190) | async resize() {
  method getRequestTimeout (line 198) | getRequestTimeout() {
  method updateRequestTimeout (line 201) | async updateRequestTimeout(params: Timeout) {

FILE: src/stores/variable.ts
  function newVariableStore (line 19) | function newVariableStore(name: string, category: string) {

FILE: src/userWorker.ts
  method getWorker (line 12) | getWorker(_: any, label: string) {

FILE: src/views/AppHeader.tsx
  type FnKey (line 90) | enum FnKey {
  method setup (line 104) | setup() {
  method render (line 282) | render() {

FILE: src/views/AppSetting.tsx
  method setup (line 26) | setup() {
  method render (line 141) | render() {

FILE: src/views/Collection.tsx
  method setup (line 47) | setup() {
  method render (line 225) | render() {

FILE: src/views/CookieSetting.tsx
  type Mode (line 24) | enum Mode {
  method setup (line 36) | setup() {
  method render (line 95) | render() {

FILE: src/views/Dashboard.tsx
  type SortType (line 66) | enum SortType {
  function filterAndSort (line 96) | function filterAndSort(
  method setup (line 150) | setup() {
  method render (line 258) | render() {

FILE: src/views/StoreSetting.tsx
  method setup (line 29) | setup() {
  method render (line 65) | render() {

FILE: src/views/VariableSetting.tsx
  function convertKVParams (line 32) | function convertKVParams(variables: Variable[]): KVParam[] {
  method setup (line 62) | setup(props) {
  method render (line 167) | render() {
Condensed preview — 123 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (492K chars).
[
  {
    "path": ".eslintignore",
    "chars": 195,
    "preview": "# don't ever lint node_modules\nnode_modules\n# don't lint build output (make sure it's set to your correct build folder n"
  },
  {
    "path": ".eslintrc.js",
    "chars": 298,
    "preview": "module.exports = {\n  root: true,\n  parser: 'vue-eslint-parser',\n  plugins: [\n    '@typescript-eslint',\n  ],\n  parserOpti"
  },
  {
    "path": ".gitattributes",
    "chars": 28,
    "preview": "*.ts linguist-language=rust\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 1115,
    "preview": "name: \"publish\"\non:\n  push:\n    branches:\n      - release\n\njobs:\n  publish-tauri:\n    strategy:\n      fail-fast: false\n "
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 543,
    "preview": "name: \"test\"\non:\n  push:\n    branches: [ main ]\njobs:\n  publish-tauri:\n    strategy:\n      fail-fast: false\n      matrix"
  },
  {
    "path": ".gitignore",
    "chars": 264,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 39,
    "preview": "{\n  \"recommendations\": [\"Vue.volar\"]\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 4100,
    "preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## [0.1.21] - 2024-05-30\n\n### Bug Fix"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "Makefile",
    "chars": 555,
    "preview": ".PHONY: default\n\nlint:\n\tcd src-tauri && cargo clippy\nfmt:\n\tcd src-tauri && cargo fmt --all --\ndev:\n\tcargo tauri dev\nicon"
  },
  {
    "path": "README.md",
    "chars": 2237,
    "preview": "# Cyber API\n\n\n[![test library](https://img.shields.io/github/workflow/status/vicanso/cyberapi/test?label=test)](https://"
  },
  {
    "path": "README_uk.md",
    "chars": 2142,
    "preview": "# Cyber API\n\n\n[![test library](https://img.shields.io/github/workflow/status/vicanso/cyberapi/test?label=test)](https://"
  },
  {
    "path": "README_zh.md",
    "chars": 5204,
    "preview": "# Cyber API\n\n\n[![test library](https://img.shields.io/github/workflow/status/vicanso/cyberapi/test?label=test)](https://"
  },
  {
    "path": "cliff.toml",
    "chars": 2223,
    "preview": "# configuration file for git-cliff (0.1.0)\n\n[changelog]\n# changelog header\nheader = \"\"\"\n# Changelog\\n\nAll notable change"
  },
  {
    "path": "dev.md",
    "chars": 219,
    "preview": "# 查询证件列表\n\n```\nsecurity find-identity -v -p codesigning\n```\n# 校验IMG\n\n校验IMG是否使用证件签名:\n```\nspctl -a -v src-tauri/target/rele"
  },
  {
    "path": "index.html",
    "chars": 918,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"I"
  },
  {
    "path": "package.json",
    "chars": 1726,
    "preview": "{\n  \"name\": \"cyberapi\",\n  \"private\": true,\n  \"version\": \"\",\n  \"scripts\": {\n    \"format\": \"prettier --write src/*.ts src/"
  },
  {
    "path": "splashscreen.html",
    "chars": 6723,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\""
  },
  {
    "path": "src/App.tsx",
    "chars": 4039,
    "preview": "import { defineComponent, onMounted } from \"vue\";\nimport { NLayout, NLayoutHeader, useLoadingBar, NModal } from \"naive-u"
  },
  {
    "path": "src/commands/api_collection.ts",
    "chars": 1592,
    "preview": "import dayjs from \"dayjs\";\nimport { ulid } from \"ulid\";\n\nimport { isWebMode } from \"../helpers/util\";\nimport { fakeAdd, "
  },
  {
    "path": "src/commands/api_folder.ts",
    "chars": 2851,
    "preview": "import dayjs from \"dayjs\";\nimport { ulid } from \"ulid\";\n\nimport { isWebMode } from \"../helpers/util\";\nimport {\n  run,\n  "
  },
  {
    "path": "src/commands/api_setting.ts",
    "chars": 2314,
    "preview": "import dayjs from \"dayjs\";\nimport { ulid } from \"ulid\";\n\nimport { isWebMode } from \"../helpers/util\";\nimport {\n  run,\n  "
  },
  {
    "path": "src/commands/cookies.ts",
    "chars": 2059,
    "preview": "import { get, values } from \"lodash-es\";\nimport { isWebMode } from \"../helpers/util\";\nimport {\n  cmdAddCookie,\n  cmdClea"
  },
  {
    "path": "src/commands/database.ts",
    "chars": 1480,
    "preview": "import { getVersion } from \"@tauri-apps/api/app\";\nimport { message } from \"@tauri-apps/api/dialog\";\nimport dayjs from \"d"
  },
  {
    "path": "src/commands/fake.ts",
    "chars": 1976,
    "preview": "import localforage from \"localforage\";\n\nconst stores = new Map<string, LocalForage>();\n\ninterface WithID {\n  id: string;"
  },
  {
    "path": "src/commands/fn.ts",
    "chars": 4845,
    "preview": "import {\n  BaseDirectory,\n  FsOptions,\n  readBinaryFile,\n  readTextFile,\n} from \"@tauri-apps/api/fs\";\nimport { open } fr"
  },
  {
    "path": "src/commands/http_request.ts",
    "chars": 10387,
    "preview": "import { forEach, isArray } from \"lodash-es\";\nimport { encode } from \"js-base64\";\nimport { ulid } from \"ulid\";\nimport { "
  },
  {
    "path": "src/commands/http_response.ts",
    "chars": 5910,
    "preview": "import { decode } from \"js-base64\";\nimport dayjs from \"dayjs\";\nimport { forEach } from \"lodash-es\";\nimport mitt, { Emitt"
  },
  {
    "path": "src/commands/import_api.ts",
    "chars": 13304,
    "preview": "import { Promise } from \"bluebird\";\nimport { get, uniq, forEach, has } from \"lodash-es\";\nimport dayjs from \"dayjs\";\nimpo"
  },
  {
    "path": "src/commands/interface.ts",
    "chars": 107,
    "preview": "export interface KVParam {\n  [key: string]: unknown;\n  key: string;\n  value: string;\n  enabled: boolean;\n}\n"
  },
  {
    "path": "src/commands/invoke.ts",
    "chars": 2032,
    "preview": "import { invoke, InvokeArgs } from \"@tauri-apps/api/tauri\";\nimport Debug from \"debug\";\nimport { isWebMode } from \"../hel"
  },
  {
    "path": "src/commands/variable.ts",
    "chars": 2000,
    "preview": "import { ulid } from \"ulid\";\nimport dayjs from \"dayjs\";\nimport { isWebMode } from \"../helpers/util\";\nimport {\n  cmdAddVa"
  },
  {
    "path": "src/commands/window.ts",
    "chars": 698,
    "preview": "import { run } from \"./invoke\";\nimport { appWindow, LogicalSize, getAll } from \"@tauri-apps/api/window\";\nimport { isWebM"
  },
  {
    "path": "src/components/APIResponse/index.tsx",
    "chars": 14203,
    "preview": "import {\n  defineComponent,\n  onBeforeUnmount,\n  onMounted,\n  PropType,\n  ref,\n  StyleValue,\n  watch,\n} from \"vue\";\nimpo"
  },
  {
    "path": "src/components/APIResponse/list.tsx",
    "chars": 3710,
    "preview": "import {\n  NButton,\n  NDivider,\n  NDropdown,\n  NIcon,\n  NP,\n  NSpace,\n  useMessage,\n} from \"naive-ui\";\nimport { css } fr"
  },
  {
    "path": "src/components/APIResponse/status_text.tsx",
    "chars": 674,
    "preview": "import { NGradientText } from \"naive-ui\";\nimport { defineComponent } from \"vue\";\nimport { getStatusText } from \"../../co"
  },
  {
    "path": "src/components/APISettingParams/index.tsx",
    "chars": 5885,
    "preview": "import {\n  defineComponent,\n  watch,\n  ref,\n  onBeforeUnmount,\n  PropType,\n  VNode,\n} from \"vue\";\nimport { css } from \"@"
  },
  {
    "path": "src/components/APISettingParams/req_params.tsx",
    "chars": 15345,
    "preview": "import {\n  NBadge,\n  NButton,\n  NButtonGroup,\n  NDropdown,\n  NIcon,\n  NTab,\n  NTabs,\n  useDialog,\n  useMessage,\n} from \""
  },
  {
    "path": "src/components/APISettingParams/uri.tsx",
    "chars": 7707,
    "preview": "import { defineComponent, PropType, ref } from \"vue\";\nimport { css } from \"@linaria/core\";\nimport {\n  NButton,\n  NInput,"
  },
  {
    "path": "src/components/APISettingTree/header.tsx",
    "chars": 7708,
    "preview": "// API栏目的顶部功能栏\nimport { defineComponent, inject, onBeforeUnmount, PropType } from \"vue\";\nimport { css } from \"@linaria/c"
  },
  {
    "path": "src/components/APISettingTree/index.tsx",
    "chars": 3598,
    "preview": "// API应用配置列表\nimport { css } from \"@linaria/core\";\nimport { defineComponent, provide, ref } from \"vue\";\nimport { useDialo"
  },
  {
    "path": "src/components/APISettingTree/item_dropdown.tsx",
    "chars": 10381,
    "preview": "// API功能下拉选项框\nimport {\n  AddOutline,\n  ChevronDownOutline,\n  CopyOutline,\n  CreateOutline,\n  DownloadOutline,\n  FolderOp"
  },
  {
    "path": "src/components/APISettingTree/items.tsx",
    "chars": 20098,
    "preview": "// API列表,实现拖动\nimport { defineComponent, ref, onBeforeUnmount } from \"vue\";\nimport { storeToRefs } from \"pinia\";\nimport {"
  },
  {
    "path": "src/components/ExColumn.tsx",
    "chars": 3054,
    "preview": "import { defineComponent, PropType, StyleValue } from \"vue\";\nimport { css } from \"@linaria/core\";\nimport { NDivider } fr"
  },
  {
    "path": "src/components/ExCookieEditor.tsx",
    "chars": 5528,
    "preview": "import { defineComponent, PropType, ref } from \"vue\";\nimport {\n  NForm,\n  NFormItem,\n  NInput,\n  NP,\n  NGrid,\n  NGi,\n  N"
  },
  {
    "path": "src/components/ExDeleteCheck.tsx",
    "chars": 1084,
    "preview": "import { defineComponent, ref, PropType } from \"vue\";\nimport { NButton, NIcon } from \"naive-ui\";\nimport { AlertCircleOut"
  },
  {
    "path": "src/components/ExDialog.tsx",
    "chars": 7313,
    "preview": "import { editor } from \"monaco-editor/esm/vs/editor/editor.api\";\nimport { DialogApiInjection } from \"naive-ui/es/dialog/"
  },
  {
    "path": "src/components/ExForm.tsx",
    "chars": 3171,
    "preview": "import { defineComponent, PropType, ref } from \"vue\";\nimport {\n  FormInst,\n  FormRules,\n  FormItemRule,\n  NForm,\n  NForm"
  },
  {
    "path": "src/components/ExKeyValue.tsx",
    "chars": 7300,
    "preview": "import { ulid } from \"ulid\";\nimport { NButton, NGi, NGrid, NIcon, NInput, useMessage } from \"naive-ui\";\nimport { defineC"
  },
  {
    "path": "src/components/ExLoading.tsx",
    "chars": 2641,
    "preview": "import { defineComponent, PropType, StyleValue } from \"vue\";\nimport { css } from \"@linaria/core\";\n\nimport { i18nCommon }"
  },
  {
    "path": "src/components/ExPreview.tsx",
    "chars": 967,
    "preview": "import { defineComponent } from \"vue\";\nimport { css } from \"@linaria/core\";\n\nconst wrapperClass = css`\n  padding: 15px;\n"
  },
  {
    "path": "src/components/ExTimer.tsx",
    "chars": 614,
    "preview": "import { defineComponent, onBeforeUnmount, ref } from \"vue\";\n\nimport { css } from \"@linaria/core\";\n\nconst timerClass = c"
  },
  {
    "path": "src/constants/common.ts",
    "chars": 36,
    "preview": "export const appName = \"Cyber API\";\n"
  },
  {
    "path": "src/constants/handle_key.ts",
    "chars": 280,
    "preview": "export enum HandleKey {\n  Delete = \"delete\",\n  Modify = \"modify\",\n  Create = \"create\",\n  CreateFolder = \"createFolder\",\n"
  },
  {
    "path": "src/constants/provide.ts",
    "chars": 353,
    "preview": "export const addHTTPSettingKey = \"addHTTPSetting\";\n\nexport const addHTTPSettingDefaultValue = (folder: string) => {\n  co"
  },
  {
    "path": "src/constants/style.ts",
    "chars": 89,
    "preview": "export const mainHeaderHeight = 50;\nexport const padding = 10;\nexport const margin = 20;\n"
  },
  {
    "path": "src/env.d.ts",
    "chars": 284,
    "preview": "/// <reference types=\"vite/client\" />\n\ndeclare module \"*.vue\" {\n  import type { DefineComponent } from \"vue\";\n  // eslin"
  },
  {
    "path": "src/event.ts",
    "chars": 307,
    "preview": "import { appWindow } from \"@tauri-apps/api/window\";\nimport { hide } from \"@tauri-apps/api/app\";\nimport { isMacOS } from "
  },
  {
    "path": "src/helpers/curl.js",
    "chars": 3540,
    "preview": "import * as words from \"shellwords\";\nimport { forEach } from \"lodash-es\";\nimport { isJSON } from \"./util\";\n\nexport defau"
  },
  {
    "path": "src/helpers/editor.ts",
    "chars": 743,
    "preview": "import { editor } from \"monaco-editor/esm/vs/editor/editor.api\";\n\nexport function createEditor(params: {\n  isDark: boole"
  },
  {
    "path": "src/helpers/hot_key.ts",
    "chars": 914,
    "preview": "const metaKey = \"⌘\";\nconst shiftKey = \"⇧\";\n\nfunction match(hotKey: string, e: KeyboardEvent) {\n  let matched = true;\n  h"
  },
  {
    "path": "src/helpers/html.ts",
    "chars": 2963,
    "preview": "import { forEach } from \"lodash-es\";\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nfunctio"
  },
  {
    "path": "src/helpers/pinyin.ts",
    "chars": 14006,
    "preview": "function newPinYinMap() {\n  const data: { [key: string]: unknown } = {\n    a: \"阿啊呵腌嗄吖锕\",\n    e: \"额阿俄恶鹅遏鄂厄饿峨扼娥鳄哦蛾噩愕讹锷垩婀鹗萼"
  },
  {
    "path": "src/helpers/util.ts",
    "chars": 5763,
    "preview": "import { FormRules, MessageApi } from \"naive-ui\";\nimport dayjs from \"dayjs\";\nimport { get, has, isNil } from \"lodash-es\""
  },
  {
    "path": "src/i18n/en.ts",
    "chars": 6637,
    "preview": "export default {\n  collection: {\n    filterPlaceholder: \"filter keywords\",\n    newHTTPRequest: \"Create a HTTP request\",\n"
  },
  {
    "path": "src/i18n/index.ts",
    "chars": 1622,
    "preview": "import { createI18n } from \"vue-i18n\";\nimport { enUS, zhCN, ukUA } from \"naive-ui\";\n\nimport en from \"./en\";\nimport zh fr"
  },
  {
    "path": "src/i18n/uk.ts",
    "chars": 6801,
    "preview": "export default {\n  collection: {\n    filterPlaceholder: \"фільтрувати ключові слова\",\n    newHTTPRequest: \"Створити HTTP-"
  },
  {
    "path": "src/i18n/zh.ts",
    "chars": 4453,
    "preview": "export default {\n  collection: {\n    filterPlaceholder: \"关键字过滤\",\n    newHTTPRequest: \"新建HTTP请求\",\n    newFolder: \"新建目录\",\n"
  },
  {
    "path": "src/icons.ts",
    "chars": 9709,
    "preview": "export const openFolderIcon =\n  \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAIsUlEQVR4Xu2XTahkx"
  },
  {
    "path": "src/main.css",
    "chars": 1283,
    "preview": "body {\n  margin: 0;\n  min-height: 100vh;\n  /* overflow: hidden; */\n  font-size: 14px;\n  font-family: v-sans, system-ui, "
  },
  {
    "path": "src/main.ts",
    "chars": 1710,
    "preview": "import { createApp } from \"vue\";\nimport { create } from \"naive-ui\";\nimport { createPinia } from \"pinia\";\nimport { messag"
  },
  {
    "path": "src/root.tsx",
    "chars": 2090,
    "preview": "import { defineComponent, onBeforeMount, ref } from \"vue\";\nimport {\n  darkTheme,\n  NConfigProvider,\n  NDialogProvider,\n "
  },
  {
    "path": "src/router/index.ts",
    "chars": 1122,
    "preview": "import {\n  createRouter,\n  createWebHashHistory,\n  LocationQueryRaw,\n} from \"vue-router\";\n\nimport { updateAppLatestRoute"
  },
  {
    "path": "src/router/routes.ts",
    "chars": 500,
    "preview": "import { Component } from \"vue\";\n\nimport Dashboard from \"../views/Dashboard\";\nimport Collection from \"../views/Collectio"
  },
  {
    "path": "src/stores/api_collection.ts",
    "chars": 5010,
    "preview": "import dayjs from \"dayjs\";\nimport { uniq } from \"lodash-es\";\nimport { defineStore } from \"pinia\";\n\nimport {\n  APICollect"
  },
  {
    "path": "src/stores/api_folder.ts",
    "chars": 4850,
    "preview": "import dayjs from \"dayjs\";\nimport { defineStore } from \"pinia\";\nimport { compact, uniq } from \"lodash-es\";\nimport {\n  cr"
  },
  {
    "path": "src/stores/api_setting.ts",
    "chars": 4619,
    "preview": "import { defineStore } from \"pinia\";\nimport dayjs from \"dayjs\";\n\nimport {\n  APISetting,\n  listAPISetting,\n  createAPISet"
  },
  {
    "path": "src/stores/app.ts",
    "chars": 998,
    "preview": "import { defineStore } from \"pinia\";\nimport { getVersion, getTauriVersion } from \"@tauri-apps/api/app\";\nimport { arch, p"
  },
  {
    "path": "src/stores/cookie.ts",
    "chars": 1944,
    "preview": "import { sortBy } from \"lodash-es\";\nimport { defineStore } from \"pinia\";\n\nimport {\n  listCookie,\n  Cookie,\n  deleteCooki"
  },
  {
    "path": "src/stores/dialog.ts",
    "chars": 848,
    "preview": "import { defineStore } from \"pinia\";\n\nexport const useDialogStore = defineStore(\"dialogs\", {\n  state: () => {\n    return"
  },
  {
    "path": "src/stores/environment.ts",
    "chars": 257,
    "preview": "import { VariableCategory } from \"../commands/variable\";\nimport { newVariableStore } from \"./variable\";\nexport const ENV"
  },
  {
    "path": "src/stores/global_req_header.ts",
    "chars": 224,
    "preview": "import { VariableCategory } from \"../commands/variable\";\nimport { newVariableStore } from \"./variable\";\n\nexport const us"
  },
  {
    "path": "src/stores/header.ts",
    "chars": 625,
    "preview": "import { defineStore } from \"pinia\";\n\ninterface Breadcrumb {\n  route: string;\n  name: string;\n}\n\nexport const useHeaderS"
  },
  {
    "path": "src/stores/local.ts",
    "chars": 1722,
    "preview": "import localforage from \"localforage\";\n\nconst stores: Map<string, LocalForage> = new Map();\n\nfunction createNewStore(nam"
  },
  {
    "path": "src/stores/pin_request.ts",
    "chars": 1451,
    "preview": "import { defineStore } from \"pinia\";\nimport { getPinRequestStore } from \"./local\";\n\ninterface LatestRequest {\n  id: stri"
  },
  {
    "path": "src/stores/setting.ts",
    "chars": 5422,
    "preview": "import { defineStore } from \"pinia\";\nimport { appWindow } from \"@tauri-apps/api/window\";\n\nimport { setWindowSize } from "
  },
  {
    "path": "src/stores/variable.ts",
    "chars": 2678,
    "preview": "import dayjs from \"dayjs\";\nimport { sortBy } from \"lodash-es\";\nimport { defineStore } from \"pinia\";\nimport {\n  createVar"
  },
  {
    "path": "src/userWorker.ts",
    "chars": 1095,
    "preview": "import * as monaco from \"monaco-editor\";\nimport editorWorker from \"monaco-editor/esm/vs/editor/editor.worker?worker\";\nim"
  },
  {
    "path": "src/views/AppHeader.tsx",
    "chars": 12677,
    "preview": "import {\n  NBreadcrumb,\n  NBreadcrumbItem,\n  NDivider,\n  NIcon,\n  NTab,\n  NTabs,\n  NDropdown,\n  NSpace,\n  useMessage,\n  "
  },
  {
    "path": "src/views/AppSetting.tsx",
    "chars": 7489,
    "preview": "import { defineComponent, onBeforeUnmount } from \"vue\";\nimport {\n  NCard,\n  NDescriptions,\n  NDescriptionsItem,\n  NDivid"
  },
  {
    "path": "src/views/Collection.tsx",
    "chars": 7900,
    "preview": "import { useMessage } from \"naive-ui\";\nimport {\n  defineComponent,\n  onBeforeMount,\n  onBeforeUnmount,\n  ref,\n  watch,\n}"
  },
  {
    "path": "src/views/CookieSetting.tsx",
    "chars": 5343,
    "preview": "import { storeToRefs } from \"pinia\";\nimport { defineComponent, onBeforeMount, ref } from \"vue\";\nimport {\n  NCard,\n  useM"
  },
  {
    "path": "src/views/Dashboard.tsx",
    "chars": 10760,
    "preview": "import {\n  NButton,\n  NCard,\n  NDropdown,\n  NEllipsis,\n  NGi,\n  NGrid,\n  NH3,\n  NIcon,\n  NInput,\n  NText,\n  useDialog,\n "
  },
  {
    "path": "src/views/StoreSetting.tsx",
    "chars": 2550,
    "preview": "import { css } from \"@linaria/core\";\nimport { TrashOutline } from \"@vicons/ionicons5\";\nimport {\n  DataTableColumns,\n  NB"
  },
  {
    "path": "src/views/VariableSetting.tsx",
    "chars": 5282,
    "preview": "import { NCard, NP, useMessage } from \"naive-ui\";\nimport { defineComponent, onBeforeMount } from \"vue\";\nimport { css } f"
  },
  {
    "path": "src-tauri/.gitignore",
    "chars": 73,
    "preview": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n"
  },
  {
    "path": "src-tauri/Cargo.toml",
    "chars": 2069,
    "preview": "[package]\nname = \"cyberapi\"\nversion = \"0.1.21\"\ndescription = \"API tool based on tauri, it is smaller and faster.\"\nauthor"
  },
  {
    "path": "src-tauri/build.rs",
    "chars": 39,
    "preview": "fn main() {\n    tauri_build::build()\n}\n"
  },
  {
    "path": "src-tauri/src/commands.rs",
    "chars": 5371,
    "preview": "use crate::error::CyberAPIError;\nuse crate::schemas::{self, APICollection, APIFolder, APISetting, Variable};\nuse crate::"
  },
  {
    "path": "src-tauri/src/cookies.rs",
    "chars": 4039,
    "preview": "use cookie_store::CookieStore;\nuse once_cell::sync::OnceCell;\nuse serde::{Deserialize, Serialize};\nuse std::{\n    fs::Fi"
  },
  {
    "path": "src-tauri/src/entities/api_collections.rs",
    "chars": 609,
    "preview": "//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2\n\nuse sea_orm::entity::prelude::*;\nuse serde::{Deserialize, Seri"
  },
  {
    "path": "src-tauri/src/entities/api_folders.rs",
    "chars": 630,
    "preview": "//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2\n\nuse sea_orm::entity::prelude::*;\nuse serde::{Deserialize, Seri"
  },
  {
    "path": "src-tauri/src/entities/api_settings.rs",
    "chars": 664,
    "preview": "//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2\n\nuse sea_orm::entity::prelude::*;\nuse serde::{Deserialize, Seri"
  },
  {
    "path": "src-tauri/src/entities/mod.rs",
    "chars": 181,
    "preview": "//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2\n\npub mod prelude;\n\npub mod api_collections;\npub mod api_folders"
  },
  {
    "path": "src-tauri/src/entities/prelude.rs",
    "chars": 310,
    "preview": "//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2\n\npub use super::api_collections::Entity as ApiCollections;\npub "
  },
  {
    "path": "src-tauri/src/entities/variables.rs",
    "chars": 684,
    "preview": "//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2\n\nuse sea_orm::entity::prelude::*;\nuse serde::{Deserialize, Seri"
  },
  {
    "path": "src-tauri/src/entities/versions.rs",
    "chars": 560,
    "preview": "//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2\n\nuse sea_orm::entity::prelude::*;\nuse serde::{Deserialize, Seri"
  },
  {
    "path": "src-tauri/src/error.rs",
    "chars": 3615,
    "preview": "use serde::Serialize;\nuse zip::result::ZipError;\n\n#[derive(Debug, Clone, Serialize)]\npub struct CyberAPIError {\n    mess"
  },
  {
    "path": "src-tauri/src/http_request.rs",
    "chars": 19246,
    "preview": "use crate::cookies;\nuse crate::error::CyberAPIError;\nuse base64::{engine::general_purpose, Engine as _};\nuse hyper::{\n  "
  },
  {
    "path": "src-tauri/src/main.rs",
    "chars": 2402,
    "preview": "#![cfg_attr(\n    all(not(debug_assertions), target_os = \"windows\"),\n    windows_subsystem = \"windows\"\n)]\n\nuse tracing_su"
  },
  {
    "path": "src-tauri/src/schemas/api_collection.rs",
    "chars": 3597,
    "preview": "use crate::{\n    entities::{api_collections, prelude::*},\n    error::CyberAPIError,\n};\nuse chrono::Utc;\nuse sea_orm::{Ac"
  },
  {
    "path": "src-tauri/src/schemas/api_folder.rs",
    "chars": 6072,
    "preview": "use crate::{\n    entities::{api_folders, prelude::*},\n    error::CyberAPIError,\n};\nuse chrono::Utc;\nuse sea_orm::{Active"
  },
  {
    "path": "src-tauri/src/schemas/api_setting.rs",
    "chars": 4285,
    "preview": "use crate::{\n    entities::{api_settings, prelude::*},\n    error::CyberAPIError,\n};\nuse chrono::Utc;\nuse sea_orm::{Activ"
  },
  {
    "path": "src-tauri/src/schemas/database.rs",
    "chars": 4609,
    "preview": "use crate::error::CyberAPIError;\nuse chrono::Local;\nuse sea_orm::{ConnectOptions, ConnectionTrait, Database, DatabaseCon"
  },
  {
    "path": "src-tauri/src/schemas/mod.rs",
    "chars": 808,
    "preview": "mod api_collection;\nmod api_folder;\nmod api_setting;\nmod database;\nmod variable;\nmod version;\n\npub use database::{export"
  },
  {
    "path": "src-tauri/src/schemas/variable.rs",
    "chars": 4145,
    "preview": "use crate::{\n    entities::{prelude::*, variables},\n    error::CyberAPIError,\n};\nuse chrono::Utc;\nuse sea_orm::{ActiveMo"
  },
  {
    "path": "src-tauri/src/schemas/version.rs",
    "chars": 2062,
    "preview": "use crate::entities::{prelude::*, versions};\nuse chrono::Utc;\nuse sea_orm::{ActiveModelTrait, DbErr, EntityTrait, QueryO"
  },
  {
    "path": "src-tauri/src/util.rs",
    "chars": 223,
    "preview": "use once_cell::sync::OnceCell;\n\nstatic APP_DIR: OnceCell<String> = OnceCell::new();\npub fn set_app_dir(dir: String) {\n  "
  },
  {
    "path": "src-tauri/tauri.conf.json",
    "chars": 2720,
    "preview": "{\n  \"$schema\": \"../node_modules/@tauri-apps/cli/schema.json\",\n  \"build\": {\n    \"beforeBuildCommand\": \"npm run build\",\n  "
  },
  {
    "path": "tsconfig.json",
    "chars": 491,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"esnext\",\n    \"modul"
  },
  {
    "path": "tsconfig.node.json",
    "chars": 142,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\"\n  },\n  \"include\":"
  },
  {
    "path": "vite.config.ts",
    "chars": 1267,
    "preview": "import { resolve } from 'path'\nimport { defineConfig } from \"vite\";\nimport { visualizer } from \"rollup-plugin-visualizer"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the vicanso/cyberapi GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 123 files (426.1 KB), approximately 129.0k tokens, and a symbol index with 492 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!