Full Code of putyy/res-downloader for AI

master 046cbb2b83a6 cached
111 files
5.2 MB
1.4M tokens
465 symbols
1 requests
Download .txt
Showing preview only (5,519K chars total). Download the full file or copy to clipboard to get everything.
Repository: putyy/res-downloader
Branch: master
Commit: 046cbb2b83a6
Files: 111
Total size: 5.2 MB

Directory structure:
gitextract_tj3kx0fc/

├── .github/
│   └── ISSUE_TEMPLATE/
│       ├── bug_report.yml
│       ├── config.yml
│       └── feature_request.yml
├── .gitignore
├── LICENSE
├── README-EN.md
├── README.md
├── build/
│   ├── README.md
│   ├── darwin/
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── linux/
│   │   ├── .gitignore
│   │   ├── AppImage/
│   │   │   ├── res-downloader.desktop
│   │   │   └── usr/
│   │   │       ├── bin/
│   │   │       │   └── .gitkeep
│   │   │       └── share/
│   │   │           └── applications/
│   │   │               └── res-downloader.desktop
│   │   ├── Arch/
│   │   │   └── res-downloader.desktop
│   │   ├── Debian/
│   │   │   ├── DEBIAN/
│   │   │   │   └── .control
│   │   │   └── usr/
│   │   │       ├── local/
│   │   │       │   └── bin/
│   │   │       │       └── .gitkeep
│   │   │       └── share/
│   │   │           └── applications/
│   │   │               └── res-downloader.desktop
│   │   └── dockerfile
│   └── windows/
│       ├── info.json
│       ├── installer/
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── core/
│   ├── aes.go
│   ├── app.go
│   ├── bind.go
│   ├── config.go
│   ├── downloader.go
│   ├── http.go
│   ├── logger.go
│   ├── middleware.go
│   ├── plugins/
│   │   ├── plugin.default.go
│   │   └── plugin.qq.com.go
│   ├── proxy.go
│   ├── resource.go
│   ├── rule.go
│   ├── shared/
│   │   ├── base.go
│   │   ├── const.go
│   │   ├── plugin.go
│   │   └── utils.go
│   ├── storage.go
│   ├── system.go
│   ├── system_darwin.go
│   ├── system_linux.go
│   ├── system_windows.go
│   └── utils.go
├── docs/
│   ├── .nojekyll
│   ├── _coverpage.md
│   ├── _navbar.md
│   ├── _sidebar.md
│   ├── examples.md
│   ├── getting-started.md
│   ├── index.html
│   ├── installation.md
│   ├── more.md
│   ├── readme.md
│   └── troubleshooting.md
├── frontend/
│   ├── READ-THIS.md
│   ├── README.md
│   ├── auto-imports.d.ts
│   ├── components.d.ts
│   ├── env.d.ts
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── postcss.config.js
│   ├── src/
│   │   ├── App.vue
│   │   ├── api/
│   │   │   ├── app.ts
│   │   │   └── request.ts
│   │   ├── assets/
│   │   │   ├── css/
│   │   │   │   ├── base.css
│   │   │   │   └── main.css
│   │   │   └── js/
│   │   │       └── decrypt.js
│   │   ├── components/
│   │   │   ├── Action.vue
│   │   │   ├── ActionDesc.vue
│   │   │   ├── Footer.vue
│   │   │   ├── ImportJson.vue
│   │   │   ├── NaiveProvider.vue
│   │   │   ├── Password.vue
│   │   │   ├── Preview.vue
│   │   │   ├── Screen.vue
│   │   │   ├── ShowLoading.vue
│   │   │   ├── ShowOrEdit.vue
│   │   │   └── layout/
│   │   │       ├── Index.vue
│   │   │       └── Sider.vue
│   │   ├── func.ts
│   │   ├── i18n.ts
│   │   ├── locales/
│   │   │   ├── en.json
│   │   │   └── zh.json
│   │   ├── main.ts
│   │   ├── router/
│   │   │   └── index.ts
│   │   ├── stores/
│   │   │   ├── event.ts
│   │   │   └── index.ts
│   │   ├── types/
│   │   │   ├── app.d.ts
│   │   │   └── global.d.ts
│   │   └── views/
│   │       ├── index.vue
│   │       └── setting.vue
│   ├── tailwind.config.js
│   ├── tsconfig.app.json
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs/
│       ├── go/
│       │   ├── core/
│       │   │   ├── Bind.d.ts
│       │   │   └── Bind.js
│       │   └── models.ts
│       └── runtime/
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── main.go
└── wails.json

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

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: "Bug Report \\ 问题反馈"
description: "Create a report to help us improve \\ 帮助改进"
labels: ["Bug"]

body:
  - type: input
    id: title
    attributes:
      label: "Title \\ 标题"
      description: "A brief summary of the bug. \\ 对于该错误的简要总结。"
      placeholder: "Enter the bug title here. \\ 在此输入错误标题。"
    validations:
      required: true

  - type: textarea
    id: reproduce
    attributes:
      label: "To Reproduce \\ 操作流程"
      description: "Steps to reproduce the behaviour. \\ 重现该行为的步骤。"
      placeholder: |
        1. Go to '...' \ 进入 '...'
        2. Click on '....' \ 点击 '....'
    validations:
      required: true

  - type: textarea
    id: expected-behaviour
    attributes:
      label: "Expected Behaviour \\ 预期结果"
      description: "A clear and concise description of what you expected to happen. \\ 对您期望发生的事情的清晰简明描述。"
      placeholder: "A clear and concise description of what you expected to happen. \\ 对您期望发生的事情的清晰简明描述。"
    validations:
      required: true

  - type: textarea
    id: software-version
    attributes:
      label: "Software Version \\ 软件版本"
      description: "Please specify the version of the software you are using. \\ 请指定您使用的软件版本。"
      placeholder: "Enter the software version here. \\ 在此输入软件版本。"
    validations:
      required: true


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true

================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: "Feature Request \\ 功能建议"
description: "Suggest an idea for this project \\ 为这个项目提出一个新想法"
labels: ["Enhancement"]

body:
  - type: input
    id: title
    attributes:
      label: "Title \\ 标题"
      description: "A brief summary of your feature request. \\ 对您功能建议的简要总结。"
      placeholder: "Enter the feature title here. \\ 在此输入功能标题。 "
    validations:
      required: true

  - type: textarea
    id: feature-suggestion
    attributes:
      label: "Feature Suggestion \\ 功能建议"
      description: "A clear and concise description of the feature you would like to suggest. \\ 对您想要建议的功能的清晰简明描述。"
      placeholder: "Describe your feature suggestion here. \\ 在此描述您的功能建议。"
    validations:
      required: true

  - type: textarea
    id: proposed-solution
    attributes:
      label: "Proposed Solution \\ 你的方案"
      description: "A clear and concise description of your proposed solution. \\ 对您提议的解决方案的清晰简明描述。"
      placeholder: "Describe your proposed solution here. \\ 在此描述您的提议方案。"
    validations:
      required: true


================================================
FILE: .gitignore
================================================
.idea
build/bin
node_modules
frontend/dist
.DS_Store

================================================
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: README-EN.md
================================================
<div align="center">

<a href="https://github.com/putyy/res-downloader"><img src="build/appicon.png" width="120"/></a>
<h1>res-downloader</h1>
<h4>📖 English | <a href="https://github.com/putyy/res-downloader/blob/master/README.md">中文</a></h4>

[![GitHub stars](https://img.shields.io/github/stars/putyy/res-downloader)](https://github.com/putyy/res-downloader/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/putyy/res-downloader)](https://github.com/putyy/res-downloader/fork)
[![GitHub release](https://img.shields.io/github/release/putyy/res-downloader)](https://github.com/putyy/res-downloader/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/putyy/res-downloader/total)
[![License](https://img.shields.io/github/license/putyy/res-downloader)](https://github.com/putyy/res-downloader/blob/master/LICENSE)

</div>

---

### 🎉 Aixiang Resource Downloader

> A cross-platform resource downloader built with Go + [Wails](https://github.com/wailsapp/wails).  
Clean UI, easy to use, and supports a wide range of resource sniffing and downloading.

## ✨ Features

- 🚀 **User-Friendly**: Simple operation with an intuitive and beautiful UI
- 🖥️ **Cross-Platform**: Available on Windows / macOS / Linux
- 🌐 **Supports Multiple Resource Types**: Video / Audio / Images / m3u8 / Live streams, and more
- 📱 **Wide Platform Compatibility**: Works with WeChat Channels, Mini Programs, Douyin, Kuaishou, Xiaohongshu, KuGou Music, QQ Music, and more
- 🌍 **Proxy Capture**: Built-in proxy allows fetching resources behind network restrictions

## 📚 Docs & Versions

- 📘 [Online Documentation (Chinese)](https://res.putyy.com/)
- 🧩 [Mini Version Ui Display using default browser](https://github.com/putyy/res-downloader) | [Old Electron Version Support Win7](https://github.com/putyy/res-downloader/tree/old)
- 💬 [Join the User Group (Chinese)](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)
  > *If full, you can add WeChat `AmorousWorld` with a note “github”*

## 🧩 Download Links

- 🆕 [Download from GitHub](https://github.com/putyy/res-downloader/releases)
- 🆕 [Download via Lanzou Cloud (Password: 9vs5)](https://wwjv.lanzoum.com/b04wgtfyb)
- ⚠️ *Windows 7 users: Please use version `2.3.0`*


## 🖼️ Preview

![Preview](docs/images/show.webp)

## 🚀 How to Use

> Follow these steps to use the software correctly:

1. During installation, be sure to **allow certificate installation** and **grant network access**
2. Launch the software → Click **"Start Proxy"** at the top left
3. Choose the resource types to capture (default is all)
4. Open the target content externally (WeChat, Mini App, Browser, etc.)
5. Return to the homepage to view the captured resource list

---

## ❓ FAQ

### 📺 m3u8 Video Resources

- Online Preview: [m3u8play](https://m3u8play.com/)
- Download Tool: [m3u8-down](https://m3u8-down.gowas.cn/)

### 📡 Live Stream Resources

- We recommend [OBS](https://obsproject.com/) for recording (search for setup tutorials)

### 🐢 Slow Downloads or Large File Failures?

- Recommended download managers:
    - [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)
    - [Motrix](https://motrix.app/download)
- For WeChat videos, click `Decrypt Video` after download

### 🧩 Unable to Intercept Resources?

- Check your system proxy settings:  
  Address: 127.0.0.1  
  Port: 8899

### 🌐 Can't Access Internet After Closing the App?

- Manually disable the system proxy settings

### 🧠 More Questions?

- [GitHub Issues](https://github.com/putyy/res-downloader/issues)
- [Aixiang Forum Thread (Chinese)](https://s.gowas.cn/d/4089)

## 💡 Principles & Motivation

This tool captures traffic via a local proxy and filters useful resources.  
Its working principle is similar to tools like Fiddler, Charles, or browser DevTools, but with a more user-friendly display and enhanced filtering, making it suitable for everyday users with minimal tech background.

---

## ⚠️ Disclaimer

> This software is for educational and research purposes only.  
Commercial or illegal use is strictly prohibited.  
The author is not responsible for any consequences arising from misuse.


================================================
FILE: README.md
================================================
<div align="center">

<a href="https://github.com/putyy/res-downloader"><img src="build/appicon.png" width="120"/></a>
<h1>res-downloader</h1>
<h4>📖 中文 | <a href="https://github.com/putyy/res-downloader/blob/master/README-EN.md">English</a></h4>

[![GitHub stars](https://img.shields.io/github/stars/putyy/res-downloader)](https://github.com/putyy/res-downloader/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/putyy/res-downloader)](https://github.com/putyy/res-downloader/fork)
[![GitHub release](https://img.shields.io/github/release/putyy/res-downloader)](https://github.com/putyy/res-downloader/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/putyy/res-downloader/total)
[![License](https://img.shields.io/github/license/putyy/res-downloader)](https://github.com/putyy/res-downloader/blob/master/LICENSE)

</div>

---

### 🎉 爱享素材下载器

> 一款基于 Go + [Wails](https://github.com/wailsapp/wails) 的跨平台资源下载工具,简洁易用,支持多种资源嗅探与下载。

## ✨ 功能特色

- 🚀 **简单易用**:操作简单,界面清晰美观
- 🖥️ **多平台支持**:Windows / macOS / Linux
- 🌐 **多资源类型支持**:视频 / 音频 / 图片 / m3u8 / 直播流等
- 📱 **平台兼容广泛**:支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、QQ音乐等
- 🌍 **代理抓包**:支持设置代理获取受限网络下的资源

## 📚 文档 & 版本

- 📘 [在线文档](https://res.putyy.com/)
- 💬 [加入交流群](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)
- 🧩 [最新版](https://github.com/putyy/res-downloader/releases) | [Mini版 使用默认浏览器展示UI](https://github.com/putyy/resd-mini) | [Electron旧版 支持Win7](https://github.com/putyy/res-downloader/tree/old)
  > *群满时可加微信 `AmorousWorld`,请备注“github”*

## 🧩 下载地址

- 🆕 [GitHub 下载](https://github.com/putyy/res-downloader/releases)
- 🆕 [蓝奏云下载(密码:9vs5)](https://wwjv.lanzoum.com/b04wgtfyb)
- ⚠️ *Win7 用户请下载 `2.3.0` 版本*


## 🖼️ 预览

![预览](docs/images/show.webp)
--- 

## 🚀 使用方法

> 请按以下步骤操作以正确使用软件:

1. 安装时务必 **允许安装证书文件** 并 **允许网络访问**
2. 打开软件 → 首页左上角点击 **“启动代理”**
3. 选择要获取的资源类型(默认全部)
4. 在外部打开资源页面(如视频号、小程序、网页等)
5. 返回软件首页,即可看到资源列表

## ❓ 常见问题

### 📺 m3u8 视频资源

- 在线预览:[m3u8play](https://m3u8play.com/)
- 视频下载:[m3u8-down](https://m3u8-down.gowas.cn/)

### 📡 直播流资源

- 推荐使用 [OBS](https://obsproject.com/) 进行录制(教程请百度)

### 🐢 下载慢、大文件失败?

- 推荐工具:
  - [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)
  - [Motrix](https://motrix.app/download)
- 视频号资源下载后可在操作项点击 `视频解密(视频号)`

### 🧩 软件无法拦截资源?

- 检查是否正确设置系统代理:  
  地址:127.0.0.1
  端口:8899

### 🌐 关闭软件后无法上网?

- 手动关闭系统代理设置

### 🧠 更多问题

- [GitHub Issues](https://github.com/putyy/res-downloader/issues)
- [爱享论坛讨论帖](https://s.gowas.cn/d/4089)

## 💡 实现原理 & 初衷

本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。

---

## ⚠️ 免责声明

> 本软件仅供学习与研究用途,禁止用于任何商业或违法用途。  
如因此产生的任何法律责任,概与作者无关!


================================================
FILE: build/README.md
================================================
## Mac
```bash
wails build -platform "darwin/universal"
create-dmg 'build/bin/res-downloader.app' --overwrite ./build/bin
mv -f "build/bin/res-downloader $(jq -r '.info.productVersion' wails.json).dmg" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_mac.dmg"
```

## Windows
```bash
wails build -f -nsis -platform "windows/amd64" -webview2 Embed -skipbindings && mv -f "build/bin/res-downloader-amd64-installer.exe" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_win_amd64.exe"
wails build -f -nsis -platform "windows/arm64" -webview2 Embed -skipbindings && mv -f "build/bin/res-downloader-arm64-installer.exe" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_win_arm64.exe"
```

## Linux

###  docker方式
> x86_64
```bash
docker build --network host -f build/linux/dockerfile -t res-downloader-amd-linux .
docker run -it --name res-downloader-amd-build --network host --privileged -v ./:/www/res-downloader res-downloader-amd-linux /bin/bash
# 容器内
cd /www/res-downloader
wails build -platform "linux/amd64" -s -skipbindings

# 打包debian
cp build/bin/res-downloader build/linux/Debian/usr/local/bin/
echo "$(cat build/linux/Debian/DEBIAN/.control | sed -e "s/{{Version}}/$(jq -r '.info.productVersion' wails.json)/g" -e "s/{{Architecture}}/amd64/g")" > build/linux/Debian/DEBIAN/control
dpkg-deb --build ./build/linux/Debian build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64.deb

# 打包AppImage
cp build/bin/res-downloader build/linux/AppImage/usr/bin/

# 复制WebKit相关文件
pushd build/linux/AppImage

for f in WebKitNetworkProcess WebKitWebProcess libwebkit2gtkinjectedbundle.so; do
    path=$(find /usr/lib* -name "$f" 2>/dev/null | head -n 1)
    if [ -n "$path" ]; then
        mkdir -p ./$(dirname "$path")
        cp --parents "$path" .
    else
        echo "⚠️ $f not found, you may need to install libwebkit2gtk"
    fi
done

popd

# 下载appimagetool
wget -O ./build/bin/appimagetool-x86_64.AppImage https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage 
chmod +x ./build/bin/appimagetool-x86_64.AppImage
./build/bin/appimagetool-x86_64.AppImage build/linux/AppImage build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64.AppImage

mv -f build/bin/res-downloader build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64
```

> arm64
```bash
# arm
docker build --platform linux/arm64 --network host -f build/linux/dockerfile -t res-downloader-arm-linux .
docker run --platform linux/arm64 -it --name res-downloader-arm-build --network host --privileged -v ./:/www/res-downloader res-downloader-arm-linux /bin/bash
# 容器内
cd /www/res-downloader
wails build -platform "linux/arm64" -s -skipbindings

# 打包debian
cp build/bin/res-downloader build/linux/Debian/usr/local/bin/
echo "$(cat build/linux/Debian/DEBIAN/.control | sed -e "s/{{Version}}/$(jq -r '.info.productVersion' wails.json)/g" -e "s/{{Architecture}}/arm64/g")" > build/linux/Debian/DEBIAN/control
dpkg-deb --build ./build/linux/Debian build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_arm64.deb

mv -f build/bin/res-downloader build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_arm64
```

### Arch Linux 

[![Packaging status](https://repology.org/badge/vertical-allrepos/res-downloader.svg)](https://repology.org/project/res-downloader/versions)

```bash
yay -Syu res-downloader 
```
### Linux 本地编译

```bash
git clone https://github.com/putyy/res-downloader.git
cd res-downloader
# -- GO Proxy --
# 如果国内编译时 go 下载慢或报错,可以设置 go 国内代理加速,否则不用设置
export GO111MODULE=on
export GOPROXY=https://goproxy.cn,direct
# -- Go Proxy --
wails build
cd build
sudo install -Dvm755 bin/res-downloader -t /usr/bin
sudo install -Dvm644 appicon.png /usr/share/icons/hicolor/512x512/apps/res-downloader.png
sudo install -Dvm644 build/linux/Arch/res-downloader.desktop /usr/share/applications/res-downloader.desktop 
```


================================================
FILE: build/darwin/Info.dev.plist
================================================
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleName</key>
        <string>{{.Info.ProductName}}</string>
        <key>CFBundleExecutable</key>
        <string>{{.Name}}</string>
        <key>CFBundleIdentifier</key>
        <string>com.wails.{{.Name}}</string>
        <key>CFBundleVersion</key>
        <string>{{.Info.ProductVersion}}</string>
        <key>CFBundleGetInfoString</key>
        <string>{{.Info.Comments}}</string>
        <key>CFBundleShortVersionString</key>
        <string>{{.Info.ProductVersion}}</string>
        <key>CFBundleIconFile</key>
        <string>iconfile</string>
        <key>LSMinimumSystemVersion</key>
        <string>10.13.0</string>
        <key>NSHighResolutionCapable</key>
        <string>true</string>
        <key>NSHumanReadableCopyright</key>
        <string>{{.Info.Copyright}}</string>
        {{if .Info.FileAssociations}}
        <key>CFBundleDocumentTypes</key>
        <array>
          {{range .Info.FileAssociations}}
          <dict>
            <key>CFBundleTypeExtensions</key>
            <array>
              <string>{{.Ext}}</string>
            </array>
            <key>CFBundleTypeName</key>
            <string>{{.Name}}</string>
            <key>CFBundleTypeRole</key>
            <string>{{.Role}}</string>
            <key>CFBundleTypeIconFile</key>
            <string>{{.IconName}}</string>
          </dict>
          {{end}}
        </array>
        {{end}}
        {{if .Info.Protocols}}
        <key>CFBundleURLTypes</key>
        <array>
          {{range .Info.Protocols}}
            <dict>
                <key>CFBundleURLName</key>
                <string>com.wails.{{.Scheme}}</string>
                <key>CFBundleURLSchemes</key>
                <array>
                    <string>{{.Scheme}}</string>
                </array>
                <key>CFBundleTypeRole</key>
                <string>{{.Role}}</string>
            </dict>
          {{end}}
        </array>
        {{end}}
        <key>NSAppTransportSecurity</key>
        <dict>
            <key>NSAllowsLocalNetworking</key>
            <true/>
        </dict>
    </dict>
</plist>


================================================
FILE: build/darwin/Info.plist
================================================
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleName</key>
        <string>{{.Info.ProductName}}</string>
        <key>CFBundleExecutable</key>
        <string>{{.Name}}</string>
        <key>CFBundleIdentifier</key>
        <string>com.wails.{{.Name}}</string>
        <key>CFBundleVersion</key>
        <string>{{.Info.ProductVersion}}</string>
        <key>CFBundleGetInfoString</key>
        <string>{{.Info.Comments}}</string>
        <key>CFBundleShortVersionString</key>
        <string>{{.Info.ProductVersion}}</string>
        <key>CFBundleIconFile</key>
        <string>iconfile</string>
        <key>LSMinimumSystemVersion</key>
        <string>10.13.0</string>
        <key>NSHighResolutionCapable</key>
        <string>true</string>
        <key>NSHumanReadableCopyright</key>
        <string>{{.Info.Copyright}}</string>
        {{if .Info.FileAssociations}}
        <key>CFBundleDocumentTypes</key>
        <array>
          {{range .Info.FileAssociations}}
          <dict>
            <key>CFBundleTypeExtensions</key>
            <array>
              <string>{{.Ext}}</string>
            </array>
            <key>CFBundleTypeName</key>
            <string>{{.Name}}</string>
            <key>CFBundleTypeRole</key>
            <string>{{.Role}}</string>
            <key>CFBundleTypeIconFile</key>
            <string>{{.IconName}}</string>
          </dict>
          {{end}}
        </array>
        {{end}}
        {{if .Info.Protocols}}
        <key>CFBundleURLTypes</key>
        <array>
          {{range .Info.Protocols}}
            <dict>
                <key>CFBundleURLName</key>
                <string>com.wails.{{.Scheme}}</string>
                <key>CFBundleURLSchemes</key>
                <array>
                    <string>{{.Scheme}}</string>
                </array>
                <key>CFBundleTypeRole</key>
                <string>{{.Role}}</string>
            </dict>
          {{end}}
        </array>
        {{end}}
    </dict>
</plist>


================================================
FILE: build/linux/.gitignore
================================================
!.gitkeep
debian/usr/local/bin/*
debian/DEBIAN/control
AppImage/usr/bin/*
AppImage/usr/lib/*


================================================
FILE: build/linux/AppImage/res-downloader.desktop
================================================
[Desktop Entry]
Type=Application
Name=res-downloader
Comment=This is a high-value and high-performance and diverse resource downloader called res-downloader
Exec=/usr/bin/res-downloader
Icon=/usr/share/icons/hicolor/256x256/apps/res-downloader
Terminal=false
Categories=Utility;

================================================
FILE: build/linux/AppImage/usr/bin/.gitkeep
================================================


================================================
FILE: build/linux/AppImage/usr/share/applications/res-downloader.desktop
================================================
[Desktop Entry]
Type=Application
Name=res-downloader
Comment=This is a high-value and high-performance and diverse resource downloader called res-downloader
Exec=/usr/bin/res-downloader
Icon=/usr/share/icons/hicolor/256x256/apps/res-downloader
Terminal=false
Categories=Utility;

================================================
FILE: build/linux/Arch/res-downloader.desktop
================================================
[Desktop Entry]
Type=Application
Name=res-downloader
Comment=This is a high-value and high-performance and diverse resource downloader called res-downloader
Exec=res-downloader
Icon=res-downloader.png
Terminal=false
Categories=Utility;


================================================
FILE: build/linux/Debian/DEBIAN/.control
================================================
Package: res-downloader
Version: {{Version}}
Section: utils
Priority: optional
Architecture: {{Architecture}}
Depends: libwebkit2gtk-4.0-37
Maintainer: putyy@qq.com
Homepage: https://github.com/putyy/res-downloader
Description: This is a high-value and high-performance and diverse resource downloader called res-downloader


================================================
FILE: build/linux/Debian/usr/local/bin/.gitkeep
================================================


================================================
FILE: build/linux/Debian/usr/share/applications/res-downloader.desktop
================================================
[Desktop Entry]
Type=Application
Name=res-downloader
Comment=This is a high-value and high-performance and diverse resource downloader called res-downloader
Exec=/usr/local/bin/res-downloader
Icon=/usr/share/icons/hicolor/256x256/apps/res-downloader.png
Terminal=false
Categories=Utility;

================================================
FILE: build/linux/dockerfile
================================================
FROM golang:1.24.2-bookworm

WORKDIR /

RUN apt-get update && \
    apt-get install -y --fix-missing \
    build-essential \
    git \
    jq \
    kmod \
    fuse \
    libgtk-3-dev \
    libwebkit2gtk-4.0-dev \
    nsis \
    wget \
    curl \
    gnupg2 \
    lsb-release \
    libfuse-dev \
    libfuse2 \
    file \
    && rm -rf /var/lib/apt/lists/*

RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y nodejs

RUN go install github.com/wailsapp/wails/v2/cmd/wails@latest

RUN wails doctor


================================================
FILE: build/windows/info.json
================================================
{
	"fixed": {
		"file_version": "{{.Info.ProductVersion}}"
	},
	"info": {
		"0000": {
			"ProductVersion": "{{.Info.ProductVersion}}",
			"CompanyName": "{{.Info.CompanyName}}",
			"FileDescription": "{{.Info.ProductName}}",
			"LegalCopyright": "{{.Info.Copyright}}",
			"ProductName": "{{.Info.ProductName}}",
			"Comments": "{{.Info.Comments}}"
		}
	}
}

================================================
FILE: build/windows/installer/project.nsi
================================================
Unicode true

####
## Please note: Template replacements don't work in this file. They are provided with default defines like
## mentioned underneath.
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
## from outside of Wails for debugging and development of the installer.
##
## For development first make a wails nsis build to populate the "wails_tools.nsh":
## > wails build --target windows/amd64 --nsis
## Then you can call makensis on this file with specifying the path to your binary:
## For a AMD64 only installer:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
## For a ARM64 only installer:
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
## For a installer with both architectures:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
####
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
####
## !define INFO_PROJECTNAME    "MyProject" # Default "{{.Name}}"
## !define INFO_COMPANYNAME    "MyCompany" # Default "{{.Info.CompanyName}}"
## !define INFO_PRODUCTNAME    "MyProduct" # Default "{{.Info.ProductName}}"
## !define INFO_PRODUCTVERSION "1.0.0"     # Default "{{.Info.ProductVersion}}"
## !define INFO_COPYRIGHT      "Copyright" # Default "{{.Info.Copyright}}"
###
## !define PRODUCT_EXECUTABLE  "Application.exe"      # Default "${INFO_PROJECTNAME}.exe"
## !define UNINST_KEY_NAME     "UninstKeyInRegistry"  # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
####
## !define REQUEST_EXECUTION_LEVEL "admin"            # Default "admin"  see also https://nsis.sourceforge.io/Docs/Chapter4.html
####
## Include the wails tools
####
!include "wails_tools.nsh"

# The version information for this two must consist of 4 parts
VIProductVersion "${INFO_PRODUCTVERSION}.0"
VIFileVersion    "${INFO_PRODUCTVERSION}.0"

VIAddVersionKey "CompanyName"     "${INFO_COMPANYNAME}"
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
VIAddVersionKey "ProductVersion"  "${INFO_PRODUCTVERSION}"
VIAddVersionKey "FileVersion"     "${INFO_PRODUCTVERSION}"
VIAddVersionKey "LegalCopyright"  "${INFO_COPYRIGHT}"
VIAddVersionKey "ProductName"     "${INFO_PRODUCTNAME}"

# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
ManifestDPIAware true

!include "MUI.nsh"

!define MUI_ICON "..\icon.ico"
!define MUI_UNICON "..\icon.ico"
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.

!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
!insertmacro MUI_PAGE_INSTFILES # Installing page.
!insertmacro MUI_PAGE_FINISH # Finished installation page.

!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page

!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer

## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
#!uninstfinalize 'signtool --file "%1"'
#!finalize 'signtool --file "%1"'

Name "${INFO_PRODUCTNAME}"
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
ShowInstDetails show # This will always show the installation details.

Function .onInit
   !insertmacro wails.checkArchitecture
FunctionEnd

Section
    !insertmacro wails.setShellContext

    !insertmacro wails.webview2runtime

    SetOutPath $INSTDIR

    !insertmacro wails.files

    CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
    CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"

    !insertmacro wails.associateFiles
    !insertmacro wails.associateCustomProtocols

    !insertmacro wails.writeUninstaller
SectionEnd

Section "uninstall"
    !insertmacro wails.setShellContext

    RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath

    RMDir /r $INSTDIR

    Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
    Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"

    !insertmacro wails.unassociateFiles
    !insertmacro wails.unassociateCustomProtocols

    !insertmacro wails.deleteUninstaller
SectionEnd


================================================
FILE: build/windows/installer/wails_tools.nsh
================================================
# DO NOT EDIT - Generated automatically by `wails build`

!include "x64.nsh"
!include "WinVer.nsh"
!include "FileFunc.nsh"

!ifndef INFO_PROJECTNAME
    !define INFO_PROJECTNAME "res-downloader"
!endif
!ifndef INFO_COMPANYNAME
    !define INFO_COMPANYNAME "res-downloader"
!endif
!ifndef INFO_PRODUCTNAME
    !define INFO_PRODUCTNAME "res-downloader"
!endif
!ifndef INFO_PRODUCTVERSION
    !define INFO_PRODUCTVERSION "3.1.3"
!endif
!ifndef INFO_COPYRIGHT
    !define INFO_COPYRIGHT "Copyright © 2023"
!endif
!ifndef PRODUCT_EXECUTABLE
    !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
!endif
!ifndef UNINST_KEY_NAME
    !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
!endif
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"

!ifndef REQUEST_EXECUTION_LEVEL
    !define REQUEST_EXECUTION_LEVEL "admin"
!endif

RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"

!ifdef ARG_WAILS_AMD64_BINARY
    !define SUPPORTS_AMD64
!endif

!ifdef ARG_WAILS_ARM64_BINARY
    !define SUPPORTS_ARM64
!endif

!ifdef SUPPORTS_AMD64
    !ifdef SUPPORTS_ARM64
        !define ARCH "amd64_arm64"
    !else
        !define ARCH "amd64"
    !endif
!else
    !ifdef SUPPORTS_ARM64
        !define ARCH "arm64"
    !else
        !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
    !endif
!endif

!macro wails.checkArchitecture
    !ifndef WAILS_WIN10_REQUIRED
        !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
    !endif

    !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
        !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
    !endif

    ${If} ${AtLeastWin10}
        !ifdef SUPPORTS_AMD64
            ${if} ${IsNativeAMD64}
                Goto ok
            ${EndIf}
        !endif

        !ifdef SUPPORTS_ARM64
            ${if} ${IsNativeARM64}
                Goto ok
            ${EndIf}
        !endif

        IfSilent silentArch notSilentArch
        silentArch:
            SetErrorLevel 65
            Abort
        notSilentArch:
            MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
            Quit
    ${else}
        IfSilent silentWin notSilentWin
        silentWin:
            SetErrorLevel 64
            Abort
        notSilentWin:
            MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
            Quit
    ${EndIf}

    ok:
!macroend

!macro wails.files
    !ifdef SUPPORTS_AMD64
        ${if} ${IsNativeAMD64}
            File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
        ${EndIf}
    !endif

    !ifdef SUPPORTS_ARM64
        ${if} ${IsNativeARM64}
            File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
        ${EndIf}
    !endif
!macroend

!macro wails.writeUninstaller
    WriteUninstaller "$INSTDIR\uninstall.exe"

    SetRegView 64
    WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
    WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
    WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
    WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
    WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
    WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"

    ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
    IntFmt $0 "0x%08X" $0
    WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
!macroend

!macro wails.deleteUninstaller
    Delete "$INSTDIR\uninstall.exe"

    SetRegView 64
    DeleteRegKey HKLM "${UNINST_KEY}"
!macroend

!macro wails.setShellContext
    ${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
        SetShellVarContext all
    ${else}
        SetShellVarContext current
    ${EndIf}
!macroend

# Install webview2 by launching the bootstrapper
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
!macro wails.webview2runtime
    !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
        !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
    !endif

    SetRegView 64
	# If the admin key exists and is not empty then webview2 is already installed
	ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
    ${If} $0 != ""
        Goto ok
    ${EndIf}

    ${If} ${REQUEST_EXECUTION_LEVEL} == "user"
        # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
	    ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
        ${If} $0 != ""
            Goto ok
        ${EndIf}
     ${EndIf}

	SetDetailsPrint both
    DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
    SetDetailsPrint listonly

    InitPluginsDir
    CreateDirectory "$pluginsdir\webview2bootstrapper"
    SetOutPath "$pluginsdir\webview2bootstrapper"
    File "tmp\MicrosoftEdgeWebview2Setup.exe"
    ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'

    SetDetailsPrint both
    ok:
!macroend

# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
  ; Backup the previously associated file class
  ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
  WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"

  WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"

  WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
  WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
  WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
  WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
  WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
!macroend

!macro APP_UNASSOCIATE EXT FILECLASS
  ; Backup the previously associated file class
  ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
  WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"

  DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
!macroend

!macro wails.associateFiles
    ; Create file associations
    
!macroend

!macro wails.unassociateFiles
    ; Delete app associations
    
!macroend

!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
  DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
!macroend

!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
  DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
!macroend

!macro wails.associateCustomProtocols
    ; Create custom protocols associations
    
!macroend

!macro wails.unassociateCustomProtocols
    ; Delete app custom protocol associations
    
!macroend


================================================
FILE: build/windows/wails.exe.manifest
================================================
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
    <assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
    <dependency>
        <dependentAssembly>
            <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
        </dependentAssembly>
    </dependency>
    <asmv3:application>
        <asmv3:windowsSettings>
            <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
            <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
        </asmv3:windowsSettings>
    </asmv3:application>
</assembly>

================================================
FILE: core/aes.go
================================================
package core

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"io"
)

type AESCipher struct {
	key []byte
}

func NewAESCipher(key string) *AESCipher {
	return &AESCipher{key: []byte(key)}
}

func (a *AESCipher) Encrypt(plainText string) (string, error) {
	block, err := aes.NewCipher(a.key)
	if err != nil {
		return "", err
	}

	padding := block.BlockSize() - len(plainText)%block.BlockSize()
	padText := bytes.Repeat([]byte{byte(padding)}, padding)
	plainText = plainText + string(padText)

	cipherText := make([]byte, aes.BlockSize+len(plainText))
	iv := cipherText[:aes.BlockSize]

	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		return "", err
	}

	mode := cipher.NewCBCEncrypter(block, iv)
	mode.CryptBlocks(cipherText[aes.BlockSize:], []byte(plainText))

	return base64.StdEncoding.EncodeToString(cipherText), nil
}

func (a *AESCipher) Decrypt(cipherText string) (string, error) {
	cipherTextBytes, err := base64.StdEncoding.DecodeString(cipherText)
	if err != nil {
		return "", err
	}

	block, err := aes.NewCipher(a.key)
	if err != nil {
		return "", err
	}

	if len(cipherTextBytes) < aes.BlockSize {
		return "", errors.New("ciphertext too short")
	}

	iv := cipherTextBytes[:aes.BlockSize]
	cipherTextBytes = cipherTextBytes[aes.BlockSize:]

	mode := cipher.NewCBCDecrypter(block, iv)
	mode.CryptBlocks(cipherTextBytes, cipherTextBytes)
	
	padding := int(cipherTextBytes[len(cipherTextBytes)-1])
	if padding > len(cipherTextBytes) || padding > aes.BlockSize {
		return "", errors.New("padding size error")
	}
	plainText := cipherTextBytes[:len(cipherTextBytes)-padding]

	return string(plainText), nil
}


================================================
FILE: core/app.go
================================================
package core

import (
	"context"
	"embed"
	"fmt"
	"github.com/vrischmann/userdir"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"res-downloader/core/shared"
	"strconv"
	"time"
)

type App struct {
	ctx         context.Context
	assets      embed.FS
	AppName     string `json:"AppName"`
	Version     string `json:"Version"`
	Description string `json:"Description"`
	Copyright   string `json:"Copyright"`
	UserDir     string `json:"-"`
	LockFile    string `json:"-"`
	PublicCrt   []byte `json:"-"`
	PrivateKey  []byte `json:"-"`
	IsProxy     bool   `json:"IsProxy"`
	IsReset     bool   `json:"-"`
}

var (
	appOnce        *App
	globalConfig   *Config
	globalLogger   *Logger
	resourceOnce   *Resource
	systemOnce     *SystemSetup
	proxyOnce      *Proxy
	httpServerOnce *HttpServer
	ruleOnce       *RuleSet
)

func GetApp(assets embed.FS, wjs string) *App {
	if appOnce == nil {
		matches := regexp.MustCompile(`"productVersion":\s*"([\d.]+)"`).FindStringSubmatch(wjs)
		version := "1.0.1"
		if len(matches) > 0 {
			version = matches[1]
		}

		appOnce = &App{
			assets:      assets,
			AppName:     "res-downloader",
			Version:     version,
			Description: "res-downloader是一款集网络资源嗅探 + 高速下载功能于一体的软件,高颜值、高性能和多样化,提供个人用户下载自己上传到各大平台的网络资源功能!",
			Copyright:   "Copyright © 2023~" + strconv.Itoa(time.Now().Year()),
			IsReset:     false,
			PublicCrt: []byte(`-----BEGIN CERTIFICATE-----
MIIDwzCCAqugAwIBAgIUFAnC6268dp/z1DR9E1UepiWgWzkwDQYJKoZIhvcNAQEL
BQAwcDELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUNob25ncWluZzESMBAGA1UEBwwJ
Q2hvbmdxaW5nMQ4wDAYDVQQKDAVnb3dhczEWMBQGA1UECwwNSVQgRGVwYXJ0bWVu
dDERMA8GA1UEAwwIZ293YXMuY24wIBcNMjQwMjE4MDIwOTI2WhgPMjEyNDAxMjUw
MjA5MjZaMHAxCzAJBgNVBAYTAkNOMRIwEAYDVQQIDAlDaG9uZ3FpbmcxEjAQBgNV
BAcMCUNob25ncWluZzEOMAwGA1UECgwFZ293YXMxFjAUBgNVBAsMDUlUIERlcGFy
dG1lbnQxETAPBgNVBAMMCGdvd2FzLmNuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA3A7dt7eoqAaBxv2Npjo8Z7VkGvXT93jZfpgAuuNuQ5RLcnOnMzQC
CrrjPcLfsAMA0AIK3eUWsXXKSR9SZTJBLQRZCJHZ9AIPfA+58JVQPTjd8UIuQZJf
rDf6FjhPJTsLzcjTU+mT7t6lEimPEl2VWN9eXWqs9nkVrJtqLao6m1hoYfXOxRh6
96/WgBtPHcmjujryteBiSITVflDjx+YQzDGsbqw7fM52klMPd2+w/vmhJ4pxq6P7
Ni2OBvdXYDPIuLfPFFqG16arORjBkyNCJy19iOuh5LXh+EUX11wvbLwNgsTd8j9v
eBSD+4HUUNQhiXiXJbs7I7cdFYthvb609QIDAQABo1MwUTAdBgNVHQ4EFgQUdI8p
aY1A47rWCRvQKSTRCCk6FoMwHwYDVR0jBBgwFoAUdI8paY1A47rWCRvQKSTRCCk6
FoMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEArMCAfqidgXL7
cW5TAZTCqnUeKzbbqMJgk6iFsma8scMRsUXz9ZhF0UVf98376KvoJpy4vd81afbi
TehQ8wVBuKTtkHeh/MkXMWC/FU4HqSjtvxpic2+Or5dMjIrfa5VYPgzfqNaBIUh4
InD5lo8b/n5V+jdwX7RX9VYAKug6QZlCg5YSKIvgNRChb36JmrGcvsp5R0Vejnii
e3oowvgwikqm6XR6BEcRpPkztqcKST7jPFGHiXWsAqiibc+/plMW9qebhfMXEGhQ
5yVNeSxX2zqasZvP/fRy+3I5iVilxtKvJuVpPZ0UZzGS0CJ/lF67ntibktiPa3sR
D8HixYbEDg==
-----END CERTIFICATE-----
`),
			PrivateKey: []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDcDt23t6ioBoHG
/Y2mOjxntWQa9dP3eNl+mAC6425DlEtyc6czNAIKuuM9wt+wAwDQAgrd5RaxdcpJ
H1JlMkEtBFkIkdn0Ag98D7nwlVA9ON3xQi5Bkl+sN/oWOE8lOwvNyNNT6ZPu3qUS
KY8SXZVY315daqz2eRWsm2otqjqbWGhh9c7FGHr3r9aAG08dyaO6OvK14GJIhNV+
UOPH5hDMMaxurDt8znaSUw93b7D++aEninGro/s2LY4G91dgM8i4t88UWobXpqs5
GMGTI0InLX2I66HkteH4RRfXXC9svA2CxN3yP294FIP7gdRQ1CGJeJcluzsjtx0V
i2G9vrT1AgMBAAECggEAF0obfQ4a82183qqHC0iui+tOpOvPeyl3G0bLDPx09wIC
2iITV//xF2GgGzE8q0wmEd2leMZ+GFn3BrYh6kPfUfxbz+RfxMtTCDZB34xt6YzT
MG1op9ft+DQUa7WZ6r7NCQJwGzllRqqZncp4MeFlpPo+6nQXyh4WhSYNnredbENE
uPZ63Kme4RZfMvtVso+XgAQM3oDih0onv1YitmNQpL9rRzlthTfybAT4737DBINq
zsmBNE6QIsXnSKpzo11OtDgof2QM9ac6eAXf73oTpDxfodwCotILytKn+8WYvlR+
T15uuknb4M3XI1FPVolkF4qtK5SLAAbVzV4DsCmuIQKBgQD6bTKKbL2huvU6dEKx
bgS079LfQUxxOTClgwkhVsMxRtvcPBnHYMAsPK4mnMhEh9x+TF6wxMx0pmhQluPI
ZULNBj/qdoiBL0RwVLA+9jgE0NeWB3XXFDsEavQBr9Q8CC0uzrsgsxFcvHpqqs2Q
RtngxRWtJP06D6mKC23s4YjDHwKBgQDg9KUCFqOmWcRXyeg9gYMC4jFFQw4lUQBd
sYpqSMHDw1b+T1W/dCPbwbxZL/+d8y930BYy9QYDtQwHdLyXCH0pHM7S6rfgr5xk
2Szd8xBUIqmeV/zcR00mTeQHJ1M50VHfclAVgZgkpWSoLwbX+bXyx/mfqLAtynZ5
yU9RfrT5awKBgQC0uJ8TlFvZXjFgyMvkfY/5/2R/ZwFCaFI573FkVNeyNP+vVNQJ
tUGZ6wSGqvg/tIgjwPtIuA0QVZLMLcgeMy1dBhiUHIxwJetO4V77YPaWSxx5kdKx
r1DT5FdI7FnOJNxufhQ/CdsKwJ3bYn3Mk8TiV3hIJnx0LR9dltfybeQjYwKBgDOY
6aApATBOtrJMJXC2HA61QwfX8Y6tnZ/f8RefyJHWZEXAfLKFORRWw5TRZZgdB247
1Furx81h4Xh0Vi1uTQb5DJdkLvjiTsTy60+dSMmDidQ/6ke8Mv3uL7dUVcqVMGpI
FgZYy0TcitHot3EiXZFqPN9aGc7m+XXFruPKZEgxAoGBAMA96jsow7CzulU+GRW8
Njg4zWuAEVErgPoNBcOXAVWLCTU/qGIEMNpZL6Ok34kf13pJDMjQ8eDuQHu5CSqf
0ul5Zy85fwfVq2IvNAyYT8eflQprTejFw22CHhfPBfADVW9ro8dK/Jw+J/31Vh7V
ILKEQKmPPzKs7kp/7Nz+2cT3
-----END PRIVATE KEY-----
`),
		}
		appOnce.UserDir = filepath.Join(userdir.GetConfigHome(), appOnce.AppName)
		err := os.MkdirAll(appOnce.UserDir, 0750)
		if err != nil {
			fmt.Println("Mkdir UserDir err: ", err.Error())
		}
		appOnce.LockFile = filepath.Join(appOnce.UserDir, "install.lock")
		initLogger()
		initConfig()
		initProxy()
		initResource()
		initHttpServer()
		initSystem()
		initRule()
	}
	return appOnce
}

func (a *App) Startup(ctx context.Context) {
	a.ctx = ctx
	go httpServerOnce.run()
}

func (a *App) OnExit() {
	a.UnsetSystemProxy()
	globalLogger.Close()
	if appOnce.IsReset {
		err := a.ResetApp()
		fmt.Println("err:", err)
	}
}

func (a *App) installCert() (string, error) {
	out, err := systemOnce.installCert()
	if err != nil {
		globalLogger.Esg(err, out)
		return out, err
	} else {
		if err := a.lock(); err != nil {
			globalLogger.Err(err)
		}
	}
	return out, nil
}

func (a *App) OpenSystemProxy() error {
	if a.IsProxy {
		return nil
	}
	err := systemOnce.setProxy()
	if err == nil {
		a.IsProxy = true
		return nil
	}
	return err
}

func (a *App) UnsetSystemProxy() error {
	if !a.IsProxy {
		return nil
	}
	err := systemOnce.unsetProxy()
	if err == nil {
		a.IsProxy = false
		return nil
	}
	return err
}

func (a *App) isInstall() bool {
	return shared.FileExist(a.LockFile)
}

func (a *App) lock() error {
	err := os.WriteFile(a.LockFile, []byte("success"), 0644)
	if err != nil {
		return err
	}
	return nil
}

func (a *App) ResetApp() error {
	exePath, err := os.Executable()
	if err != nil {
		return err
	}

	exePath, err = filepath.Abs(exePath)
	if err != nil {
		return err
	}

	_ = os.Remove(filepath.Join(appOnce.UserDir, "install.lock"))
	_ = os.Remove(filepath.Join(appOnce.UserDir, "pass.cache"))
	_ = os.Remove(filepath.Join(appOnce.UserDir, "config.json"))
	_ = os.Remove(filepath.Join(appOnce.UserDir, "cert.crt"))

	cmd := exec.Command(exePath)
	cmd.Start()
	return nil
}


================================================
FILE: core/bind.go
================================================
package core

import (
	"github.com/wailsapp/wails/v2/pkg/runtime"
)

type Bind struct {
}

func NewBind() *Bind {
	return &Bind{}
}

func (b *Bind) Config() *ResponseData {
	return httpServerOnce.buildResp(1, "ok", globalConfig)
}

func (b *Bind) AppInfo() *ResponseData {
	return httpServerOnce.buildResp(1, "ok", appOnce)
}

func (b *Bind) ResetApp() {
	appOnce.IsReset = true
	runtime.Quit(appOnce.ctx)
}


================================================
FILE: core/config.go
================================================
package core

import (
	"encoding/json"
	"os"
	"os/user"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
)

type MimeInfo struct {
	Type   string `json:"Type"`
	Suffix string `json:"Suffix"`
}

// Config struct
type Config struct {
	storage       *Storage
	Theme         string              `json:"Theme"`
	Locale        string              `json:"Locale"`
	Host          string              `json:"Host"`
	Port          string              `json:"Port"`
	Quality       int                 `json:"Quality"`
	SaveDirectory string              `json:"SaveDirectory"`
	FilenameLen   int                 `json:"FilenameLen"`
	FilenameTime  bool                `json:"FilenameTime"`
	UpstreamProxy string              `json:"UpstreamProxy"`
	OpenProxy     bool                `json:"OpenProxy"`
	DownloadProxy bool                `json:"DownloadProxy"`
	AutoProxy     bool                `json:"AutoProxy"`
	WxAction      bool                `json:"WxAction"`
	TaskNumber    int                 `json:"TaskNumber"`
	DownNumber    int                 `json:"DownNumber"`
	UserAgent     string              `json:"UserAgent"`
	UseHeaders    string              `json:"UseHeaders"`
	InsertTail    bool                `json:"InsertTail"`
	MimeMap       map[string]MimeInfo `json:"MimeMap"`
	Rule          string              `json:"Rule"`
}

var (
	mimeMux sync.RWMutex
)

func initConfig() *Config {
	if globalConfig != nil {
		return globalConfig
	}

	defaultConfig := &Config{
		Theme:         "lightTheme",
		Locale:        "zh",
		Host:          "127.0.0.1",
		Port:          "8899",
		Quality:       0,
		SaveDirectory: getDefaultDownloadDir(),
		FilenameLen:   0,
		FilenameTime:  true,
		UpstreamProxy: "",
		OpenProxy:     false,
		DownloadProxy: false,
		AutoProxy:     false,
		WxAction:      true,
		TaskNumber:    runtime.NumCPU() * 2,
		DownNumber:    3,
		UserAgent:     "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
		UseHeaders:    "default",
		InsertTail:    true,
		MimeMap:       getDefaultMimeMap(),
		Rule:          "*",
	}

	rawDefaults, err := json.Marshal(defaultConfig)
	if err != nil {
		return globalConfig
	}

	storage := NewStorage("config.json", rawDefaults)
	defaultConfig.storage = storage
	globalConfig = defaultConfig

	data, err := storage.Load()
	if err != nil {
		globalLogger.Esg(err, "load config failed, using defaults")
		return globalConfig
	}

	var cacheMap map[string]interface{}
	if err := json.Unmarshal(data, &cacheMap); err != nil {
		globalLogger.Esg(err, "parse cached config failed, using defaults")
		return globalConfig
	}

	var defaultMap map[string]interface{}
	defaultBytes, _ := json.Marshal(defaultConfig)
	_ = json.Unmarshal(defaultBytes, &defaultMap)

	for k, v := range cacheMap {
		if _, ok := defaultMap[k]; ok {
			defaultMap[k] = v
		}
	}

	finalBytes, err := json.Marshal(defaultMap)
	if err != nil {
		globalLogger.Esg(err, "marshal merged config failed")
		return globalConfig
	}

	if err := json.Unmarshal(finalBytes, globalConfig); err != nil {
		globalLogger.Esg(err, "unmarshal merged config to struct failed")
	}

	return globalConfig
}

func getDefaultMimeMap() map[string]MimeInfo {
	return map[string]MimeInfo{
		"image/png":                     {Type: "image", Suffix: ".png"},
		"image/webp":                    {Type: "image", Suffix: ".webp"},
		"image/jpeg":                    {Type: "image", Suffix: ".jpeg"},
		"image/jpg":                     {Type: "image", Suffix: ".jpg"},
		"image/gif":                     {Type: "image", Suffix: ".gif"},
		"image/avif":                    {Type: "image", Suffix: ".avif"},
		"image/bmp":                     {Type: "image", Suffix: ".bmp"},
		"image/tiff":                    {Type: "image", Suffix: ".tiff"},
		"image/heic":                    {Type: "image", Suffix: ".heic"},
		"image/x-icon":                  {Type: "image", Suffix: ".ico"},
		"image/svg+xml":                 {Type: "image", Suffix: ".svg"},
		"image/vnd.adobe.photoshop":     {Type: "image", Suffix: ".psd"},
		"image/jp2":                     {Type: "image", Suffix: ".jp2"},
		"image/jpeg2000":                {Type: "image", Suffix: ".jp2"},
		"image/apng":                    {Type: "image", Suffix: ".apng"},
		"audio/mpeg":                    {Type: "audio", Suffix: ".mp3"},
		"audio/mp3":                     {Type: "audio", Suffix: ".mp3"},
		"audio/wav":                     {Type: "audio", Suffix: ".wav"},
		"audio/aiff":                    {Type: "audio", Suffix: ".aiff"},
		"audio/x-aiff":                  {Type: "audio", Suffix: ".aiff"},
		"audio/aac":                     {Type: "audio", Suffix: ".aac"},
		"audio/ogg":                     {Type: "audio", Suffix: ".ogg"},
		"audio/flac":                    {Type: "audio", Suffix: ".flac"},
		"audio/midi":                    {Type: "audio", Suffix: ".mid"},
		"audio/x-midi":                  {Type: "audio", Suffix: ".mid"},
		"audio/x-ms-wma":                {Type: "audio", Suffix: ".wma"},
		"audio/opus":                    {Type: "audio", Suffix: ".opus"},
		"audio/webm":                    {Type: "audio", Suffix: ".webm"},
		"audio/mp4":                     {Type: "audio", Suffix: ".m4a"},
		"audio/amr":                     {Type: "audio", Suffix: ".amr"},
		"video/mp4":                     {Type: "video", Suffix: ".mp4"},
		"video/webm":                    {Type: "video", Suffix: ".webm"},
		"video/ogg":                     {Type: "video", Suffix: ".ogv"},
		"video/x-msvideo":               {Type: "video", Suffix: ".avi"},
		"video/mpeg":                    {Type: "video", Suffix: ".mpeg"},
		"video/quicktime":               {Type: "video", Suffix: ".mov"},
		"video/x-ms-wmv":                {Type: "video", Suffix: ".wmv"},
		"video/3gpp":                    {Type: "video", Suffix: ".3gp"},
		"video/x-matroska":              {Type: "video", Suffix: ".mkv"},
		"audio/video":                   {Type: "live", Suffix: ".flv"},
		"video/x-flv":                   {Type: "live", Suffix: ".flv"},
		"application/dash+xml":          {Type: "live", Suffix: ".mpd"},
		"application/vnd.apple.mpegurl": {Type: "m3u8", Suffix: ".m3u8"},
		"application/x-mpegurl":         {Type: "m3u8", Suffix: ".m3u8"},
		"application/x-mpeg":            {Type: "m3u8", Suffix: ".m3u8"},
		"audio/x-mpegurl":               {Type: "m3u8", Suffix: ".m3u8"},
		"application/pdf":               {Type: "pdf", Suffix: ".pdf"},
		"application/vnd.ms-powerpoint": {Type: "ppt", Suffix: ".ppt"},
		"application/vnd.openxmlformats-officedocument.presentationml.presentation": {Type: "ppt", Suffix: ".pptx"},
		"application/vnd.ms-excel": {Type: "xls", Suffix: ".xls"},
		"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {Type: "xls", Suffix: ".xlsx"},
		"text/csv":           {Type: "xls", Suffix: ".csv"},
		"application/msword": {Type: "doc", Suffix: ".doc"},
		"application/rtf":    {Type: "doc", Suffix: ".rtf"},
		"text/rtf":           {Type: "doc", Suffix: ".rtf"},
		"application/vnd.oasis.opendocument.text":                                 {Type: "doc", Suffix: ".odt"},
		"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {Type: "doc", Suffix: ".docx"},
		"font/woff":                {Type: "font", Suffix: ".woff"},
		"application/octet-stream": {Type: "stream", Suffix: "default"},
	}
}

func getDefaultDownloadDir() string {
	usr, err := user.Current()
	if err != nil {
		return ""
	}

	homeDir := usr.HomeDir
	var downloadDir string

	switch runtime.GOOS {
	case "windows", "darwin":
		downloadDir = filepath.Join(homeDir, "Downloads")
	case "linux":
		downloadDir = filepath.Join(homeDir, "Downloads")
		if xdgDir := os.Getenv("XDG_DOWNLOAD_DIR"); xdgDir != "" {
			downloadDir = xdgDir
		}
	}

	if stat, err := os.Stat(downloadDir); err == nil && stat.IsDir() {
		return downloadDir
	}

	return ""
}

func (c *Config) setConfig(config Config) {
	oldProxy := c.UpstreamProxy
	openProxy := c.OpenProxy
	oldRule := c.Rule
	c.Host = config.Host
	c.Port = config.Port
	c.Theme = config.Theme
	c.Locale = config.Locale
	c.Quality = config.Quality
	c.SaveDirectory = config.SaveDirectory
	c.FilenameLen = config.FilenameLen
	c.FilenameTime = config.FilenameTime
	c.UpstreamProxy = config.UpstreamProxy
	c.UserAgent = config.UserAgent
	c.OpenProxy = config.OpenProxy
	c.DownloadProxy = config.DownloadProxy
	c.AutoProxy = config.AutoProxy
	c.TaskNumber = config.TaskNumber
	c.DownNumber = config.DownNumber
	c.WxAction = config.WxAction
	c.UseHeaders = config.UseHeaders
	c.InsertTail = config.InsertTail
	c.Rule = config.Rule
	if oldProxy != c.UpstreamProxy || openProxy != c.OpenProxy {
		proxyOnce.setTransport()
	}

	if oldRule != c.Rule {
		err := ruleOnce.Load(c.Rule)
		if err != nil {
			globalLogger.Esg(err, "set rule failed")
		}
	}

	mimeMux.Lock()
	c.MimeMap = config.MimeMap
	mimeMux.Unlock()

	jsonData, err := json.Marshal(c)
	if err == nil {
		_ = globalConfig.storage.Store(jsonData)
	}
}

func (c *Config) getConfig(key string) interface{} {
	switch key {
	case "Host":
		return c.Host
	case "Port":
		return c.Port
	case "Theme":
		return c.Theme
	case "Locale":
		return c.Locale
	case "Quality":
		return c.Quality
	case "SaveDirectory":
		return c.SaveDirectory
	case "FilenameLen":
		return c.FilenameLen
	case "FilenameTime":
		return c.FilenameTime
	case "UpstreamProxy":
		return c.UpstreamProxy
	case "UserAgent":
		return c.UserAgent
	case "OpenProxy":
		return c.OpenProxy
	case "DownloadProxy":
		return c.DownloadProxy
	case "AutoProxy":
		return c.AutoProxy
	case "TaskNumber":
		return c.TaskNumber
	case "DownNumber":
		return c.DownNumber
	case "WxAction":
		return c.WxAction
	case "UseHeaders":
		return c.UseHeaders
	case "InsertTail":
		return c.InsertTail
	case "MimeMap":
		mimeMux.RLock()
		defer mimeMux.RUnlock()
		return c.MimeMap
	case "Rule":
		return c.Rule
	default:
		return nil
	}
}

func (c *Config) typeSuffix(mime string) (string, string) {
	mimeMux.RLock()
	defer mimeMux.RUnlock()
	mime = strings.ToLower(strings.Split(mime, ";")[0])
	if v, ok := c.MimeMap[mime]; ok {
		return v.Type, v.Suffix
	}
	return "", ""
}


================================================
FILE: core/downloader.go
================================================
package core

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"res-downloader/core/shared"
	"strings"
	"sync"
	"time"
)

const (
	MaxRetries  = 3               // 最大重试次数
	RetryDelay  = 3 * time.Second // 重试延迟
	MinPartSize = 1 * 1024 * 1024 // 最小分片大小(1MB)
)

type ProgressCallback func(totalDownloaded float64, totalSize float64, taskID int, taskProgress float64)

type ProgressChan struct {
	taskID int
	bytes  int64
}

type DownloadTask struct {
	taskID         int
	rangeStart     int64
	rangeEnd       int64
	downloadedSize int64
	isCompleted    bool
	err            error
}

type FileDownloader struct {
	Url              string
	Referer          string
	ProxyUrl         *url.URL
	FileName         string
	File             *os.File
	totalTasks       int
	TotalSize        int64
	IsMultiPart      bool
	RetryOnError     bool
	Headers          map[string]string
	DownloadTaskList []*DownloadTask
	progressCallback ProgressCallback
	ctx              context.Context
	cancelFunc       context.CancelFunc
}

func NewFileDownloader(url, filename string, totalTasks int, headers map[string]string) *FileDownloader {
	ctx, cancelFunc := context.WithCancel(context.Background())
	return &FileDownloader{
		Url:              url,
		FileName:         filename,
		totalTasks:       totalTasks,
		IsMultiPart:      false,
		RetryOnError:     false,
		TotalSize:        0,
		Headers:          headers,
		DownloadTaskList: make([]*DownloadTask, 0),
		ctx:              ctx,
		cancelFunc:       cancelFunc,
	}
}

func (fd *FileDownloader) buildClient() *http.Client {
	transport := &http.Transport{
		MaxIdleConnsPerHost: 100,
		IdleConnTimeout:     90 * time.Second,
	}
	if fd.ProxyUrl != nil {
		transport.Proxy = http.ProxyURL(fd.ProxyUrl)
	}
	return &http.Client{
		Transport: transport,
	}
}

var forbiddenDownloadHeaders = map[string]struct{}{
	"accept-encoding":   {},
	"content-length":    {},
	"host":              {},
	"connection":        {},
	"keep-alive":        {},
	"proxy-connection":  {},
	"transfer-encoding": {},

	"sec-fetch-site":     {},
	"sec-fetch-mode":     {},
	"sec-fetch-dest":     {},
	"sec-fetch-user":     {},
	"sec-ch-ua":          {},
	"sec-ch-ua-mobile":   {},
	"sec-ch-ua-platform": {},

	"if-none-match":     {},
	"if-modified-since": {},

	"x-forwarded-for": {},
	"x-real-ip":       {},
}

func (fd *FileDownloader) setHeaders(request *http.Request) {
	for key, value := range fd.Headers {
		if globalConfig.UseHeaders == "default" {
			lk := strings.ToLower(key)
			if _, forbidden := forbiddenDownloadHeaders[lk]; forbidden {
				continue
			}
			request.Header.Set(key, value)
			continue
		}
		
		if strings.Contains(globalConfig.UseHeaders, key) {
			request.Header.Set(key, value)
		}
	}
}

func (fd *FileDownloader) init() error {
	parsedURL, err := url.Parse(fd.Url)
	if err != nil {
		return fmt.Errorf("parse URL failed: %w", err)
	}
	if parsedURL.Scheme != "" && parsedURL.Host != "" {
		fd.Referer = parsedURL.Scheme + "://" + parsedURL.Host + "/"
	}

	if globalConfig.DownloadProxy && globalConfig.UpstreamProxy != "" && !strings.Contains(globalConfig.UpstreamProxy, globalConfig.Port) {
		proxyURL, err := url.Parse(globalConfig.UpstreamProxy)
		if err == nil {
			fd.ProxyUrl = proxyURL
		}
	}

	request, err := http.NewRequest("HEAD", fd.Url, nil)
	if err != nil {
		return fmt.Errorf("create HEAD request failed: %w", err)
	}

	if _, ok := fd.Headers["User-Agent"]; !ok {
		fd.Headers["User-Agent"] = globalConfig.UserAgent
	}
	if _, ok := fd.Headers["Referer"]; !ok {
		fd.Headers["Referer"] = fd.Referer
	}

	fd.setHeaders(request)

	var resp *http.Response
	for retries := 0; retries < MaxRetries; retries++ {
		resp, err = fd.buildClient().Do(request)
		if err == nil {
			break
		}
		if retries < MaxRetries-1 {
			time.Sleep(RetryDelay)
			globalLogger.Warn().Msgf("HEAD request failed, retrying (%d/%d): %v", retries+1, MaxRetries, err)
		}
	}

	if err != nil {
		return fmt.Errorf("HEAD request failed after %d retries: %w", MaxRetries, err)
	}
	defer resp.Body.Close()

	fd.TotalSize = resp.ContentLength
	if fd.TotalSize <= 0 {
		fd.IsMultiPart = false
		fd.TotalSize = -1
	} else if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > MinPartSize {
		fd.IsMultiPart = true
	}

	dir := filepath.Dir(fd.FileName)
	if err := os.MkdirAll(dir, os.ModePerm); err != nil {
		return fmt.Errorf("create directory failed: %w", err)
	}

	fd.FileName = shared.GetUniqueFileName(fd.FileName)

	fd.File, err = os.OpenFile(fd.FileName, os.O_RDWR|os.O_CREATE, 0644)
	if err != nil {
		return fmt.Errorf("file open failed: %w", err)
	}
	if fd.TotalSize > 0 {
		if err := fd.File.Truncate(fd.TotalSize); err != nil {
			fd.File.Close()
			return fmt.Errorf("file truncate failed: %w", err)
		}
	}
	return nil
}

func (fd *FileDownloader) createDownloadTasks() {
	if fd.IsMultiPart {
		if fd.totalTasks <= 0 {
			fd.totalTasks = 4
		}
		eachSize := fd.TotalSize / int64(fd.totalTasks)
		if eachSize < MinPartSize {
			fd.totalTasks = int(fd.TotalSize / MinPartSize)
			if fd.totalTasks < 1 {
				fd.totalTasks = 1
			}
			eachSize = fd.TotalSize / int64(fd.totalTasks)
		}

		for i := 0; i < fd.totalTasks; i++ {
			start := eachSize * int64(i)
			end := eachSize*int64(i+1) - 1
			if i == fd.totalTasks-1 {
				end = fd.TotalSize - 1
			}
			fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{
				taskID:     i,
				rangeStart: start,
				rangeEnd:   end,
			})
		}
	} else {
		fd.totalTasks = 1
		rangeEnd := int64(-1)
		if fd.TotalSize > 0 {
			rangeEnd = fd.TotalSize - 1
		}
		fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{
			taskID:     0,
			rangeStart: 0,
			rangeEnd:   rangeEnd,
		})
	}
}

func (fd *FileDownloader) startDownload() error {
	wg := &sync.WaitGroup{}
	progressChan := make(chan ProgressChan, len(fd.DownloadTaskList))
	errorChan := make(chan error, len(fd.DownloadTaskList))

	for _, task := range fd.DownloadTaskList {
		wg.Add(1)
		go fd.startDownloadTask(wg, progressChan, errorChan, task)
	}

	go func() {
		taskProgress := make([]int64, len(fd.DownloadTaskList))
		totalDownloaded := int64(0)

		for progress := range progressChan {
			taskProgress[progress.taskID] += progress.bytes
			totalDownloaded += progress.bytes

			if fd.progressCallback != nil {
				taskPercentage := float64(0)
				if task := fd.DownloadTaskList[progress.taskID]; task != nil {
					taskSize := task.rangeEnd - task.rangeStart + 1
					if taskSize > 0 {
						taskPercentage = float64(taskProgress[progress.taskID]) / float64(taskSize) * 100
					}
				}
				fd.progressCallback(float64(totalDownloaded), float64(fd.TotalSize), progress.taskID, taskPercentage)
			}
		}
	}()

	go func() {
		wg.Wait()
		close(progressChan)
		close(errorChan)
	}()

	var errArr []error
	for err := range errorChan {
		errArr = append(errArr, err)
	}

	if len(errArr) > 0 {
		if !fd.RetryOnError && fd.IsMultiPart {
			// 降级
			fd.RetryOnError = true
			fd.DownloadTaskList = []*DownloadTask{}
			fd.totalTasks = 1
			fd.IsMultiPart = false
			fd.createDownloadTasks()
			return fd.startDownload()
		}
		return fmt.Errorf("download failed with %d errors: %v", len(errArr), errArr[0])
	}

	if err := fd.verifyDownload(); err != nil {
		return err
	}

	return nil
}

func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan chan ProgressChan, errorChan chan error, task *DownloadTask) {
	defer wg.Done()

	for retries := 0; retries < MaxRetries; retries++ {
		err := fd.doDownloadTask(progressChan, task)
		if err == nil {
			task.isCompleted = true
			return
		}

		if strings.Contains(err.Error(), "cancelled") {
			errorChan <- err
			return
		}

		task.err = err
		globalLogger.Warn().Msgf("Task %d failed (attempt %d/%d): %v", task.taskID, retries+1, MaxRetries, err)

		if retries < MaxRetries-1 {
			select {
			case <-fd.ctx.Done():
				errorChan <- fmt.Errorf("task %d cancelled during retry", task.taskID)
				return
			case <-time.After(RetryDelay):
			}
		}
	}

	errorChan <- fmt.Errorf("task %d failed after %d attempts: %v", task.taskID, MaxRetries, task.err)
}

func (fd *FileDownloader) doDownloadTask(progressChan chan ProgressChan, task *DownloadTask) error {
	select {
	case <-fd.ctx.Done():
		return fmt.Errorf("download cancelled")
	default:
	}

	request, err := http.NewRequestWithContext(fd.ctx, "GET", fd.Url, nil)
	if err != nil {
		return fmt.Errorf("create request failed: %w", err)
	}
	fd.setHeaders(request)

	if fd.IsMultiPart {
		rangeStart := task.rangeStart + task.downloadedSize
		rangeHeader := fmt.Sprintf("bytes=%d-%d", rangeStart, task.rangeEnd)
		request.Header.Set("Range", rangeHeader)
	}

	client := fd.buildClient()
	resp, err := client.Do(request)
	if err != nil {
		return fmt.Errorf("send request failed: %w", err)
	}
	defer resp.Body.Close()

	if fd.IsMultiPart && resp.StatusCode != http.StatusPartialContent {
		return fmt.Errorf("server does not support range requests, status: %d", resp.StatusCode)
	} else if !fd.IsMultiPart && resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	buf := make([]byte, 32*1024)
	for {
		select {
		case <-fd.ctx.Done():
			return fmt.Errorf("download cancelled")
		default:
		}

		n, err := resp.Body.Read(buf)
		if n > 0 {
			writeSize := int64(n)
			offset := task.rangeStart + task.downloadedSize
			_, writeErr := fd.File.WriteAt(buf[:writeSize], offset)
			if writeErr != nil {
				return fmt.Errorf("write file failed at offset %d: %w", offset, writeErr)
			}

			task.downloadedSize += writeSize
			progressChan <- ProgressChan{taskID: task.taskID, bytes: writeSize}

			if fd.TotalSize > 0 && task.rangeStart+task.downloadedSize-1 >= task.rangeEnd {
				return nil
			}
		}

		if err != nil {
			if err == io.EOF {
				return nil
			}
			return fmt.Errorf("read response failed: %w", err)
		}
	}
}

func (fd *FileDownloader) verifyDownload() error {
	for _, task := range fd.DownloadTaskList {
		if !task.isCompleted {
			return fmt.Errorf("task %d not completed", task.taskID)
		}
	}

	if fd.TotalSize > 0 {
		_, err := fd.File.Stat()
		if err != nil {
			return fmt.Errorf("get file info failed: %w", err)
		}
	}

	return nil
}

func (fd *FileDownloader) Start() error {
	if err := fd.init(); err != nil {
		return err
	}
	fd.createDownloadTasks()

	err := fd.startDownload()

	if fd.File != nil {
		fd.File.Close()
	}

	return err
}

func (fd *FileDownloader) Cancel() {
	if fd.cancelFunc != nil {
		fd.cancelFunc()
	}

	if fd.File != nil {
		fd.File.Close()
	}

	if fd.FileName != "" {
		_ = os.Remove(fd.FileName)
	}
}


================================================
FILE: core/http.go
================================================
package core

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"res-downloader/core/shared"
	"strings"

	"github.com/wailsapp/wails/v2/pkg/runtime"
)

type respData map[string]interface{}

type ResponseData struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data"`
}

type HttpServer struct{}

func initHttpServer() *HttpServer {
	if httpServerOnce == nil {
		httpServerOnce = &HttpServer{}
	}
	return httpServerOnce
}

func (h *HttpServer) run() {
	listener, err := net.Listen("tcp", globalConfig.Host+":"+globalConfig.Port)
	if err != nil {
		globalLogger.Err(err)
		log.Fatalf("Service cannot start: %v", err)
	}
	fmt.Println("Service started, listening http://" + globalConfig.Host + ":" + globalConfig.Port)
	if err1 := http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Host == "127.0.0.1:"+globalConfig.Port && HandleApi(w, r) {

		} else {
			proxyOnce.Proxy.ServeHTTP(w, r) // 代理
		}
	})); err1 != nil {
		globalLogger.Err(err1)
		fmt.Printf("Service startup exception: %v", err1)
	}
}

func (h *HttpServer) downCert(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/x-x509-ca-data")
	w.Header().Set("Content-Disposition", "attachment;filename=res-downloader-public.crt")
	w.Header().Set("Content-Transfer-Encoding", "binary")
	w.Header().Set("Content-Length", fmt.Sprintf("%d", len(appOnce.PublicCrt)))
	w.WriteHeader(http.StatusOK)
	io.Copy(w, io.NopCloser(bytes.NewReader(appOnce.PublicCrt)))
}

func (h *HttpServer) preview(w http.ResponseWriter, r *http.Request) {
	realURL := r.URL.Query().Get("url")
	if realURL == "" {
		http.Error(w, "Missing 'url' parameter", http.StatusBadRequest)
		return
	}
	realURL, _ = url.QueryUnescape(realURL)
	parsedURL, err := url.Parse(realURL)
	if err != nil {
		http.Error(w, "Invalid URL", http.StatusBadRequest)
		return
	}
	request, err := http.NewRequest("GET", parsedURL.String(), nil)
	if err != nil {
		http.Error(w, "Failed to fetch the resource", http.StatusInternalServerError)
		return
	}

	if rangeHeader := r.Header.Get("Range"); rangeHeader != "" {
		request.Header.Set("Range", rangeHeader)
	}

	resp, err := http.DefaultClient.Do(request)
	if err != nil {
		http.Error(w, "Failed to fetch the resource", http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	for k, v := range resp.Header {
		if strings.ToLower(k) == "access-control-allow-origin" {
			continue
		}
		for _, vv := range v {
			w.Header().Add(k, vv)
		}
	}
	w.WriteHeader(resp.StatusCode)

	_, err = io.Copy(w, resp.Body)
	if err != nil {
		http.Error(w, "Failed to serve the resource", http.StatusInternalServerError)
	}
	return
}

func (h *HttpServer) send(t string, data interface{}) {
	jsonData, err := json.Marshal(map[string]interface{}{
		"type": t,
		"data": data,
	})
	if err != nil {
		fmt.Println("Error converting map to JSON:", err)
		return
	}
	runtime.EventsEmit(appOnce.ctx, "event", string(jsonData))
}

func (h *HttpServer) writeJson(w http.ResponseWriter, data *ResponseData) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(200)
	err := json.NewEncoder(w).Encode(data)
	if err != nil {
		globalLogger.Err(err)
	}
}

func (h *HttpServer) error(w http.ResponseWriter, args ...interface{}) {
	message := "ok"
	var data interface{}

	if len(args) > 0 {
		message = args[0].(string)
	}
	if len(args) > 1 {
		data = args[1]
	}
	h.writeJson(w, h.buildResp(0, message, data))
}

func (h *HttpServer) success(w http.ResponseWriter, args ...interface{}) {
	message := "ok"
	var data interface{}

	if len(args) > 0 {
		data = args[0]
	}

	if len(args) > 1 {
		message = args[1].(string)
	}
	h.writeJson(w, h.buildResp(1, message, data))
}

func (h *HttpServer) buildResp(code int, message string, data interface{}) *ResponseData {
	return &ResponseData{
		Code:    code,
		Message: message,
		Data:    data,
	}
}

func (h *HttpServer) openDirectoryDialog(w http.ResponseWriter, r *http.Request) {
	folder, err := runtime.OpenDirectoryDialog(appOnce.ctx, runtime.OpenDialogOptions{
		DefaultDirectory: "",
		Title:            "Select a folder",
	})
	if err != nil {
		h.error(w, err.Error())
		return
	}
	h.success(w, respData{
		"folder": folder,
	})
}

func (h *HttpServer) openFileDialog(w http.ResponseWriter, r *http.Request) {
	filePath, err := runtime.OpenFileDialog(appOnce.ctx, runtime.OpenDialogOptions{
		Filters: []runtime.FileFilter{
			{
				DisplayName: "Videos (*.mov;*.mp4)",
				Pattern:     "*.mp4",
			},
		},
		Title: "Select a file",
	})
	if err != nil {
		h.error(w, err.Error())
		return
	}
	h.success(w, respData{
		"file": filePath,
	})
}

func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
	var data struct {
		FilePath string `json:"filePath"`
	}
	err := json.NewDecoder(r.Body).Decode(&data)
	if err == nil && data.FilePath == "" {
		return
	}

	err = shared.OpenFolder(data.FilePath)
	if err != nil {
		globalLogger.Err(err)
		h.error(w, err.Error())
		return
	}
	h.success(w)
	return
}

func (h *HttpServer) install(w http.ResponseWriter, r *http.Request) {
	if appOnce.isInstall() {
		h.success(w, respData{
			"isPass": systemOnce.Password == "",
		})
		return
	}

	out, err := appOnce.installCert()
	if err != nil {
		h.error(w, err.Error()+"\n"+out, respData{
			"isPass": systemOnce.Password == "",
		})
		return
	}

	h.success(w, respData{
		"isPass": systemOnce.Password == "",
	})
}

func (h *HttpServer) setSystemPassword(w http.ResponseWriter, r *http.Request) {
	var data struct {
		Password string `json:"password"`
		IsCache  bool   `json:"isCache"`
	}
	err := json.NewDecoder(r.Body).Decode(&data)
	if err != nil {
		h.error(w, err.Error())
		return
	}
	systemOnce.SetPassword(data.Password, data.IsCache)
	h.success(w)
}

func (h *HttpServer) openSystemProxy(w http.ResponseWriter, r *http.Request) {
	err := appOnce.OpenSystemProxy()
	if err != nil {
		h.error(w, err.Error(), respData{
			"value": appOnce.IsProxy,
		})
		return
	}
	h.success(w, respData{
		"value": appOnce.IsProxy,
	})
}

func (h *HttpServer) unsetSystemProxy(w http.ResponseWriter, r *http.Request) {
	err := appOnce.UnsetSystemProxy()
	if err != nil {
		h.error(w, err.Error(), respData{
			"value": appOnce.IsProxy,
		})
		return
	}
	h.success(w, respData{
		"value": appOnce.IsProxy,
	})
}

func (h *HttpServer) isProxy(w http.ResponseWriter, r *http.Request) {
	h.success(w, respData{
		"value": appOnce.IsProxy,
	})
}

func (h *HttpServer) appInfo(w http.ResponseWriter, r *http.Request) {
	h.success(w, appOnce)
}

func (h *HttpServer) getConfig(w http.ResponseWriter, r *http.Request) {
	h.success(w, globalConfig)
}

func (h *HttpServer) setConfig(w http.ResponseWriter, r *http.Request) {
	var data Config
	if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
		h.error(w, err.Error())
		return
	}
	globalConfig.setConfig(data)
	h.success(w)
}

func (h *HttpServer) setType(w http.ResponseWriter, r *http.Request) {
	var data struct {
		Type string `json:"type"`
	}
	err := json.NewDecoder(r.Body).Decode(&data)
	if err == nil {
		if data.Type != "" {
			resourceOnce.setResType(strings.Split(data.Type, ","))
		} else {
			resourceOnce.setResType([]string{})
		}
	}

	h.success(w)
}

func (h *HttpServer) clear(w http.ResponseWriter, r *http.Request) {
	resourceOnce.clear()
	h.success(w)
}

func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) {
	var data struct {
		Sign []string `json:"sign"`
	}
	err := json.NewDecoder(r.Body).Decode(&data)
	if err == nil && len(data.Sign) > 0 {
		for _, v := range data.Sign {
			resourceOnce.delete(v)
		}
	}
	h.success(w)
}

func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
	var data struct {
		shared.MediaInfo
		DecodeStr string `json:"decodeStr"`
	}
	if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
		h.error(w, err.Error())
		return
	}
	resourceOnce.download(data.MediaInfo, data.DecodeStr)
	h.success(w)
}

func (h *HttpServer) cancel(w http.ResponseWriter, r *http.Request) {
	var data struct {
		shared.MediaInfo
	}

	if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
		h.error(w, err.Error())
		return
	}

	err := resourceOnce.cancel(data.Id)
	if err != nil {
		h.error(w, err.Error())
		return
	}
	h.success(w)
}

func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) {
	var data struct {
		shared.MediaInfo
		Filename  string `json:"filename"`
		DecodeStr string `json:"decodeStr"`
	}
	if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
		h.error(w, err.Error())
		return
	}
	savePath, err := resourceOnce.wxFileDecode(data.MediaInfo, data.Filename, data.DecodeStr)
	if err != nil {
		h.error(w, err.Error())
		return
	}
	h.success(w, respData{
		"save_path": savePath,
	})
}

func (h *HttpServer) batchExport(w http.ResponseWriter, r *http.Request) {
	var data struct {
		Content string `json:"content"`
	}
	if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
		h.error(w, err.Error())
		return
	}
	fileName := filepath.Join(globalConfig.SaveDirectory, "res-downloader-"+shared.GetCurrentDateTimeFormatted()+".txt")
	err := os.WriteFile(fileName, []byte(data.Content), 0644)
	if err != nil {
		h.error(w, err.Error())
		return
	}

	_ = shared.OpenFolder(fileName)
	h.success(w, respData{
		"file_name": fileName,
	})
}


================================================
FILE: core/logger.go
================================================
package core

import (
	"fmt"
	"github.com/rs/zerolog"
	"io"
	"os"
	"path/filepath"
	"res-downloader/core/shared"
)

type Logger struct {
	zerolog.Logger
	logFile *os.File
}

func initLogger() *Logger {
	if globalLogger == nil {
		globalLogger = NewLogger(!shared.IsDevelopment(), filepath.Join(appOnce.UserDir, "logs", "app.log"))
	}
	return globalLogger
}

func (l *Logger) Close() {
	_ = l.logFile.Close()
}

func (l *Logger) Err(err error) {
	l.Error().Stack().Err(err)
}

func (l *Logger) Esg(err error, format string, v ...interface{}) {
	l.Error().Stack().Err(err).Msgf(fmt.Sprintf(format, v...))
}

// NewLogger create a new logger
func NewLogger(logFile bool, logPath string) *Logger {
	var out io.Writer
	if logFile {
		// log to file
		logDir := filepath.Dir(logPath)
		if err := shared.CreateDirIfNotExist(logDir); err != nil {
			panic(err)
		}
		var (
			logfile *os.File
			err     error
		)
		logfile, err = os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
		if err != nil {
			panic(err)
		}
		out = logfile
	} else {
		out = os.Stdout
	}

	logger := &Logger{}
	if logFile {
		logger.logFile = out.(*os.File)
	}
	logger.Logger = zerolog.New(zerolog.ConsoleWriter{
		NoColor:    true,
		Out:        out,
		TimeFormat: "2006-01-02 15:04:05",
	}).With().Timestamp().Logger()
	return logger
}


================================================
FILE: core/middleware.go
================================================
package core

import (
	"net/http"
	"strings"
)

func Middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if HandleApi(w, r) {
			return
		}
		next.ServeHTTP(w, r)
	})
}

func HandleApi(w http.ResponseWriter, r *http.Request) bool {
	if strings.HasPrefix(r.URL.Path, "/api") {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		if r.URL.Path != "/api/preview" {
			w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
		}
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusNoContent)
			return true
		}
		switch r.URL.Path {
		case "/api/install":
			httpServerOnce.install(w, r)
		case "/api/set-system-password":
			httpServerOnce.setSystemPassword(w, r)
		case "/api/preview":
			httpServerOnce.preview(w, r)
		case "/api/proxy-open":
			httpServerOnce.openSystemProxy(w, r)
		case "/api/proxy-unset":
			httpServerOnce.unsetSystemProxy(w, r)
		case "/api/open-directory":
			httpServerOnce.openDirectoryDialog(w, r)
		case "/api/open-file":
			httpServerOnce.openFileDialog(w, r)
		case "/api/open-folder":
			httpServerOnce.openFolder(w, r)
		case "/api/is-proxy":
			httpServerOnce.isProxy(w, r)
		case "/api/app-info":
			httpServerOnce.appInfo(w, r)
		case "/api/set-config":
			httpServerOnce.setConfig(w, r)
		case "/api/get-config":
			httpServerOnce.getConfig(w, r)
		case "/api/set-type":
			httpServerOnce.setType(w, r)
		case "/api/clear":
			httpServerOnce.clear(w, r)
		case "/api/delete":
			httpServerOnce.delete(w, r)
		case "/api/download":
			httpServerOnce.download(w, r)
		case "/api/cancel":
			httpServerOnce.cancel(w, r)
		case "/api/wx-file-decode":
			httpServerOnce.wxFileDecode(w, r)
		case "/api/batch-export":
			httpServerOnce.batchExport(w, r)
		case "/api/cert":
			httpServerOnce.downCert(w, r)
		}
		return true
	}
	return false
}


================================================
FILE: core/plugins/plugin.default.go
================================================
package plugins

import (
	"encoding/json"
	"github.com/elazarl/goproxy"
	gonanoid "github.com/matoous/go-nanoid/v2"
	"net/http"
	"path/filepath"
	"res-downloader/core/shared"
	"strconv"
	"strings"
)

type DefaultPlugin struct {
	bridge *shared.Bridge
}

func (p *DefaultPlugin) SetBridge(bridge *shared.Bridge) {
	p.bridge = bridge
}

func (p *DefaultPlugin) Domains() []string {
	return []string{"default"}
}

func (p *DefaultPlugin) OnRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
	return r, nil
}

func (p *DefaultPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
	if resp == nil || resp.Request == nil || (resp.StatusCode != 200 && resp.StatusCode != 206 && resp.StatusCode != 304) {
		return resp
	}

	classify, suffix := p.bridge.TypeSuffix(resp.Header.Get("Content-Type"))
	if classify == "" {
		return resp
	}

	rawUrl := resp.Request.URL.String()
	isAll, _ := p.bridge.GetResType("all")
	isClassify, _ := p.bridge.GetResType(classify)

	if suffix == "default" {
		ext := filepath.Ext(filepath.Base(strings.Split(strings.Split(rawUrl, "?")[0], "#")[0]))
		if ext != "" {
			suffix = ext
		}
	}

	urlSign := shared.Md5(rawUrl)
	if ok := p.bridge.MediaIsMarked(urlSign); !ok && (isAll || isClassify) {
		value, _ := strconv.ParseFloat(resp.Header.Get("content-length"), 64)
		id, err := gonanoid.New()
		if err != nil {
			id = urlSign
		}
		res := shared.MediaInfo{
			Id:          id,
			Url:         rawUrl,
			UrlSign:     urlSign,
			CoverUrl:    "",
			Size:        value,
			Domain:      shared.GetTopLevelDomain(rawUrl),
			Classify:    classify,
			Suffix:      suffix,
			Status:      shared.DownloadStatusReady,
			SavePath:    "",
			DecodeKey:   "",
			OtherData:   map[string]string{},
			Description: "",
			ContentType: resp.Header.Get("Content-Type"),
		}

		// Store entire request headers as JSON
		if headers, err := json.Marshal(resp.Request.Header); err == nil {
			res.OtherData["headers"] = string(headers)
		}

		p.bridge.MarkMedia(urlSign)
		go func(res shared.MediaInfo) {
			p.bridge.Send("newResources", res)
		}(res)
	}

	return resp
}


================================================
FILE: core/plugins/plugin.qq.com.go
================================================
package plugins

import (
	"bytes"
	"encoding/json"
	"fmt"
	"github.com/elazarl/goproxy"
	gonanoid "github.com/matoous/go-nanoid/v2"
	"io"
	"net/http"
	"regexp"
	"res-downloader/core/shared"
	"strconv"
	"strings"
)

var qqMediaRegex = regexp.MustCompile(`get\s*media\(\)\{`)
var qqCommentRegex = regexp.MustCompile(`async\s*finderGetCommentDetail\((\w+)\)\s*\{return(.*?)\s*}\s*async`)

type QqPlugin struct {
	bridge *shared.Bridge
}

func (p *QqPlugin) SetBridge(bridge *shared.Bridge) {
	p.bridge = bridge
}

func (p *QqPlugin) Domains() []string {
	return []string{"qq.com"}
}

func (p *QqPlugin) OnRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
	if strings.Contains(r.Host, "qq.com") && strings.Contains(r.URL.Path, "/res-downloader/wechat") {
		if p.bridge.GetConfig("WxAction").(bool) && r.URL.Query().Get("type") == "1" {
			return p.handleWechatRequest(r, ctx)
		} else if !p.bridge.GetConfig("WxAction").(bool) && r.URL.Query().Get("type") == "2" {
			return p.handleWechatRequest(r, ctx)
		} else {
			return r, p.buildEmptyResponse(r)
		}
	}

	return nil, nil
}

func (p *QqPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
	if resp.StatusCode != 200 && resp.StatusCode != 206 {
		return nil
	}

	host := resp.Request.Host
	Path := resp.Request.URL.Path

	classify, _ := p.bridge.TypeSuffix(resp.Header.Get("Content-Type"))
	if classify == "video" && strings.HasSuffix(host, "finder.video.qq.com") {
		if strings.Contains(resp.Request.Header.Get("Origin"), "mp.weixin.qq.com") {
			return nil
		}
		return resp
	}

	if strings.HasSuffix(host, "channels.weixin.qq.com") &&
		(strings.Contains(Path, "/web/pages/feed") || strings.Contains(Path, "/web/pages/home")) {
		return p.replaceWxJsContent(resp, ".js\"", ".js?v="+p.v()+"\"")
	}

	if strings.HasSuffix(host, "res.wx.qq.com") {
		respTemp := resp
		is := false
		if strings.HasSuffix(respTemp.Request.URL.RequestURI(), ".js?v="+p.v()) {
			respTemp = p.replaceWxJsContent(respTemp, ".js\"", ".js?v="+p.v()+"\"")
			is = true
		}

		if strings.Contains(Path, "web/web-finder/res/js/virtual_svg-icons-register.publish") {
			body, err := io.ReadAll(respTemp.Body)
			if err != nil {
				return respTemp
			}
			bodyStr := string(body)
			newBody := qqMediaRegex.
				ReplaceAllString(bodyStr, `
							get media(){
								if(this.objectDesc){
									fetch("https://wxapp.tc.qq.com/res-downloader/wechat?type=1", {
									  method: "POST",
									  mode: "no-cors",
									  body: JSON.stringify(this.objectDesc),
									});
								};
			
			`)

			newBody = qqCommentRegex.
				ReplaceAllString(newBody, `
							async finderGetCommentDetail($1) {
								var res = await$2;
								if (res?.data?.object?.objectDesc) {
									fetch("https://wxapp.tc.qq.com/res-downloader/wechat?type=2", {
									  method: "POST",
									  mode: "no-cors",
									  body: JSON.stringify(res.data.object.objectDesc),
									});
								}
								return res;
							}async
			`)
			newBodyBytes := []byte(newBody)
			respTemp.Body = io.NopCloser(bytes.NewBuffer(newBodyBytes))
			respTemp.ContentLength = int64(len(newBodyBytes))
			respTemp.Header.Set("Content-Length", fmt.Sprintf("%d", len(newBodyBytes)))
			return respTemp
		}
		if is {
			return respTemp
		}
	}

	return nil
}

func (p *QqPlugin) handleWechatRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		return r, p.buildEmptyResponse(r)
	}

	go p.handleMedia(body)

	return r, p.buildEmptyResponse(r)
}

func (p *QqPlugin) handleMedia(body []byte) {
	var result map[string]interface{}
	if err := json.Unmarshal(body, &result); err != nil {
		return
	}

	mediaArr, ok := result["media"].([]interface{})
	if !ok || len(mediaArr) == 0 {
		return
	}

	firstMedia, ok := mediaArr[0].(map[string]interface{})
	if !ok {
		return
	}

	rawUrl, ok := firstMedia["url"].(string)
	if !ok || rawUrl == "" {
		return
	}

	urlSign := shared.Md5(rawUrl)
	if p.bridge.MediaIsMarked(urlSign) {
		return
	}

	id, err := gonanoid.New()
	if err != nil {
		id = urlSign
	}

	res := shared.MediaInfo{
		Id:          id,
		Url:         rawUrl,
		UrlSign:     urlSign,
		CoverUrl:    "",
		Size:        0,
		Domain:      shared.GetTopLevelDomain(rawUrl),
		Classify:    "video",
		Suffix:      ".mp4",
		Status:      shared.DownloadStatusReady,
		SavePath:    "",
		DecodeKey:   "",
		OtherData:   map[string]string{},
		Description: "",
		ContentType: "video/mp4",
	}

	if mediaType, ok := firstMedia["mediaType"].(float64); ok && mediaType == 9 {
		res.Classify = "image"
		res.Suffix = ".png"
		res.ContentType = "image/png"
	}

	isAll, _ := p.bridge.GetResType("all")
	isImage, _ := p.bridge.GetResType("image")
	if res.Classify == "image" && !isImage && !isAll {
		return
	}

	isVideo, _ := p.bridge.GetResType("video")
	if res.Classify == "video" && !isVideo && !isAll {
		return
	}

	if urlToken, ok := firstMedia["urlToken"].(string); ok {
		res.Url += urlToken
	}

	switch size := firstMedia["fileSize"].(type) {
	case float64:
		res.Size = size
	case string:
		if value, err := strconv.ParseFloat(size, 64); err == nil {
			res.Size = value
		}
	}

	if coverUrl, ok := firstMedia["coverUrl"].(string); ok {
		res.CoverUrl = coverUrl
	}

	if decodeKey, ok := firstMedia["decodeKey"].(string); ok {
		res.DecodeKey = decodeKey
	}

	if desc, ok := result["description"].(string); ok {
		res.Description = desc
	}

	if spec, ok := firstMedia["spec"].([]interface{}); ok {
		var fileFormats []string
		for _, item := range spec {
			if m, ok := item.(map[string]interface{}); ok {
				if format, ok := m["fileFormat"].(string); ok {
					fileFormats = append(fileFormats, format)
				}
			}
		}
		res.OtherData["wx_file_formats"] = strings.Join(fileFormats, "#")
	}

	p.bridge.MarkMedia(urlSign)

	go func(res shared.MediaInfo) {
		p.bridge.Send("newResources", res)
	}(res)
}

func (p *QqPlugin) buildEmptyResponse(r *http.Request) *http.Response {
	body := "The content does not exist"
	resp := &http.Response{
		Status:        http.StatusText(http.StatusOK),
		StatusCode:    http.StatusOK,
		Header:        make(http.Header),
		Body:          io.NopCloser(strings.NewReader(body)),
		ContentLength: int64(len(body)),
		Request:       r,
	}
	resp.Header.Set("Content-Type", "text/plain; charset=utf-8")
	return resp
}

func (p *QqPlugin) replaceWxJsContent(resp *http.Response, old, new string) *http.Response {
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return resp
	}
	bodyString := string(body)
	newBodyString := strings.ReplaceAll(bodyString, old, new)
	newBodyBytes := []byte(newBodyString)
	resp.Body = io.NopCloser(bytes.NewBuffer(newBodyBytes))
	resp.ContentLength = int64(len(newBodyBytes))
	resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(newBodyBytes)))
	return resp
}

func (p *QqPlugin) v() string {
	return p.bridge.GetVersion()
}


================================================
FILE: core/proxy.go
================================================
package core

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"net"
	"net/http"
	"net/url"
	"res-downloader/core/plugins"
	"res-downloader/core/shared"
	"strings"
	"time"

	"github.com/elazarl/goproxy"
)

type Proxy struct {
	ctx   context.Context
	Proxy *goproxy.ProxyHttpServer
	Is    bool
}

var pluginRegistry = make(map[string]shared.Plugin)

func init() {
	ps := []shared.Plugin{
		&plugins.QqPlugin{},
		&plugins.DefaultPlugin{},
	}

	bridge := &shared.Bridge{
		GetVersion: func() string {
			return appOnce.Version
		},
		GetResType: func(key string) (bool, bool) {
			return resourceOnce.getResType(key)
		},
		TypeSuffix: func(mine string) (string, string) {
			return globalConfig.typeSuffix(mine)
		},
		MediaIsMarked: func(key string) bool {
			return resourceOnce.mediaIsMarked(key)
		},
		MarkMedia: func(key string) {
			resourceOnce.markMedia(key)
		},
		GetConfig: func(key string) interface{} {
			return globalConfig.getConfig(key)
		},
		Send: func(t string, data interface{}) {
			httpServerOnce.send(t, data)
		},
	}

	for _, p := range ps {
		p.SetBridge(bridge)
		for _, domain := range p.Domains() {
			pluginRegistry[domain] = p
		}
	}
}

func initProxy() *Proxy {
	if proxyOnce == nil {
		proxyOnce = &Proxy{}
		proxyOnce.Startup()
	}
	return proxyOnce
}

func (p *Proxy) Startup() {
	err := p.setCa()
	if err != nil {
		DialogErr("Failed to start proxy service:" + err.Error())
		return
	}

	p.Proxy = goproxy.NewProxyHttpServer()
	//p.Proxy.KeepDestinationHeaders = true
	//p.Proxy.Verbose = false
	p.setTransport()
	//p.Proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
	p.Proxy.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
		if ruleOnce.shouldMitm(host) {
			return goproxy.MitmConnect, host
		}
		return goproxy.OkConnect, host
	})

	p.Proxy.OnRequest().DoFunc(p.httpRequestEvent)
	p.Proxy.OnResponse().DoFunc(p.httpResponseEvent)
}

func (p *Proxy) setCa() error {
	ca, err := tls.X509KeyPair(appOnce.PublicCrt, appOnce.PrivateKey)
	if err != nil {
		return err
	}
	if ca.Leaf, err = x509.ParseCertificate(ca.Certificate[0]); err != nil {
		return err
	}
	goproxy.GoproxyCa = ca
	goproxy.OkConnect = &goproxy.ConnectAction{Action: goproxy.ConnectAccept, TLSConfig: goproxy.TLSConfigFromCA(&ca)}
	goproxy.MitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(&ca)}
	goproxy.HTTPMitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectHTTPMitm, TLSConfig: goproxy.TLSConfigFromCA(&ca)}
	goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: goproxy.TLSConfigFromCA(&ca)}
	return nil
}

func (p *Proxy) setTransport() {
	transport := &http.Transport{
		DisableKeepAlives: false,
		// MaxIdleConnsPerHost: 10,
		DialContext: (&net.Dialer{
			Timeout: 60 * time.Second,
		}).DialContext,
		TLSHandshakeTimeout:   60 * time.Second,
		ResponseHeaderTimeout: 60 * time.Second,
		IdleConnTimeout:       30 * time.Second,
	}

	p.Proxy.ConnectDial = nil
	p.Proxy.ConnectDialWithReq = nil

	if globalConfig.UpstreamProxy != "" && globalConfig.OpenProxy && !strings.Contains(globalConfig.UpstreamProxy, globalConfig.Port) {
		proxyURL, err := url.Parse(globalConfig.UpstreamProxy)
		if err == nil {
			transport.Proxy = http.ProxyURL(proxyURL)
			p.Proxy.ConnectDial = p.Proxy.NewConnectDialToProxy(globalConfig.UpstreamProxy)
		}
	}
	p.Proxy.Tr = transport
}

func (p *Proxy) matchPlugin(host string) shared.Plugin {
	domain := shared.GetTopLevelDomain(host)
	if plugin, ok := pluginRegistry[domain]; ok {
		return plugin
	}
	return nil
}

func (p *Proxy) httpRequestEvent(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
	plugin := p.matchPlugin(r.Host)
	if plugin != nil {
		newReq, newResp := plugin.OnRequest(r, ctx)
		if newResp != nil {
			return newReq, newResp
		}

		if newReq != nil {
			return newReq, nil
		}
	}
	return pluginRegistry["default"].OnRequest(r, ctx)
}

func (p *Proxy) httpResponseEvent(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
	if resp == nil || resp.Request == nil {
		return resp
	}

	plugin := p.matchPlugin(resp.Request.Host)
	if plugin != nil {
		newResp := plugin.OnResponse(resp, ctx)
		if newResp != nil {
			return newResp
		}
	}

	return pluginRegistry["default"].OnResponse(resp, ctx)
}


================================================
FILE: core/resource.go
================================================
package core

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"res-downloader/core/shared"
	"strconv"
	"strings"
	"sync"
)

type WxFileDecodeResult struct {
	SavePath string
	Message  string
}

type Resource struct {
	mediaMark  sync.Map
	tasks      sync.Map
	resType    map[string]bool
	resTypeMux sync.RWMutex
}

func initResource() *Resource {
	if resourceOnce == nil {
		resourceOnce = &Resource{}
		resourceOnce.resType = resourceOnce.buildResType(globalConfig.MimeMap)
	}
	return resourceOnce
}

func (r *Resource) buildResType(mime map[string]MimeInfo) map[string]bool {
	t := map[string]bool{
		"all": true,
	}

	for _, item := range mime {
		if _, ok := t[item.Type]; !ok {
			t[item.Type] = true
		}
	}

	return t
}

func (r *Resource) mediaIsMarked(key string) bool {
	_, loaded := r.mediaMark.Load(key)
	return loaded
}

func (r *Resource) markMedia(key string) {
	r.mediaMark.Store(key, true)
}

func (r *Resource) getResType(key string) (bool, bool) {
	r.resTypeMux.RLock()
	value, ok := r.resType[key]
	r.resTypeMux.RUnlock()
	return value, ok
}

func (r *Resource) setResType(n []string) {
	r.resTypeMux.Lock()
	for key := range r.resType {
		r.resType[key] = false
	}

	for _, value := range n {
		if _, ok := r.resType[value]; ok {
			r.resType[value] = true
		}
	}
	r.resTypeMux.Unlock()
}

func (r *Resource) clear() {
	r.mediaMark.Clear()
}

func (r *Resource) delete(sign string) {
	r.mediaMark.Delete(sign)
}

func (r *Resource) cancel(id string) error {
	if d, ok := r.tasks.Load(id); ok {
		d.(*FileDownloader).Cancel()
		r.tasks.Delete(id) // 可选:取消后清理
		return nil
	}
	return errors.New("task not found")
}

func (r *Resource) download(mediaInfo shared.MediaInfo, decodeStr string) {
	if globalConfig.SaveDirectory == "" {
		return
	}
	go func(mediaInfo shared.MediaInfo) {
		rawUrl := mediaInfo.Url
		fileName := shared.Md5(rawUrl)

		if v := shared.GetFileNameFromURL(rawUrl); v != "" {
			fileName = v
		}

		if mediaInfo.Description != "" {
			fileName = regexp.MustCompile(`[^\w\p{Han}]`).ReplaceAllString(mediaInfo.Description, "")
			fileLen := globalConfig.FilenameLen
			if fileLen <= 0 {
				fileLen = 10
			}

			runes := []rune(fileName)
			if len(runes) > fileLen {
				fileName = string(runes[:fileLen])
			}
		}

		if globalConfig.FilenameTime {
			mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+"_"+shared.GetCurrentDateTimeFormatted())
		} else {
			mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName)
		}

		if !strings.HasSuffix(mediaInfo.SavePath, mediaInfo.Suffix) {
			mediaInfo.SavePath = mediaInfo.SavePath + mediaInfo.Suffix
		}

		if strings.Contains(rawUrl, "qq.com") {
			if globalConfig.Quality == 1 &&
				strings.Contains(rawUrl, "encfilekey=") &&
				strings.Contains(rawUrl, "token=") {
				parseUrl, err := url.Parse(rawUrl)
				queryParams := parseUrl.Query()
				if err == nil && queryParams.Has("encfilekey") && queryParams.Has("token") {
					rawUrl = parseUrl.Scheme + "://" + parseUrl.Host + "/" + parseUrl.Path +
						"?encfilekey=" + queryParams.Get("encfilekey") +
						"&token=" + queryParams.Get("token")
				}
			} else if globalConfig.Quality > 1 && mediaInfo.OtherData["wx_file_formats"] != "" {
				format := strings.Split(mediaInfo.OtherData["wx_file_formats"], "#")
				qualityMap := []string{
					format[0],
					format[len(format)/2],
					format[len(format)-1],
				}
				rawUrl += "&X-snsvideoflag=" + qualityMap[globalConfig.Quality-2]
			}
		}

		headers, _ := r.parseHeaders(mediaInfo)

		downloader := NewFileDownloader(rawUrl, mediaInfo.SavePath, globalConfig.TaskNumber, headers)
		downloader.progressCallback = func(totalDownloaded, totalSize float64, taskID int, taskProgress float64) {
			r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded*100/totalSize))+"%", shared.DownloadStatusRunning)
		}
		r.tasks.Store(mediaInfo.Id, downloader)
		err := downloader.Start()
		mediaInfo.SavePath = downloader.FileName
		if err != nil {
			if !strings.Contains(err.Error(), "cancelled") {
				r.progressEventsEmit(mediaInfo, err.Error())
			}
			return
		}
		if decodeStr != "" {
			r.progressEventsEmit(mediaInfo, "decrypting in progress", shared.DownloadStatusRunning)
			if err := r.decodeWxFile(mediaInfo.SavePath, decodeStr); err != nil {
				r.progressEventsEmit(mediaInfo, "decryption error: "+err.Error())
				return
			}
		}
		r.progressEventsEmit(mediaInfo, "complete", shared.DownloadStatusDone)
	}(mediaInfo)
}

func (r *Resource) parseHeaders(mediaInfo shared.MediaInfo) (map[string]string, error) {
	headers := make(map[string]string)

	if hh, ok := mediaInfo.OtherData["headers"]; ok {
		var tempHeaders map[string][]string
		if err := json.Unmarshal([]byte(hh), &tempHeaders); err != nil {
			return headers, fmt.Errorf("parse headers JSON err: %v", err)
		}

		for key, values := range tempHeaders {
			if len(values) > 0 {
				headers[key] = values[0]
			}
		}
	}

	return headers, nil
}

func (r *Resource) wxFileDecode(mediaInfo shared.MediaInfo, fileName, decodeStr string) (string, error) {
	sourceFile, err := os.Open(fileName)
	if err != nil {
		return "", err
	}
	defer sourceFile.Close()
	mediaInfo.SavePath = strings.ReplaceAll(fileName, ".mp4", "_decrypt.mp4")

	destinationFile, err := os.Create(mediaInfo.SavePath)
	if err != nil {
		return "", err
	}
	defer destinationFile.Close()

	_, err = io.Copy(destinationFile, sourceFile)
	if err != nil {
		return "", err
	}
	err = r.decodeWxFile(mediaInfo.SavePath, decodeStr)
	if err != nil {
		return "", err
	}
	return mediaInfo.SavePath, nil
}

func (r *Resource) progressEventsEmit(mediaInfo shared.MediaInfo, args ...string) {
	Status := shared.DownloadStatusError
	Message := "ok"

	if len(args) > 0 {
		Message = args[0]
	}
	if len(args) > 1 {
		Status = args[1]
	}

	httpServerOnce.send("downloadProgress", map[string]interface{}{
		"Id":       mediaInfo.Id,
		"Status":   Status,
		"SavePath": mediaInfo.SavePath,
		"Message":  Message,
	})
	return
}

func (r *Resource) decodeWxFile(fileName, decodeStr string) error {
	decodedBytes, err := base64.StdEncoding.DecodeString(decodeStr)
	if err != nil {
		return err
	}
	file, err := os.OpenFile(fileName, os.O_RDWR, 0644)
	if err != nil {
		return err
	}
	defer file.Close()

	byteCount := len(decodedBytes)
	fileBytes := make([]byte, byteCount)
	n, err := file.Read(fileBytes)
	if err != nil && err != io.EOF {
		return err
	}

	if n < byteCount {
		byteCount = n
	}

	xorResult := make([]byte, byteCount)
	for i := 0; i < byteCount; i++ {
		xorResult[i] = decodedBytes[i] ^ fileBytes[i]
	}
	_, err = file.Seek(0, 0)
	if err != nil {
		return err
	}

	_, err = file.Write(xorResult)
	if err != nil {
		return err
	}
	return nil
}


================================================
FILE: core/rule.go
================================================
package core

import (
	"bufio"
	"net"
	"strings"
	"sync"
)

type Rule struct {
	raw        string
	isNeg      bool // 是否否定规则(以 ! 开头)
	isWildcard bool // 是否为 *.domain 形式
	isAll      bool
	domain     string // 域名部分,不含 "*."
}

type RuleSet struct {
	mu    sync.RWMutex
	rules []Rule
}

func initRule() *RuleSet {
	if ruleOnce == nil {
		ruleOnce = &RuleSet{}
		err := ruleOnce.Load(globalConfig.Rule)
		if err != nil {
			globalLogger.Esg(err, "init rule failed")
			return nil
		}
	}
	return ruleOnce
}

func (r *RuleSet) Load(rs string) error {
	reader := strings.NewReader(rs)
	scanner := bufio.NewScanner(reader)

	var rules []Rule
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}

		isNeg := false
		if strings.HasPrefix(line, "!") {
			isNeg = true
			line = strings.TrimSpace(line[1:])
			if line == "" {
				continue
			}
		}

		if line == "*" {
			rules = append(rules, Rule{
				raw:   "*",
				isAll: true,
				isNeg: isNeg,
			})
			continue
		}

		isWildcard := false
		domain := line
		if strings.HasPrefix(line, "*.") {
			isWildcard = true
			domain = line[2:]
		}

		rules = append(rules, Rule{
			raw:        line,
			isNeg:      isNeg,
			isWildcard: isWildcard,
			domain:     strings.ToLower(domain),
		})
	}

	if err := scanner.Err(); err != nil {
		return err
	}

	r.mu.Lock()
	r.rules = rules
	r.mu.Unlock()
	return nil
}

// shouldMitm: 根据当前规则集判断是否对 host 做 MITM
// host 可能带端口(example.com:443),函数会只匹配 hostname 部分
// 返回 true => MITM(解密),false => 透传
func (r *RuleSet) shouldMitm(host string) bool {
	h := host
	if strings.HasPrefix(h, "[") {
		if hostSplitIdx := strings.LastIndex(h, "]"); hostSplitIdx != -1 {
			h = h[:hostSplitIdx+1]
		}
	}
	if hp, _, err := net.SplitHostPort(host); err == nil {
		h = hp
	}
	h = strings.ToLower(strings.Trim(h, "[]"))

	r.mu.RLock()
	defer r.mu.RUnlock()

	action := false
	for _, rule := range r.rules {
		if rule.isAll {
			action = !rule.isNeg
			continue
		}

		if rule.isWildcard {
			if h == rule.domain || strings.HasSuffix(h, "."+rule.domain) {
				action = !rule.isNeg
			}
			continue
		}

		if h == rule.domain {
			action = !rule.isNeg
		}
	}
	return action
}


================================================
FILE: core/shared/base.go
================================================
package shared

type MediaInfo struct {
	Id          string
	Url         string
	UrlSign     string
	CoverUrl    string
	Size        float64
	Domain      string
	Classify    string
	Suffix      string
	SavePath    string
	Status      string
	DecodeKey   string
	Description string
	ContentType string
	OtherData   map[string]string
}


================================================
FILE: core/shared/const.go
================================================
package shared

const (
	DownloadStatusReady   string = "ready" // task create but not start
	DownloadStatusRunning string = "running"
	DownloadStatusError   string = "error"
	DownloadStatusDone    string = "done"
	DownloadStatusHandle  string = "handle"
)


================================================
FILE: core/shared/plugin.go
================================================
package shared

import (
	"github.com/elazarl/goproxy"
	"net/http"
)

type Bridge struct {
	GetVersion    func() string
	GetResType    func(key string) (bool, bool)
	TypeSuffix    func(mime string) (string, string)
	MediaIsMarked func(key string) bool
	MarkMedia     func(key string)
	GetConfig     func(key string) interface{}
	Send          func(t string, data interface{})
}

type Plugin interface {
	SetBridge(*Bridge)
	Domains() []string
	OnRequest(*http.Request, *goproxy.ProxyCtx) (*http.Request, *http.Response)
	OnResponse(*http.Response, *goproxy.ProxyCtx) *http.Response
}


================================================
FILE: core/shared/utils.go
================================================
package shared

import (
	"crypto/md5"
	"encoding/hex"
	"errors"
	"fmt"
	"golang.org/x/net/publicsuffix"
	"net/url"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"regexp"
	sysRuntime "runtime"
	"strings"
	"time"
)

func Md5(data string) string {
	hashNew := md5.New()
	hashNew.Write([]byte(data))
	hash := hashNew.Sum(nil)
	return hex.EncodeToString(hash)
}

func FormatSize(size float64) string {
	if size > 1048576 {
		return fmt.Sprintf("%.2fMB", float64(size)/1048576)
	}
	if size > 1024 {
		return fmt.Sprintf("%.2fKB", float64(size)/1024)
	}
	return fmt.Sprintf("%.0fb", size)
}

func GetTopLevelDomain(rawURL string) string {
	u, err := url.Parse(rawURL)
	if err == nil && u.Host != "" {
		rawURL = u.Host
	}
	domain, err := publicsuffix.EffectiveTLDPlusOne(rawURL)
	if err != nil {
		return rawURL
	}
	return domain
}

func FileExist(file string) bool {
	info, err := os.Stat(file)
	if err != nil {
		return false
	}
	return !info.IsDir()
}

func CreateDirIfNotExist(dir string) error {
	if _, err := os.Stat(dir); os.IsNotExist(err) {
		return os.MkdirAll(dir, 0750)
	}
	return nil
}

func IsDevelopment() bool {
	return os.Getenv("APP_ENV") == "development"
}

func GetFileNameFromURL(rawUrl string) string {
	parsedURL, err := url.Parse(rawUrl)
	if err != nil {
		return ""
	}

	fileName := path.Base(parsedURL.Path)
	if fileName == "" || fileName == "/" {
		return ""
	}

	if decoded, err := url.QueryUnescape(fileName); err == nil {
		fileName = decoded
	}

	re := regexp.MustCompile(`[<>:"/\\|?*]`)
	fileName = re.ReplaceAllString(fileName, "_")

	fileName = strings.TrimRightFunc(fileName, func(r rune) bool {
		return r == '.' || r == ' '
	})

	const maxFileNameLen = 255
	runes := []rune(fileName)
	if len(runes) > maxFileNameLen {
		ext := path.Ext(fileName)
		name := strings.TrimSuffix(fileName, ext)

		runes = []rune(name)
		if len(runes) > maxFileNameLen-len(ext) {
			runes = runes[:maxFileNameLen-len(ext)]
		}
		name = string(runes)
		fileName = name + ext
	}

	return fileName
}

func GetCurrentDateTimeFormatted() string {
	now := time.Now()
	return fmt.Sprintf("%04d%02d%02d%02d%02d%02d",
		now.Year(),
		now.Month(),
		now.Day(),
		now.Hour(),
		now.Minute(),
		now.Second())
}

func GetUniqueFileName(filePath string) string {
	if !FileExist(filePath) {
		return filePath
	}

	ext := filepath.Ext(filePath)
	baseName := strings.TrimSuffix(filePath, ext)
	count := 1

	for {
		newFileName := fmt.Sprintf("%s(%d)%s", baseName, count, ext)
		if !FileExist(newFileName) {
			return newFileName
		}
		count++
	}
}

func OpenFolder(filePath string) error {
	var cmd *exec.Cmd

	switch sysRuntime.GOOS {
	case "darwin":
		cmd = exec.Command("open", "-R", filePath)
	case "windows":
		cmd = exec.Command("explorer", "/select,", filePath)
	case "linux":
		cmd = exec.Command("nautilus", filePath)
		if err := cmd.Start(); err != nil {
			cmd = exec.Command("thunar", filePath)
			if err := cmd.Start(); err != nil {
				cmd = exec.Command("dolphin", filePath)
				if err := cmd.Start(); err != nil {
					cmd = exec.Command("pcmanfm", filePath)
					if err := cmd.Start(); err != nil {
						return err
					}
				}
			}
		}
	default:
		return errors.New("unsupported platform")
	}

	return cmd.Start()
}


================================================
FILE: core/storage.go
================================================
package core

import (
	"os"
	"path"
	"res-downloader/core/shared"
)

type Storage struct {
	fileName string
	def      []byte
}

func NewStorage(filename string, def []byte) *Storage {
	return &Storage{
		fileName: path.Join(appOnce.UserDir, filename),
		def:      def,
	}
}

func (l *Storage) Load() ([]byte, error) {
	if !shared.FileExist(l.fileName) {
		err := os.WriteFile(l.fileName, l.def, 0644)
		if err != nil {
			return nil, err
		}
		return l.def, nil
	}
	d, err := os.ReadFile(l.fileName)
	if err != nil {
		return nil, err
	}
	return d, err
}

func (l *Storage) Store(data []byte) error {
	if err := os.WriteFile(l.fileName, data, 0644); err != nil {
		return err
	}
	return nil
}


================================================
FILE: core/system.go
================================================
package core

import (
	"fmt"
	"os"
	"path/filepath"
	"time"
)

type SystemSetup struct {
	CertFile  string
	CacheFile string
	Password  string
	aesCipher *AESCipher
}

func initSystem() *SystemSetup {
	if systemOnce == nil {
		systemOnce = &SystemSetup{
			aesCipher: NewAESCipher("resd48w2d7er95627d447c490a8f02ff"),
			CertFile:  filepath.Join(appOnce.UserDir, "cert.crt"),
			CacheFile: filepath.Join(appOnce.UserDir, "pass.cache"),
		}
		systemOnce.checkPasswordFile()
	}
	return systemOnce
}

func (s *SystemSetup) initCert() ([]byte, error) {
	content, err := os.ReadFile(s.CertFile)
	if err == nil {
		return content, nil
	}
	if os.IsNotExist(err) {
		err = os.WriteFile(s.CertFile, appOnce.PublicCrt, 0750)
		if err != nil {
			return nil, err
		}
		return appOnce.PublicCrt, nil
	} else {
		return nil, err
	}
}

func (s *SystemSetup) SetPassword(password string, isCache bool) {
	s.Password = password
	if isCache {
		encrypted, err := s.aesCipher.Encrypt(password)
		if err == nil {
			err1 := os.WriteFile(s.CacheFile, []byte(encrypted), 0750)
			if err1 != nil {
				fmt.Println("Failed to write password: ", err1.Error())
			}
		} else {
			fmt.Println("Failed to Encrypt password: ", err.Error())
		}
	}
}

func (s *SystemSetup) checkPasswordFile() {
	fileInfo, err := os.Stat(s.CacheFile)
	if err != nil {
		return
	}

	lastModified := fileInfo.ModTime()
	oneMonthAgo := time.Now().AddDate(0, -1, 0)
	if lastModified.Before(oneMonthAgo) {
		os.Remove(s.CacheFile)
		return
	}

	content, err := os.ReadFile(s.CacheFile)
	if err != nil {
		return
	}

	password, err := s.aesCipher.Decrypt(string(content))
	if err != nil {
		return
	}
	s.Password = password
}


================================================
FILE: core/system_darwin.go
================================================
//go:build darwin

package core

import (
	"bytes"
	"fmt"
	"os/exec"
	"strings"
)

func (s *SystemSetup) runCommand(args []string) ([]byte, error) {
	if len(args) == 0 {
		return nil, fmt.Errorf("no command provided")
	}

	var cmd *exec.Cmd
	if s.Password != "" {
		cmd = exec.Command("sudo", append([]string{"-S"}, args...)...)
		cmd.Stdin = bytes.NewReader([]byte(s.Password + "\n"))
	} else {
		cmd = exec.Command(args[0], args[1:]...)
	}

	output, err := cmd.CombinedOutput()
	return output, err
}

func (s *SystemSetup) getNetworkServices() ([]string, error) {
	output, err := s.runCommand([]string{"networksetup", "-listallnetworkservices"})
	if err != nil {
		return nil, fmt.Errorf("failed to execute command: %v", err)
	}

	services := strings.Split(string(output), "\n")
	var activeServices []string
	for _, service := range services {
		service = strings.TrimSpace(service)
		if service == "" || strings.Contains(service, "*") || strings.Contains(service, "Serial Port") {
			continue
		}

		infoOutput, err := s.runCommand([]string{"networksetup", "-getinfo", service})
		if err != nil {
			fmt.Printf("failed to get info for service %s: %v\n", service, err)
			continue
		}

		if strings.Contains(string(infoOutput), "IP address:") {
			activeServices = append(activeServices, service)
		}
	}

	if len(activeServices) == 0 {
		return nil, fmt.Errorf("no active network services found")
	}

	return activeServices, nil
}

func (s *SystemSetup) setProxy() error {
	services, err := s.getNetworkServices()
	if err != nil {
		return err
	}

	isSuccess := false
	var errs strings.Builder
	for _, serviceName := range services {
		commands := [][]string{
			{"networksetup", "-setwebproxy", serviceName, "127.0.0.1", globalConfig.Port},
			{"networksetup", "-setsecurewebproxy", serviceName, "127.0.0.1", globalConfig.Port},
		}
		for _, cmd := range commands {
			if output, err := s.runCommand(cmd); err != nil {
				errs.WriteString(fmt.Sprintf("cmd: %v\noutput: %s\nerr: %s\n", cmd, output, err))
			} else {
				isSuccess = true
			}
		}
	}

	if isSuccess {
		return nil
	}

	return fmt.Errorf("failed to set proxy for any active network service, errs:%s", errs)
}

func (s *SystemSetup) unsetProxy() error {
	services, err := s.getNetworkServices()
	if err != nil {
		return err
	}

	isSuccess := false
	var errs strings.Builder
	for _, serviceName := range services {
		commands := [][]string{
			{"networksetup", "-setwebproxystate", serviceName, "off"},
			{"networksetup", "-setsecurewebproxystate", serviceName, "off"},
		}
		for _, cmd := range commands {
			if output, err := s.runCommand(cmd); err != nil {
				errs.WriteString(fmt.Sprintf("cmd: %v\noutput: %s\nerr: %s\n", cmd, output, err))
			} else {
				isSuccess = true
			}
		}
	}

	if isSuccess {
		return nil
	}

	return fmt.Errorf("failed to unset proxy for any active network service, errs:%s", errs)
}

func (s *SystemSetup) installCert() (string, error) {
	_, err := s.initCert()
	if err != nil {
		return "", err
	}
	output, err := s.runCommand([]string{"security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", s.CertFile})
	if err != nil {
		return string(output), err
	}
	return "", nil
}


================================================
FILE: core/system_linux.go
================================================
//go:build linux

package core

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"strings"
)

func (s *SystemSetup) getLinuxDistro() (string, error) {
	data, err := os.ReadFile("/etc/os-release")
	if err != nil {
		return "", err
	}
	for _, line := range strings.Split(string(data), "\n") {
		if strings.HasPrefix(line, "ID=") {
			return strings.Trim(strings.TrimPrefix(line, "ID="), "\""), nil
		}
	}
	return "", fmt.Errorf("could not determine linux distribution")
}

func (s *SystemSetup) runCommand(args []string, sudo bool) ([]byte, error) {
	if len(args) == 0 {
		return nil, fmt.Errorf("no command provided")
	}

	var cmd *exec.Cmd
	if s.Password != "" && sudo {
		cmd = exec.Command("sudo", append([]string{"-S"}, args...)...)
		cmd.Stdin = bytes.NewReader([]byte(s.Password + "\n"))
	} else {
		cmd = exec.Command(args[0], args[1:]...)
	}

	output, err := cmd.CombinedOutput()
	return output, err
}

func (s *SystemSetup) setProxy() error {
	commands := [][]string{
		{"gsettings", "set", "org.gnome.system.proxy", "mode", "manual"},
		{"gsettings", "set", "org.gnome.system.proxy.http", "host", "127.0.0.1"},
		{"gsettings", "set", "org.gnome.system.proxy.http", "port", globalConfig.Port},
		{"gsettings", "set", "org.gnome.system.proxy.https", "host", "127.0.0.1"},
		{"gsettings", "set", "org.gnome.system.proxy.https", "port", globalConfig.Port},
	}

	isSuccess := false
	var errs strings.Builder

	for _, cmd := range commands {
		if output, err := s.runCommand(cmd, false); err != nil {
			errs.WriteString(fmt.Sprintf("cmd: %v\noutput: %s\nerr: %s\n", cmd, output, err))
		} else {
			isSuccess = true
		}
	}

	if isSuccess {
		return nil
	}

	return fmt.Errorf("failed to set proxy:\n%s", errs.String())
}

func (s *SystemSetup) unsetProxy() error {
	cmd := []string{"gsettings", "set", "org.gnome.system.proxy", "mode", "none"}
	output, err := s.runCommand(cmd, false)
	if err != nil {
		return fmt.Errorf("failed to unset proxy: %s\noutput: %s", err.Error(), string(output))
	}
	return nil
}

func (s *SystemSetup) installCert() (string, error) {
	_, err := s.initCert()
	if err != nil {
		return "", err
	}

	distro, err := s.getLinuxDistro()
	if err != nil {
		return "", fmt.Errorf("detect distro failed: %w", err)
	}

	certName := appOnce.AppName + ".crt"
	var certPath string
	var updateCmd = []string{"update-ca-certificates"}

	switch distro {
	case "deepin":
		certDir := "/usr/share/ca-certificates/" + appOnce.AppName
		certPath = certDir + "/" + certName
		s.runCommand([]string{"mkdir", "-p", certDir}, true)
	case "arch":
		certPath = "/usr/share/ca-certificates/trust-source/" + certName
		updateCmd = []string{"update-ca-trust"}
	default:
		certPath = "/usr/local/share/ca-certificates/" + certName
	}

	var outs, errs strings.Builder
	isSuccess := false

	if output, err := s.runCommand([]string{"cp", "-f", s.CertFile, certPath}, true); err != nil {
		errs.WriteString(fmt.Sprintf("copy cert failed: %s\n%s\n", err.Error(), output))
	} else {
		isSuccess = true
		outs.Write(output)
	}

	if distro == "deepin" {
		confPath := "/etc/ca-certificates.conf"
		checkCmd := []string{"grep", "-qxF", certName, confPath}
		if _, err := s.runCommand(checkCmd, true); err != nil {
			echoCmd := []string{"bash", "-c", fmt.Sprintf("echo '%s/%s' >> %s", appOnce.AppName, certName, confPath)}
			if output, err := s.runCommand(echoCmd, true); err != nil {
				errs.WriteString(fmt.Sprintf("append conf failed: %s\n%s\n", err.Error(), output))
			} else {
				isSuccess = true
				outs.Write(output)
			}
		}
	}

	if output, err := s.runCommand(updateCmd, true); err != nil {
		errs.WriteString(fmt.Sprintf("update failed: %s\n%s\n", err.Error(), output))
	} else {
		isSuccess = true
		outs.Write(output)
	}

	if isSuccess {
		return "", nil
	}

	return outs.String(), fmt.Errorf("certificate installation failed:\n%s", errs.String())
}


================================================
FILE: core/system_windows.go
================================================
//go:build windows

package core

import (
	"crypto/x509"
	"encoding/pem"
	"errors"
	"golang.org/x/sys/windows"
	"golang.org/x/sys/windows/registry"
	"unsafe"
)

func (s *SystemSetup) setProxy() error {
	key, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.SET_VALUE)
	if err != nil {
		return err
	}
	defer key.Close()

	err = key.SetStringValue("ProxyServer", "127.0.0.1:"+globalConfig.Port)
	if err != nil {
		return err
	}

	err = key.SetDWordValue("ProxyEnable", 1)
	if err != nil {
		return err
	}
	return nil
}

func (s *SystemSetup) unsetProxy() error {
	key, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.SET_VALUE)
	if err != nil {
		return err
	}
	defer key.Close()
	err = key.SetDWordValue("ProxyEnable", 0)
	if err != nil {
		return err
	}
	return nil
}

func (s *SystemSetup) installCert() (string, error) {
	certData, err := s.initCert()
	if err != nil {
		return "", errors.New("installCert1:" + err.Error())
	}

	block, _ := pem.Decode(certData)
	if block == nil {
		return "", errors.New("Failed to parse certificate PEM" + err.Error())
	}

	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		return "", errors.New("installCert3:" + err.Error())
	}

	rootStorePtr, err := windows.UTF16PtrFromString("ROOT")
	if err != nil {
		return "", errors.New("installCert4:" + err.Error())
	}

	store, err := windows.CertOpenStore(windows.CERT_STORE_PROV_SYSTEM, 0, 0, windows.CERT_SYSTEM_STORE_LOCAL_MACHINE, uintptr(unsafe.Pointer(rootStorePtr)))
	if err != nil {
		return "", errors.New("installCert5:" + err.Error())
	}
	defer windows.CertCloseStore(store, 0)

	certContext, err := windows.CertCreateCertificateContext(windows.X509_ASN_ENCODING|windows.PKCS_7_ASN_ENCODING, &cert.Raw[0], uint32(len(cert.Raw)))
	if err != nil {
		return "", errors.New("installCert6:" + err.Error())
	}
	defer windows.CertFreeCertificateContext(certContext)
	err = windows.CertAddCertificateContextToStore(store, certContext, windows.CERT_STORE_ADD_REPLACE_EXISTING, nil)
	if err != nil {
		return "", errors.New("installCert7:" + err.Error())
	}
	return "", nil
}


================================================
FILE: core/utils.go
================================================
package core

import (
	"github.com/wailsapp/wails/v2/pkg/runtime"
)

func DialogErr(message string) {
	_, _ = runtime.MessageDialog(appOnce.ctx, runtime.MessageDialogOptions{
		Type:          runtime.ErrorDialog,
		Title:         "Error",
		Message:       message,
		DefaultButton: "Cancel",
	})
}


================================================
FILE: docs/.nojekyll
================================================


================================================
FILE: docs/_coverpage.md
================================================
<div align="center">
<a href="https://github.com/putyy/res-downloader"><img src="images/logo.png" width="120"/></a>
<h1><strong>res-downloader</strong></h1>
</div>

> 全新技术栈,更新、更小、更快、更稳

### 简单、高效、轻便 (仅 ~10M)


[开始使用 Let Go](/readme.md)
[下载](/getting-started.md)

================================================
FILE: docs/_navbar.md
================================================
* [论坛](https://s.gowas.cn/d/4089)
* [反馈](https://github.com/putyy/res-downloader/issues)
* [日志](https://github.com/putyy/res-downloader/releases)
* [WX群](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)

================================================
FILE: docs/_sidebar.md
================================================
* [简介](readme.md)
* [快速开始](getting-started.md)
* [安装指南](installation.md)
* [功能演示](examples.md)
* [更多说明](more.md)
* [常见问题](troubleshooting.md)


================================================
FILE: docs/examples.md
================================================
## 开启代理  
- 安装完成后开启代理 (最新版本为“开启抓取”),如图:  
![](images/examples-1.png ':size=50%')

## 拦截资源
### 视频号  
- 打开视频号即可看到本软件中拦截到的资源   
![](images/examples-2.webp ':size=50%')

### 网页资源
- 浏览器打开或者其他软件内置浏览器打开的网页
- 这里演示打开百度这个网站:https://www.baidu.com/  
![](images/examples-4.png ':size=50%')

### 小程序、公众号、抖音、小红书、qq音乐、酷狗等应用内资源获取方式都差不多!

## 下载资源到本地
- 选择你想下载的视频 点击下载即可  
![](images/examples-3.png ':size=50%')

================================================
FILE: docs/getting-started.md
================================================
## 软件下载
🆕 [github下载](https://github.com/putyy/res-downloader/releases)  
🆕 [蓝奏云下载 密码:9vs5](https://wwjv.lanzoum.com/b04wgtfyb)

!> Win7用户请使用2.3.0版本

## 使用方法
- 安装时一定要同意安装证书文件、一定要允许网络访问
- 打开本软件(win系统首次使用管理员打开-鼠标右键选择管理员打开)
- 打开后,左上角点击 “启动代理”
- 打开要捕获的源, 如:视频号、网页、小程序等等
- 返回软件首页即可看到资源列

!> windows安装,先关闭所有安全管家之类的软件,安装完成后首次使用需右键管理员打开  

!> Mac如果无法拦截 请关闭防火墙  

![](images/show.webp ':size=50%')

================================================
FILE: docs/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>res-downloader</title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
  <meta name="keywords" content="res-downloader,视频号下载,抖音下载,快手下载,小红书下载,万能下载器,爱享素材,爱享素材下载器">
  <meta name="description" content="res-downloader是一款集网络资源嗅探 + 高速下载功能于一体的软件,高颜值、高性能和多样化,提供个人用户下载自己上传到各大平台的网络资源功能!">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css">
</head>
<body>
  <div id="app"></div>
  <script>
    window.$docsify = {
      name: 'res-downloader',
      repo: 'https://github.com/putyy/res-downloader',
      // 侧边栏支持,默认加载的是项目根目录下的_sidebar.md文件
      loadSidebar: true,
      // 导航栏支持,默认加载的是项目根目录下的_navbar.md文件
      loadNavbar: true,
      // 封面支持,默认加载的是项目根目录下的_coverpage.md文件
      coverpage: true,
      // 最大支持渲染的标题层级
      maxLevel: 5,
      // 自定义侧边栏后默认不会再生成目录,设置生成目录的最大层级(建议配置为2-4)
      subMaxLevel: 4,
      // 小屏设备下合并导航栏到侧边栏
      mergeNavbar: true,
      basePath: '/',
      homepage: 'readme.md',
      search: {
        maxAge: 86400000,// 过期时间,单位毫秒,默认一天
        paths: 'auto',// 注意:仅适用于 paths: 'auto' 模式
        // 支持本地化
        placeholder: '搜索',
        noData: '找不到结果',
        depth: 4,
        hideOtherSidebarContent: false,
        namespace: 'Docsify-Guide',
      }
    }
  </script>
  <script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
  <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/emoji.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/zoom-image.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script>
</body>
</html>


================================================
FILE: docs/installation.md
================================================
## 下载安装文件
- windows下载.exe结尾的,根据自己的系统架构下载合适的安装文件,通常下载带有“win_amd64.exe”或“x64-installer.exe”结尾的文件  
- Mac下载.dmg结尾即可  
- Linux根据系统类型下载对应的执行文件或安装文件  

## Windows安装过程
- 双击下载好的exe 正常安装即可,首次打开记得右键管理员运行

## Mac安装过程
- 双击下载好的dmg文件,将res-downloader拖入应用即可,如图:  
![installation-mac-1.png](images/installation-mac-1.png ':size=50%')

## Linux安装过程(自行替换掉对应的安装文件目录)
- ubuntu安装deb文件
> sudo apt install res-downloader_3.0.2_linux_x64.deb

- 执行文件运行方式
> chmod +x ./res-downloader_3.0.2_linux_x64
> sudo ./res-downloader_3.0.2_linux_x64




================================================
FILE: docs/more.md
================================================
## 清空列表、类型筛选
- 当资源列表过大时,无法快速找到需要的资源,这时可以先清空列表再去刷新需要的资源页面  
- 资源列表过多,可以快速根据需要的资源类型进行筛选  
![more-4.png](images/more-1.png ':size=30%')

## 拦截想要的资源类型、批量下载
- 比如只需要视频时就选择视频类型,可以多选  
![more-1.png](images/more-2.png ':size=30%')

## 批量导出、批量导入使用场景
- 导出resd格式数据,将txt文件发送到另外的电脑,打开文件复制内容,使用批量导入导入到新电脑

## 复制链接、视频解密
- 复制链接可用于第三方软件进行下载,下载完成后对该视频解密,点击“视频解密”选择用其他软件下载完成后的视频文件进行解密  
![more-3.png](images/more-3.png ':size=30%')

## 设置说明
!> 修改完成后记得点保存  
- 几乎每项配置在软件中都有说明(鼠标悬浮在?处即可查看),此处就不进行多余讲解  
![config.png](images/config.png ':size=30%')

================================================
FILE: docs/readme.md
================================================
<div align="center">

<a href="https://github.com/putyy/res-downloader"><img src="images/logo.png" width="120"/></a>
<h1>res-downloader</h1>
<h4>📖 中文 | <a href="https://github.com/putyy/res-downloader/blob/master/README-EN.md">English</a></h4>

[![GitHub stars](https://img.shields.io/github/stars/putyy/res-downloader)](https://github.com/putyy/res-downloader/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/putyy/res-downloader)](https://github.com/putyy/res-downloader/fork)
[![GitHub release](https://img.shields.io/github/release/putyy/res-downloader)](https://github.com/putyy/res-downloader/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/putyy/res-downloader/total)
[![License](https://img.shields.io/github/license/putyy/res-downloader)](https://github.com/putyy/res-downloader/blob/master/LICENSE)

</div>

---

### 🎉 爱享素材下载器

> 一款基于 Go + [Wails](https://github.com/wailsapp/wails) 的跨平台资源下载工具,简洁易用,支持多种资源嗅探与下载。

## ✨ 功能特色

- 🚀 **简单易用**:操作简单,界面清晰美观
- 🖥️ **多平台支持**:Windows / macOS / Linux
- 🌐 **多资源类型支持**:视频 / 音频 / 图片 / m3u8 / 直播流等
- 📱 **平台兼容广泛**:支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、QQ音乐等
- 🌍 **代理抓包**:支持设置代理获取受限网络下的资源

## 📚 文档 & 版本

- 📘 [在线文档](https://res.putyy.com/)
- 💬 [加入交流群](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)
- 🧩 [最新版](https://github.com/putyy/res-downloader/releases) | [Mini版 使用默认浏览器展示UI](https://github.com/putyy/resd-mini) | [Electron旧版 支持Win7](https://github.com/putyy/res-downloader/tree/old)
  > *群满时可加微信 `AmorousWorld`,请备注“来源”*

## 🧩 下载地址

- 🆕 [GitHub 下载](https://github.com/putyy/res-downloader/releases)
- 🆕 [蓝奏云下载(密码:9vs5)](https://wwjv.lanzoum.com/b04wgtfyb)
- ⚠️ *Win7 用户请下载 `2.3.0` 版本*


## 🖼️ 预览

![预览](images/show.webp)

---

### 🧠 更多问题

- [GitHub Issues](https://github.com/putyy/res-downloader/issues)
- [爱享论坛讨论帖](https://s.gowas.cn/d/4089)

## 💡 实现原理 & 初衷

本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。

---

## ⚠️ 免责声明

> 本软件仅供学习与研究用途,禁止用于任何商业或违法用途。  
如因此产生的任何法律责任,概与作者无关!


================================================
FILE: docs/troubleshooting.md
================================================
## 视频号拦截了一大堆 找不到想要的
> 设置里面关闭全量拦截,将视频转发好友后打开

## 某某网址拦截不了?
> 本软件并非万能的,所以有一些应用拦截不了很正常,实现原理 & 初衷如下,
```
本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。
```

## 软件打不开了?之前可以打开
> 删除对应目录, 然后重启
```
## Mac终端执行
rm -rf /Users/$(whoami)/Library/Preferences/res-downloader

## Windows手动删除以下目录,Administrator为用户名 通常如下:
C:\Users\Administrator\AppData\Roaming\res-downloader

## Linux手动删除以下目录
/home/user/.config/res-downloader/home/user/.config/res-downloader
```

## 某应用只支持手机打开 如何拦截?
> 这里需要注意的是 应用使用http协议通讯才能拦截,且安卓7.0以上系统不再信任用户CA证书 所以没法拦截,解决方案自行查找,
```
1. 将手机和电脑处于同一个网络
2. 在手机端安装res-downloader的证书
3. 将手机网络代理设置为res-downloader的代理
4. 正常使用
```

## Mac 提示“已损坏,无法打开”, 打开命令行执行如下命令:
> sudo xattr -d com.apple.quarantine /Applications/res-downloader.app

## 打开本软件,无法正常拦截获取
> 检查系统证书是否安装  
> 关闭网络防火墙  
> 系统代理是否正确设置(代理地址:127.0.0.1 端口:8899)

## 关闭软件后无法正常上网
> 手动关闭系统代理设置

## 链接不是私密链接
> 通常是证书未正确安装,最新版证书下载:软件左下角 ?点击后有下载地址  
> 根据自己系统进行安装证书操作(不懂的自行百度),手动安装需安装到受信任的根证书  

- Mac手动安装证书(V3+版本支持),打开终端复制以下命令 粘贴到终端回车 按照提示输入密码,完成后再打开软件:
```shell
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /Users/$(whoami)/Library/Preferences/res-downloader/cert.crt && touch /Users/$(whoami)/Library/Preferences/res-downloader/install.lock && echo "安装完成"
```

## 拦截不到小程序中的资源
清理微信缓存,删除小程序后,重新打开
> 1.设置->存储空间->缓存  
> 2.删除小程序相关缓存目录(自行搜索)

## 只拦截打开的视频号视频
关闭全量拦截,打开视频号视频详情,通常分享好友后打开的页面属于详情页

## 拦截视频号账号视频
打开对应作者个人主页,浏览即可

## 下载慢、大视频下载失败
推荐使用如下工具加速下载,视频号可以下载完成后再到对应视频操作项选择 “视频解密” 按钮  
> [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)、[Motrix](https://motrix.app/download)等软件进行下载

## 直播流: 预览和录制:
> [使用obs进行预览和录制 使用教程自行百度, 点击下载obs]( https://obsproject.com/)

## m3u8: 预览和下载:
> [在线下载](https://m3u8-down.gowas.cn/)、[在线预览](https://m3u8play.com/)

## 安装证书后还会提示安装
使用命令行打开本软件,查看 “lockfile:” 这串字符后面的锁文件路径,然后创建该文件即可  
例如 mac系统下终端执行如下命令即可创建  
> touch /Users/你的用户名/Library/Preferences/res-downloader/install.lock

## 更多问题 请前往github进行[反馈](https://github.com/putyy/res-downloader/issues)

================================================
FILE: frontend/READ-THIS.md
================================================
This template uses a work around as the default template does not compile due to this issue:
https://github.com/vuejs/core/issues/1228

In `tsconfig.json`, `isolatedModules` is set to `false` rather than `true` to work around the issue.

================================================
FILE: frontend/README.md
================================================
# Vue 3 + TypeScript + Vite

This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue
3 `<script setup>` SFCs, check out
the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.

## Recommended IDE Setup

- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)

## Type Support For `.vue` Imports in TS

Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type
by default. In most cases this is fine if you don't really care about component prop types outside of templates.
However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using
manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:

1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look
   for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default,
   Take Over mode will enable itself if the default TypeScript extension is disabled.
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.

You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).


================================================
FILE: frontend/auto-imports.d.ts
================================================
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
  const EffectScope: typeof import('vue')['EffectScope']
  const computed: typeof import('vue')['computed']
  const createApp: typeof import('vue')['createApp']
  const customRef: typeof import('vue')['customRef']
  const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
  const defineComponent: typeof import('vue')['defineComponent']
  const effectScope: typeof import('vue')['effectScope']
  const getCurrentInstance: typeof import('vue')['getCurrentInstance']
  const getCurrentScope: typeof import('vue')['getCurrentScope']
  const h: typeof import('vue')['h']
  const inject: typeof import('vue')['inject']
  const isProxy: typeof import('vue')['isProxy']
  const isReactive: typeof import('vue')['isReactive']
  const isReadonly: typeof import('vue')['isReadonly']
  const isRef: typeof import('vue')['isRef']
  const markRaw: typeof import('vue')['markRaw']
  const nextTick: typeof import('vue')['nextTick']
  const onActivated: typeof import('vue')['onActivated']
  const onBeforeMount: typeof import('vue')['onBeforeMount']
  const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
  const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
  const onDeactivated: typeof import('vue')['onDeactivated']
  const onErrorCaptured: typeof import('vue')['onErrorCaptured']
  const onMounted: typeof import('vue')['onMounted']
  const onRenderTracked: typeof import('vue')['onRenderTracked']
  const onRenderTriggered: typeof import('vue')['onRenderTriggered']
  const onScopeDispose: typeof import('vue')['onScopeDispose']
  const onServerPrefetch: typeof import('vue')['onServerPrefetch']
  const onUnmounted: typeof import('vue')['onUnmounted']
  const onUpdated: typeof import('vue')['onUpdated']
  const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
  const provide: typeof import('vue')['provide']
  const reactive: typeof import('vue')['reactive']
  const readonly: typeof import('vue')['readonly']
  const ref: typeof import('vue')['ref']
  const resolveComponent: typeof import('vue')['resolveComponent']
  const shallowReactive: typeof import('vue')['shallowReactive']
  const shallowReadonly: typeof import('vue')['shallowReadonly']
  const shallowRef: typeof import('vue')['shallowRef']
  const toRaw: typeof import('vue')['toRaw']
  const toRef: typeof import('vue')['toRef']
  const toRefs: typeof import('vue')['toRefs']
  const toValue: typeof import('vue')['toValue']
  const triggerRef: typeof import('vue')['triggerRef']
  const unref: typeof import('vue')['unref']
  const useAttrs: typeof import('vue')['useAttrs']
  const useCssModule: typeof import('vue')['useCssModule']
  const useCssVars: typeof import('vue')['useCssVars']
  const useDialog: typeof import('naive-ui')['useDialog']
  const useId: typeof import('vue')['useId']
  const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
  const useMessage: typeof import('naive-ui')['useMessage']
  const useModel: typeof import('vue')['useModel']
  const useNotification: typeof import('naive-ui')['useNotification']
  const useSlots: typeof import('vue')['useSlots']
  const useTemplateRef: typeof import('vue')['useTemplateRef']
  const watch: typeof import('vue')['watch']
  const watchEffect: typeof import('vue')['watchEffect']
  const watchPostEffect: typeof import('vue')['watchPostEffect']
  const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
  // @ts-ignore
  export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
  import('vue')
}


================================================
FILE: frontend/components.d.ts
================================================
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}

/* prettier-ignore */
declare module 'vue' {
  export interface GlobalComponents {
    Action: typeof import('./src/components/Action.vue')['default']
    ActionDesc: typeof import('./src/components/ActionDesc.vue')['default']
    Footer: typeof import('./src/components/Footer.vue')['default']
    ImportJson: typeof import('./src/components/ImportJson.vue')['default']
    Index: typeof import('./src/components/layout/Index.vue')['default']
    NaiveProvider: typeof import('./src/components/NaiveProvider.vue')['default']
    NButton: typeof import('naive-ui')['NButton']
    NButtonGroup: typeof import('naive-ui')['NButtonGroup']
    NCheckbox: typeof import('naive-ui')['NCheckbox']
    NConfigProvider: typeof import('naive-ui')['NConfigProvider']
    NDataTable: typeof import('naive-ui')['NDataTable']
    NDialogProvider: typeof import('naive-ui')['NDialogProvider']
    NForm: typeof import('naive-ui')['NForm']
    NFormItem: typeof import('naive-ui')['NFormItem']
    NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
    NIcon: typeof import('naive-ui')['NIcon']
    NInput: typeof import('naive-ui')['NInput']
    NInputNumber: typeof import('naive-ui')['NInputNumber']
    NLayout: typeof import('naive-ui')['NLayout']
    NLayoutContent: typeof import('naive-ui')['NLayoutContent']
    NLayoutFooter: typeof import('naive-ui')['NLayoutFooter']
    NLayoutSider: typeof import('naive-ui')['NLayoutSider']
    NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
    NMenu: typeof import('naive-ui')['NMenu']
    NMessageProvider: typeof import('naive-ui')['NMessageProvider']
    NModal: typeof import('naive-ui')['NModal']
    NModalProvider: typeof import('naive-ui')['NModalProvider']
    NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
    NPopconfirm: typeof import('naive-ui')['NPopconfirm']
    NPopover: typeof import('naive-ui')['NPopover']
    NScrollbar: typeof import('naive-ui')['NScrollbar']
    NSelect: typeof import('naive-ui')['NSelect']
    NSwitch: typeof import('naive-ui')['NSwitch']
    NTabPane: typeof import('naive-ui')['NTabPane']
    NTabs: typeof import('naive-ui')['NTabs']
    NTooltip: typeof import('naive-ui')['NTooltip']
    Password: typeof import('./src/components/Password.vue')['default']
    Preview: typeof import('./src/components/Preview.vue')['default']
    RouterLink: typeof import('vue-router')['RouterLink']
    RouterView: typeof import('vue-router')['RouterView']
    Screen: typeof import('./src/components/Screen.vue')['default']
    ShowLoading: typeof import('./src/components/ShowLoading.vue')['default']
    ShowOrEdit: typeof import('./src/components/ShowOrEdit.vue')['default']
    Sider: typeof import('./src/components/layout/Sider.vue')['default']
  }
}


================================================
FILE: frontend/env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: frontend/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
    <title>res-downloader</title>
</head>
<body style="--wails-draggable:drag">
<div id="app"></div>
<script src="./src/main.ts" type="module"></script>
</body>
</html>



================================================
FILE: frontend/package.json
================================================
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@vicons/ionicons5": "^0.12.0",
    "axios": "^1.7.2",
    "flv.js": "^1.6.2",
    "naive-ui": "^2.38.2",
    "pinia": "^2.1.7",
    "video.js": "^8.22.0",
    "vue": "^3.2.37",
    "vue-i18n": "^11.1.3",
    "vue-router": "^4.3.3"
  },
  "devDependencies": {
    "@babel/types": "^7.18.10",
    "@iconify/vue": "^4.1.2",
    "@types/node": "^20.14.7",
    "@vitejs/plugin-vue": "^3.0.3",
    "autoprefixer": "^10.4.19",
    "postcss": "^8.4.38",
    "sass": "^1.77.6",
    "sass-loader": "^14.2.1",
    "tailwindcss": "^3.4.4",
    "typescript": "^4.6.4",
    "unplugin-auto-import": "^0.18.3",
    "unplugin-vue-components": "^0.27.4",
    "vite": "^3.0.7",
    "vue-tsc": "^1.8.27"
  }
}


================================================
FILE: frontend/package.json.md5
================================================
853e4a476a4f41b58875469cfb541133

================================================
FILE: frontend/postcss.config.js
================================================
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}


================================================
FILE: frontend/src/App.vue
================================================
<template>
  <NConfigProvider class="h-full" :theme="theme" :locale="uiLocale">
    <NaiveProvider>
      <RouterView/>
    </NaiveProvider>
    <NGlobalStyle/>
    <NModalProvider/>
  </NConfigProvider>
</template>

<script setup lang="ts">
import NaiveProvider from '@/components/NaiveProvider.vue'
import {darkTheme, lightTheme, zhCN, enUS} from 'naive-ui'
import {useIndexStore} from "@/stores"
import {computed, onMounted} from "vue"
import {useEventStore} from "@/stores/event"
import type {appType} from "@/types/app"
import {useI18n} from 'vue-i18n'

const store = useIndexStore()
const eventStore = useEventStore()
const {locale} = useI18n()

const theme = computed(() => {
  if (store.globalConfig.Theme === "darkTheme") {
    document.documentElement.classList.add('dark');
    return darkTheme
  }
  document.documentElement.classList.remove('dark');
  return lightTheme
})

const uiLocale = computed(() => {
  locale.value = store.globalConfig.Locale
  if (store.globalConfig.Locale === "zh") {
    return zhCN
  }
  return enUS
})

onMounted(async () => {
  await store.init()

  eventStore.init()
  eventStore.addHandle({
    type: "message",
    event: (res: appType.Message) => {
      switch (res?.code) {
        case 0:
          window.$message?.error(res.message)
          break
        case 1:
          window.$message?.success(res.message)
          break
      }
    }
  })
})
</script>

================================================
FILE: frontend/src/api/app.ts
================================================
import request from '@/api/request'

export default {
    install() {
        return request({
            url: '/api/install',
            method: 'post'
        })
    },
    setSystemPassword(data: object) {
        return request({
            url: 'api/set-system-password',
            method: 'post',
            data: data
        })
    },
    openSystemProxy() {
        return request({
            url: 'api/proxy-open',
            method: 'post',
        })
    },
    unsetSystemProxy() {
        return request({
            url: 'api/proxy-unset',
            method: 'post',
        })
    },
    openDirectoryDialog() {
        return request({
            url: 'api/open-directory',
            method: 'post'
        })
    },
    openFileDialog() {
        return request({
            url: 'api/open-file',
            method: 'post'
        })
    },
    openFolder(data: object) {
        return request({
            url: 'api/open-folder',
            method: 'post',
            data: data
        })
    },
    isProxy() {
        return request({
            url: 'api/is-proxy',
            method: 'post'
        })
    },
    appInfo() {
        return request({
            url: 'api/app-info',
            method: 'post',
        })
    },
    getConfig() {
        return request({
            url: 'api/get-config',
            method: 'post',
        })
    },
    setConfig(data: object) {
        return request({
            url: 'api/set-config',
            method: 'post',
            data: data
        })
    },
    setType(data: string[]) {
        return request({
            url: 'api/set-type',
            method: 'post',
            data: {
                type: data.toString()
            }
        })
    },
    clear() {
        return request({
            url: 'api/clear',
            method: 'post'
        })
    },
    delete(data: object) {
        return request({
            url: 'api/delete',
            method: 'post',
            data: data
        })
    },
    cancel(data: object) {
        return request({
            url: 'api/cancel',
            method: 'post',
            data: data
        })
    },
    download(data: object) {
        return request({
            url: 'api/download',
            method: 'post',
            data: data
        })
    },
    wxFileDecode(data: object) {
        return request({
            url: 'api/wx-file-decode',
            method: 'post',
            data: data
        })
    },
    batchExport(data: object) {
        return request({
            url: 'api/batch-export',
            method: 'post',
            data: data
        })
    },
}

================================================
FILE: frontend/src/api/request.ts
================================================
import type {AxiosResponse, InternalAxiosRequestConfig} from 'axios'
import axios from 'axios'

interface RequestOptions {
    url: string
    method: 'get' | 'post' | 'put' | 'delete'
    params?: Record<string, any>
    data?: Record<string, any>
}

const instance = axios.create({
    baseURL: "/",
    timeout: 180000
})

instance.interceptors.request.use(
    (config: InternalAxiosRequestConfig<any>) => {
        return config
    },
    (error) => {
        return Promise.reject(error)
    }
)

instance.interceptors.response.use(
    (response: AxiosResponse) => {
        return response.data
    },
    (error) => {
        return Promise.reject(error);
    }
)

const request = ({url, method, params, data}: RequestOptions): Promise<any> => {
    return instance({url, method, params, data, baseURL: window.$baseUrl})
}

export default request

================================================
FILE: frontend/src/assets/css/base.css
================================================
/* color palette from <https://github.com/vuejs/theme> */
:root {
  --vt-c-white: #ffffff;
  --vt-c-white-soft: #f8f8f8;
  --vt-c-white-mute: #f2f2f2;

  --vt-c-black: #181818;
  --vt-c-black-soft: #222222;
  --vt-c-black-mute: #282828;

  --vt-c-indigo: #2c3e50;

  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
  --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);

  --vt-c-text-light-1: var(--vt-c-indigo);
  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
  --vt-c-text-dark-1: var(--vt-c-white);
  --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}

/* semantic color variables for this project */
:root {
  --color-background: var(--vt-c-white);
  --color-background-soft: var(--vt-c-white-soft);
  --color-background-mute: var(--vt-c-white-mute);

  --color-border: var(--vt-c-divider-light-2);
  --color-border-hover: var(--vt-c-divider-light-1);

  --color-heading: var(--vt-c-text-light-1);
  --color-text: var(--vt-c-text-light-1);

  --section-gap: 160px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-background: var(--vt-c-black);
    --color-background-soft: var(--vt-c-black-soft);
    --color-background-mute: var(--vt-c-black-mute);

    --color-border: var(--vt-c-divider-dark-2);
    --color-border-hover: var(--vt-c-divider-dark-1);

    --color-heading: var(--vt-c-text-dark-1);
    --color-text: var(--vt-c-text-dark-2);
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  font-weight: normal;
}

body {
  min-height: 100vh;
  color: var(--color-text);
  background: var(--color-background);
  transition:
    color 0.5s,
    background-color 0.5s;
  line-height: 1.6;
  font-family:
    Inter,
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    Roboto,
    Oxygen,
    Ubuntu,
    Cantarell,
    'Fira Sans',
    'Droid Sans',
    'Helvetica Neue',
    sans-serif;
  font-size: 15px;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}


================================================
FILE: frontend/src/assets/css/main.css
================================================
@import 'base.css';

@tailwind base;
@tailwind components;
@tailwind utilities;

#app {
  width: 100vw;
  height: 100vh;
}

.ellipsis-2 {
  display: -webkit-box;
  overflow: hidden;
  text-overflow: ellipsis;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

================================================
FILE: frontend/src/assets/js/decrypt.js
================================================


// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define   var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
var Module = typeof Module !== 'undefined' ? Module : {};

// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)
// {{PRE_JSES}}

// Sometimes an existing Module object exists with properties
// meant to overwrite the default module functionality. Here
// we collect those properties and reapply _after_ we configure
// the current environment's defaults to avoid having to be so
// defensive during initialization.
var moduleOverrides = {};
var key;
for (key in Module) {
  if (Module.hasOwnProperty(key)) {
    moduleOverrides[key] = Module[key];
  }
}

var arguments_ = [];
var thisProgram = './this.program';
var quit_ = function(status, toThrow) {
  throw toThrow;
};

// Determine the runtime environment we are in. You can customize this by
// setting the ENVIRONMENT setting at compile time (see settings.wxjs).

var ENVIRONMENT_IS_WEB = false;
var ENVIRONMENT_IS_WORKER = false;
var ENVIRONMENT_IS_NODE = true;
var ENVIRONMENT_IS_SHELL = false;

// `/` should be present at the end if `scriptDirectory` is not empty
var scriptDirectory = '';
function locateFile(path) {
  if (Module['locateFile']) {
    return Module['locateFile'](path, scriptDirectory);
  }
  return scriptDirectory + path;
}

// Hooks that are implemented differently in different runtime environments.
var read_,
    readAsync,
    readBinary,
    setWindowTitle;

// Note that this includes Node.wxjs workers when relevant (pthreads is enabled).
// Node.wxjs workers are detected as a combination of ENVIRONMENT_IS_WORKER and
// ENVIRONMENT_IS_NODE.
if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) {
  if (ENVIRONMENT_IS_WORKER) { // Check worker, not web, since window could be polyfilled
    scriptDirectory = self.location.href;
  } else if (typeof document !== 'undefined' && document.currentScript) { // web
    scriptDirectory = document.currentScript.src;
  }
  // blob urls look like blob:http://site.com/etc/etc and we cannot infer anything from them.
  // otherwise, slice off the final part of the url to find the script directory.
  // if scriptDirectory does not contain a slash, lastIndexOf will return -1,
  // and scriptDirectory will correctly be replaced with an empty string.
  // If scriptDirectory contains a query (starting with ?) or a fragment (starting with #),
  // they are removed because they could contain a slash.
  if (scriptDirectory.indexOf('blob:') !== 0) {
    scriptDirectory = scriptDirectory.substr(0, scriptDirectory.replace(/[?#].*/, "").lastIndexOf('/')+1);
  } else {
    scriptDirectory = '';
  }

  // Differentiate the Web Worker from the Node Worker case, as reading must
  // be done differently.
  {

// include: web_or_worker_shell_read.wxjs


  read_ = function(url) {
      var xhr = new XMLHttpRequest();
      xhr.open('GET', url, false);
      xhr.send(null);
      return xhr.responseText;
  };

  if (ENVIRONMENT_IS_WORKER) {
    readBinary = function(url) {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, false);
        xhr.responseType = 'arraybuffer';
        xhr.send(null);
        return new Uint8Array(/** @type{!ArrayBuffer} */(xhr.response));
    };
  }

  readAsync = function(url, onload, onerror) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.responseType = 'arraybuffer';
    xhr.onload = function() {
      if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0
        onload(xhr.response);
        return;
      }
      onerror();
    };
    xhr.onerror = onerror;
    xhr.send(null);
  };

// end include: web_or_worker_shell_read.wxjs
  }

  setWindowTitle = function(title) { document.title = title };
} else
{
}

var out = Module['print'] || console.log.bind(console);
var err = Module['printErr'] || console.warn.bind(console);

// Merge back in the overrides
for (key in moduleOverrides) {
  if (moduleOverrides.hasOwnProperty(key)) {
    Module[key] = moduleOverrides[key];
  }
}
// Free the object hierarchy contained in the overrides, this lets the GC
// reclaim data used e.g. in memoryInitializerRequest, which is a large typed array.
moduleOverrides = null;

// Emit code to handle expected values on the Module object. This applies Module.x
// to the proper local x. This has two benefits: first, we only emit it if it is
// expected to arrive, and second, by using a local everywhere else that can be
// minified.

if (Module['arguments']) arguments_ = Module['arguments'];

if (Module['thisProgram']) thisProgram = Module['thisProgram'];

if (Module['quit']) quit_ = Module['quit'];

// perform assertions in shell.wxjs after we set up out() and err(), as otherwise if an assertion fails it cannot print the message




var STACK_ALIGN = 16;
var POINTER_SIZE = 4;

function getNativeTypeSize(type) {
  switch (type) {
    case 'i1': case 'i8': return 1;
    case 'i16': return 2;
    case 'i32': return 4;
    case 'i64': return 8;
    case 'float': return 4;
    case 'double': return 8;
    default: {
      if (type[type.length-1] === '*') {
        return POINTER_SIZE;
      } else if (type[0] === 'i') {
        var bits = Number(type.substr(1));
        assert(bits % 8 === 0, 'getNativeTypeSize invalid bits ' + bits + ', type ' + type);
        return bits / 8;
      } else {
        return 0;
      }
    }
  }
}

function warnOnce(text) {
  if (!warnOnce.shown) warnOnce.shown = {};
  if (!warnOnce.shown[text]) {
    warnOnce.shown[text] = 1;
    err(text);
  }
}

// include: runtime_functions.wxjs


// Wraps a JS function as a wasm function with a given signature.
function convertJsFunctionToWasm(func, sig) {

  // If the type reflection proposal is available, use the new
  // "WebAssembly.Function" constructor.
  // Otherwise, construct a minimal wasm module importing the JS function and
  // re-exporting it.
  if (typeof WebAssembly.Function === "function") {
    var typeNames = {
      'i': 'i32',
      'j': 'i64',
      'f': 'f32',
      'd': 'f64'
    };
    var type = {
      parameters: [],
      results: sig[0] == 'v' ? [] : [typeNames[sig[0]]]
    };
    for (var i = 1; i < sig.length; ++i) {
      type.parameters.push(typeNames[sig[i]]);
    }
    return new WebAssembly.Function(type, func);
  }

  // The module is static, with the exception of the type section, which is
  // generated based on the signature passed in.
  var typeSection = [
    0x01, // id: section,
    0x00, // length: 0 (placeholder)
    0x01, // count: 1
    0x60, // form: func
  ];
  var sigRet = sig.slice(0, 1);
  var sigParam = sig.slice(1);
  var typeCodes = {
    'i': 0x7f, // i32
    'j': 0x7e, // i64
    'f': 0x7d, // f32
    'd': 0x7c, // f64
  };

  // Parameters, length + signatures
  typeSection.push(sigParam.length);
  for (var i = 0; i < sigParam.length; ++i) {
    typeSection.push(typeCodes[sigParam[i]]);
  }

  // Return values, length + signatures
  // With no multi-return in MVP, either 0 (void) or 1 (anything else)
  if (sigRet == 'v') {
    typeSection.push(0x00);
  } else {
    typeSection = typeSection.concat([0x01, typeCodes[sigRet]]);
  }

  // Write the overall length of the type section back into the section header
  // (excepting the 2 bytes for the section id and length)
  typeSection[1] = typeSection.length - 2;

  // Rest of the module is static
  var bytes = new Uint8Array([
    0x00, 0x61, 0x73, 0x6d, // magic ("\0asm")
    0x01, 0x00, 0x00, 0x00, // version: 1
  ].concat(typeSection, [
    0x02, 0x07, // import section
      // (import "e" "f" (func 0 (type 0)))
      0x01, 0x01, 0x65, 0x01, 0x66, 0x00, 0x00,
    0x07, 0x05, // export section
      // (export "f" (func 0 (type 0)))
      0x01, 0x01, 0x66, 0x00, 0x00,
  ]));

   // We can compile this wasm module synchronously because it is very small.
  // This accepts an import (at "e.f"), that it reroutes to an export (at "f")
  var module = new WebAssembly.Module(bytes);
  var instance = new WebAssembly.Instance(module, {
    'e': {
      'f': func
    }
  });
  var wrappedFunc = instance.exports['f'];
  return wrappedFunc;
}

var freeTableIndexes = [];

// Weak map of functions in the table to their indexes, created on first use.
var functionsInTableMap;

function getEmptyTableSlot() {
  // Reuse a free index if there is one, otherwise grow.
  if (freeTableIndexes.length) {
    return freeTableIndexes.pop();
  }
  // Grow the table
  try {
    wasmTable.grow(1);
  } catch (err) {
    if (!(err instanceof RangeError)) {
      throw err;
    }
    throw 'Unable to grow wasm table. Set ALLOW_TABLE_GROWTH.';
  }
  return wasmTable.length - 1;
}

function updateTableMap(offset, count) {
  for (var i = offset; i < offset + count; i++) {
    var item = getWasmTableEntry(i);
    // Ignore null values.
    if (item) {
      functionsInTableMap.set(item, i);
    }
  }
}

// Add a function to the table.
// 'sig' parameter is required if the function being added is a JS function.
function addFunction(func, sig) {

  // Check if the function is already in the table, to ensure each function
  // gets a unique index. First, create the map if this is the first use.
  if (!functionsInTableMap) {
    functionsInTableMap = new WeakMap();
    updateTableMap(0, wasmTable.length);
  }
  if (functionsInTableMap.has(func)) {
    return functionsInTableMap.get(func);
  }

  // It's not in the table, add it now.

  var ret = getEmptyTableSlot();

  // Set the new value.
  try {
    // Attempting to call this with JS function will cause of table.set() to fail
    setWasmTableEntry(ret, func);
  } catch (err) {
    if (!(err instanceof TypeError)) {
      throw err;
    }
    var wrapped = convertJsFunctionToWasm(func, sig);
    setWasmTableEntry(ret, wrapped);
  }

  functionsInTableMap.set(func, ret);

  return ret;
}

function removeFunction(index) {
  functionsInTableMap.delete(getWasmTableEntry(index));
  freeTableIndexes.push(index);
}

// end include: runtime_functions.wxjs
// include: runtime_debug.wxjs


// end include: runtime_debug.wxjs
var tempRet0 = 0;

var setTempRet0 = function(value) {
  tempRet0 = value;
};

var getTempRet0 = function() {
  return tempRet0;
};



// === Preamble library stuff ===

// Documentation for the public APIs defined in this file must be updated in:
//    site/source/docs/api_reference/preamble.wxjs.rst
// A prebuilt local version of the documentation is available at:
//    site/build/text/docs/api_reference/preamble.wxjs.txt
// You can also build docs locally as HTML or other formats in site/
// An online HTML version (which may be of a different version of Emscripten)
//    is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html

var wasmBinary;
function asciiToBinary(str) {
  if (typeof atob === 'function') {
    // this works in the browser
    return atob(str)
  } else {
    // this works in node
    return new Buffer(str, 'base64').toString('binary');
  }
}

function decode(encoded) {
  var binaryString =  asciiToBinary(encoded);
  var bytes = new Uint8Array(binaryString.length);
  for (var i = 0; i < binaryString.length; i++) {
      bytes[i] = binaryString.charCodeAt(i);
  }
  return bytes.buffer;
}

var wasmData = "AGFzbQEAAAABwgmSAWABfwF/YAJ/fwF/YAN/f38Bf2ACf38AYAF/AGAAAX9gBH9/f38Bf2ADf39/AGAFf39/f38Bf2AAAGAEf39/fwBgBn9/f39/fwF/YAZ/f39/f38AYAV/f39/fwBgCH9/f39/f39/AX9gAXwBfGAHf39/f39/fwF/YAd/f39/f39/AGADf35/AX5gAn9+AGACf34Bf2ABfwF+YAR/f35/AX9gAAF+YAJ/fwF8YAN/f34Bf2AFf39/f34Bf2ABfAF+YAR/f39/AX5gAX4Bf2ABfwF8YAN/f34AYAF+AX5gAn9/AX5gBH9/f34AYAN/fn8Bf2ADf39/AX5gBX9+fn5+AGAKf39/f39/f39/fwBgBX9/fn9/AGAIf39/f39/f38AYAJ8fAF8YAJ/fgF+YAR/f39+AX5gAn5/AX9gB39/f39/fn4Bf2AJf39/f399f39/AGAEfn9/fwF/YAR/f35/AX5gBH9/f34Bf2ACf3wAYAl/f39/f39/f38Bf2ADf39/AXxgAn98AXxgC39/f39/f39/f39/AX9gBH9+fn8AYAZ/fH9/f38Bf2AKf39/f39/f39/fwF/YAV/f39/fAF/YAZ/f39/fn4Bf2ADfH5+AXxgA39+fgBgBH9/f3wAYAl/f39/f31/f38Bf2ABfQF9YAV/f39+fgBgBX9/f35/AX9gBn9/f39/fgF/YAZ/f35+fn8Bf2AGf39+f39/AX9gAn5+AX5gBn9/f39/fAF/YAJ+fgF/YAN+fn4BfmACfH8BfGADfHx/AXxgAn9/AX1gA39/fwF9YAR+fn5+AX9gAn99AGAMf39/f39/f39/f39/AX9gD39/f39/f39/f39/f39/fwBgDX9/f39/f39/f39/f38AYAABfGAJf39/f399fn5+AGACf30Bf2ABfgBgBX9+f39/AGAFfn5/f38Bf2AFf39/fHwBfGADf35+AX5gCX9/f39/f35+fgF/YAp/f39/f39+f39/AX9gA39+fwBgAnx/AGAEf39+fgF/YAZ/f39/fn8Bf2AGf39/fH5+AX9gBX9/fH5+AX9gB39/f39+f38AYA1/f398f35+f39/fn5/AX9gBn9/f39/fgBgBX9/f39+AGAGf39/fn9/AX5gBX9/f39/AX5gCn9/f39+fn9/f38Bf2AGf35+f39/AX5gBX9/f35/AGAIf39/f35+f38AYAN+f34BfmAJf39/f39/f39/AGAGf35+f39/AX9gBn9/f39+fgBgCH9/f35+f39/AX9gC39/fn5+fn5+f39/AX5gBX9/f35/AX5gA39/fgF+YAZ/f39+fn8Bf2AFf39+fn8AYAN/fHwBfGAEfn5+fwF+YAR+f39/AX5gA35/fwF+YAR+f35/AX9gBn9+f39/fwF+YAR/fn9+AX5gBn9/f3x/fgF/YAZ/f3x/fn8Bf2AFf39+fn4Bf2ADf3x/AGADf398AGACfH8Bf2ABfQF/YAF8AX9gAXwAYAN+f38Bf2ACfn4BfGACfn4BfWADf399AGAKf39/f39/fX9/fwBgCn9/f39/f31/f38Bf2AFf35/f38Bf2AHf398f39/fwF/YAZ/f39+f38AYAh/f39/f39+fgF/YAR/fn9/AX8Ckws1A2VudhhfX2N4YV9hbGxvY2F0ZV9leGNlcHRpb24AAANlbnYLX19jeGFfdGhyb3cABwNlbnYMX19jeGFfYXRleGl0AAIDZW52DGdldHRpbWVvZmRheQABA2VudgRleGl0AAQDZW52BWFib3J0AAkDZW52CHN0cmZ0aW1lAAYDZW52BHRpbWUAAANlbnYYZW1zY3JpcHRlbl9hc21fY29uc3RfaW50AAIDZW52Fl9lbWJpbmRfcmVnaXN0ZXJfY2xhc3MAUgNlbnYdX2VtYmluZF9yZWdpc3Rlcl92YWx1ZV9vYmplY3QADANlbnYjX2VtYmluZF9yZWdpc3Rlcl92YWx1ZV9vYmplY3RfZmllbGQAJgNlbnYdX2VtYmluZF9maW5hbGl6ZV92YWx1ZV9vYmplY3QABANlbnYZX2VtYmluZF9yZWdpc3Rlcl9mdW5jdGlvbgAMA2VudiJfZW1iaW5kX3JlZ2lzdGVyX2NsYXNzX2NvbnN0cnVjdG9yAAwDZW52H19lbWJpbmRfcmVnaXN0ZXJfY2xhc3NfZnVuY3Rpb24AKANlbnYLbG9jYWx0aW1lX3IAAQNlbnYGbWt0aW1lAAADZW52CGdtdGltZV9yAAEDZW52IV9fYXN5bmNqc19fd2FzbV9mZm1wZWdfZm9wZW5fc3luYwACA2VudiFfX2FzeW5janNfX3dhc21fZmZtcGVnX2ZyZWFkX3N5bmMABgNlbnYFY2xvY2sABQNlbnYNY2xvY2tfZ2V0dGltZQABA2VudhVfZW1iaW5kX3JlZ2lzdGVyX3ZvaWQAAwNlbnYVX2VtYmluZF9yZWdpc3Rlcl9ib29sAA0DZW52G19lbWJpbmRfcmVnaXN0ZXJfc3RkX3N0cmluZwADA2VudhxfZW1iaW5kX3JlZ2lzdGVyX3N0ZF93c3RyaW5nAAcDZW52Fl9lbWJpbmRfcmVnaXN0ZXJfZW12YWwAAwNlbnYYX2VtYmluZF9yZWdpc3Rlcl9pbnRlZ2VyAA0DZW52Fl9lbWJpbmRfcmVnaXN0ZXJfZmxvYXQABwNlbnYcX2VtYmluZF9yZWdpc3Rlcl9tZW1vcnlfdmlldwAHA2VudhJlbXNjcmlwdGVuX2dldF9ub3cAUxZ3YXNpX3NuYXBzaG90X3ByZXZpZXcxCGZkX3dyaXRlAAYWd2FzaV9zbmFwc2hvdF9wcmV2aWV3MQdmZF9yZWFkAAYDZW52EF9fc3lzY2FsbF91bmxpbmsAABZ3YXNpX3NuYXBzaG90X3ByZXZpZXcxDWZkX2Zkc3RhdF9nZXQAARZ3YXNpX3NuYXBzaG90X3ByZXZpZXcxCGZkX2Nsb3NlAAADZW52D19fc3lzY2FsbF9pb2N0bAACA2Vudg9fX3N5c2NhbGxfcm1kaXIAAANlbnYRX19zeXNjYWxsX2ZjbnRsNjQAAgNlbnYOX19zeXNjYWxsX29wZW4AAgNlbnYPX19zeXNjYWxsX21rZGlyAAEDZW52FF9fc3lzY2FsbF9fbmV3c2VsZWN0AAgDZW52Cl9fZ210aW1lX3IAAQNlbnYNX19sb2NhbHRpbWVfcgABFndhc2lfc25hcHNob3RfcHJldmlldzERZW52aXJvbl9zaXplc19nZXQAARZ3YXNpX3NuYXBzaG90X3ByZXZpZXcxC2Vudmlyb25fZ2V0AAEDZW52CnN0cmZ0aW1lX2wACANlbnYWZW1zY3JpcHRlbl9yZXNpemVfaGVhcAAAA2VudhVlbXNjcmlwdGVuX21lbWNweV9iaWcAAgNlbnYLc2V0VGVtcFJldDAABANlbnYXX2VtYmluZF9yZWdpc3Rlcl9iaWdpbnQAERZ3YXNpX3NuYXBzaG90X3ByZXZpZXcxB2ZkX3NlZWsACAPXI8MjCQABAgQBAAAAAAEAAAAAAAAAAAAAAAAAAgAAAAAAAQAAAwABVAFVFAEAAQMAAQIBAgAHAwQAAAMDAAIAAAcAAAEGAwAABwAAAAAAAQIAAQAECgMDBAQABwAABQEAAQEAAAUCAAAAAAABAAEBAAAEAQEAAAANAAADBwADAwMAAAIBAAMFAAEAAAsAAwACAgEAAQEDAAABCQIBAgACAAAEAAADAAAKAQABAAAHBAAAAAAEAwAEAwADAwEAAAAAAQAAAAQAAwAAAQEAAAABAQAABAQEAAAAAQAAAQEBAAQBCgAAAAIAAAEAAwIKAAABAAEAAQAAAAANAAAAAAAAAAAAAAcHAAAAAAACAAAAAAEAAAAABAcDBwMDAgoAAAACAAAAAgAAAQAAAgEADQgABwAKAAABAgABAQAAAgAAAgEAAgABAgIAAAcAAwAAAAMAAAEAAgAAAAADAAAAAAAAAAMAAAAAAQACBwAAAAAAAAABAAEBAAAEBAADAAADAwcHAwMHBwEDAAADAAAAAQMDAAEJBAMADRMJCQkEAAMEAAEEChcJAQUdBQU9AgkBAQEFBAQFBQkAAQAAD1YeCwYGAAEHBAMKAwIBCQAAAAAJCgADAwMHARQjAwEBAxM+B1cCAAYHAQQHFAEGAQMBAAMBAgICAFgAAAAJAAAEAgICAQEGAwkBAQIFAQQEAgIDAAMHAgICAgICAgQNBwoCAgICAgECAgICAgICAgIAAgICAgICAgICAgICAgICAgICAgICAgICBgMAAwICAAkJCgkEBARZJA0DBgEGBgEBBgcEAgAEAAcACgICBAsHBwIKAgACAgMHBAQCAgkCAgMAAgICAgAAAgMBAgQCAgICAgICAgIDBAQEBAUACAIIAR4CAgMCAwABBQkAAAAAAC4ABAMBAAMACQABAgIACQUFBQUFBQUABQUEAAQDAwMABQUFBAUFAQAFBwUBAAcAAAAFPwAFAAAFAAMFAgAFBQUAAAEDAAUAAAEAAAUAAAcABUAABQABCQAJAwkACQABAAABAAUFCwgCAAQVBAQACgAEBQQEBAQBAQMBAQYCAgEBAwAHCAIDAQEEAwAAAQcBBwEEAAcHAgQEBwMAAAAAAAQBAAQBAQEBAAEEBAUEAQYBAQABAAEAAAEAAAUEBAEBAQEBAQEBAQFaAQEGBAYGAAQDAQECAQEBBQAAAQIfAwIGAQEBAQIBAQABAQAAAQwEAAABCAYAAAEBAgMCAQAABAIC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CDWOAY8BM0NHLQ6QAQwLCAsREDM5QZEBBAkECQUEBwFwAdMF0wUFBgEBgASAQAYrB38BQaCz3gILfwFBAAt/AUEAC38AQfTBGgt/AEHIwxoLfwFBAAt/AUEACwe8BzMGbWVtb3J5AgARX193YXNtX2NhbGxfY3RvcnMANQ1fX2dldFR5cGVOYW1lANYVKl9fZW1iaW5kX3JlZ2lzdGVyX25hdGl2ZV9hbmRfYnVpbHRpbl90eXBlcwDYFQRmcmVlAJ8jGV9faW5kaXJlY3RfZnVuY3Rpb25fdGFibGUBAAZtYWxsb2MAniMQX19lcnJub19sb2NhdGlvbgCrGAtfZ2V0X3R6bmFtZQDAGA1fZ2V0X2RheWxpZ2h0AMEYDV9nZXRfdGltZXpvbmUAwhgJc3RhY2tTYXZlAMcjDHN0YWNrUmVzdG9yZQDIIwpzdGFja0FsbG9jAMkjG2Vtc2NyaXB0ZW5fc3RhY2tfc2V0X2xpbWl0cwDLIxllbXNjcmlwdGVuX3N0YWNrX2dldF9iYXNlAMwjGGVtc2NyaXB0ZW5fc3RhY2tfZ2V0X2VuZADNIwhtZW1hbGlnbgCiIwpkeW5DYWxsX3ZpAM4jCmR5bkNhbGxfaWkAzyMNZHluQ2FsbF92aWlpaQDQIwxkeW5DYWxsX2lpaWkA0SMLZHluQ2FsbF9paWkA0iMLZHluQ2FsbF92aWkA0yMSZHluQ2FsbF92aWlpaWlmaWlpANQjCWR5bkNhbGxfaQDVIwxkeW5DYWxsX3ZpaWkA1iMSZHluQ2FsbF9paWlpaWlmaWlpANcjCWR5bkNhbGxfdgDYIw5keW5DYWxsX2lpaWlpaQDZIw9keW5DYWxsX2lpaWlpaWkA2iMNZHluQ2FsbF9pamlpaQDqIwxkeW5DYWxsX2ppamkA6yMNZHluQ2FsbF9paWlqaQDsIw9keW5DYWxsX3ZpaWlpaWkA3iMNZHluQ2FsbF9paWlpaQDfIwpkeW5DYWxsX2RkAOAjD2R5bkNhbGxfaWlkaWlpaQDhIw5keW5DYWxsX3ZpaWppaQDtIxFkeW5DYWxsX2lpaWlpaWlpaQDjIw5keW5DYWxsX2lpaWlpagDuIw5keW5DYWxsX2lpaWlpZADlIw9keW5DYWxsX2lpaWlpamoA7yMQZHluQ2FsbF9paWlpaWlpaQDnIxBkeW5DYWxs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
Download .txt
gitextract_tj3kx0fc/

├── .github/
│   └── ISSUE_TEMPLATE/
│       ├── bug_report.yml
│       ├── config.yml
│       └── feature_request.yml
├── .gitignore
├── LICENSE
├── README-EN.md
├── README.md
├── build/
│   ├── README.md
│   ├── darwin/
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── linux/
│   │   ├── .gitignore
│   │   ├── AppImage/
│   │   │   ├── res-downloader.desktop
│   │   │   └── usr/
│   │   │       ├── bin/
│   │   │       │   └── .gitkeep
│   │   │       └── share/
│   │   │           └── applications/
│   │   │               └── res-downloader.desktop
│   │   ├── Arch/
│   │   │   └── res-downloader.desktop
│   │   ├── Debian/
│   │   │   ├── DEBIAN/
│   │   │   │   └── .control
│   │   │   └── usr/
│   │   │       ├── local/
│   │   │       │   └── bin/
│   │   │       │       └── .gitkeep
│   │   │       └── share/
│   │   │           └── applications/
│   │   │               └── res-downloader.desktop
│   │   └── dockerfile
│   └── windows/
│       ├── info.json
│       ├── installer/
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── core/
│   ├── aes.go
│   ├── app.go
│   ├── bind.go
│   ├── config.go
│   ├── downloader.go
│   ├── http.go
│   ├── logger.go
│   ├── middleware.go
│   ├── plugins/
│   │   ├── plugin.default.go
│   │   └── plugin.qq.com.go
│   ├── proxy.go
│   ├── resource.go
│   ├── rule.go
│   ├── shared/
│   │   ├── base.go
│   │   ├── const.go
│   │   ├── plugin.go
│   │   └── utils.go
│   ├── storage.go
│   ├── system.go
│   ├── system_darwin.go
│   ├── system_linux.go
│   ├── system_windows.go
│   └── utils.go
├── docs/
│   ├── .nojekyll
│   ├── _coverpage.md
│   ├── _navbar.md
│   ├── _sidebar.md
│   ├── examples.md
│   ├── getting-started.md
│   ├── index.html
│   ├── installation.md
│   ├── more.md
│   ├── readme.md
│   └── troubleshooting.md
├── frontend/
│   ├── READ-THIS.md
│   ├── README.md
│   ├── auto-imports.d.ts
│   ├── components.d.ts
│   ├── env.d.ts
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── postcss.config.js
│   ├── src/
│   │   ├── App.vue
│   │   ├── api/
│   │   │   ├── app.ts
│   │   │   └── request.ts
│   │   ├── assets/
│   │   │   ├── css/
│   │   │   │   ├── base.css
│   │   │   │   └── main.css
│   │   │   └── js/
│   │   │       └── decrypt.js
│   │   ├── components/
│   │   │   ├── Action.vue
│   │   │   ├── ActionDesc.vue
│   │   │   ├── Footer.vue
│   │   │   ├── ImportJson.vue
│   │   │   ├── NaiveProvider.vue
│   │   │   ├── Password.vue
│   │   │   ├── Preview.vue
│   │   │   ├── Screen.vue
│   │   │   ├── ShowLoading.vue
│   │   │   ├── ShowOrEdit.vue
│   │   │   └── layout/
│   │   │       ├── Index.vue
│   │   │       └── Sider.vue
│   │   ├── func.ts
│   │   ├── i18n.ts
│   │   ├── locales/
│   │   │   ├── en.json
│   │   │   └── zh.json
│   │   ├── main.ts
│   │   ├── router/
│   │   │   └── index.ts
│   │   ├── stores/
│   │   │   ├── event.ts
│   │   │   └── index.ts
│   │   ├── types/
│   │   │   ├── app.d.ts
│   │   │   └── global.d.ts
│   │   └── views/
│   │       ├── index.vue
│   │       └── setting.vue
│   ├── tailwind.config.js
│   ├── tsconfig.app.json
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs/
│       ├── go/
│       │   ├── core/
│       │   │   ├── Bind.d.ts
│       │   │   └── Bind.js
│       │   └── models.ts
│       └── runtime/
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── main.go
└── wails.json
Download .txt
SYMBOL INDEX (465 symbols across 34 files)

FILE: core/aes.go
  type AESCipher (line 13) | type AESCipher struct
    method Encrypt (line 21) | func (a *AESCipher) Encrypt(plainText string) (string, error) {
    method Decrypt (line 44) | func (a *AESCipher) Decrypt(cipherText string) (string, error) {
  function NewAESCipher (line 17) | func NewAESCipher(key string) *AESCipher {

FILE: core/app.go
  type App (line 17) | type App struct
    method Startup (line 129) | func (a *App) Startup(ctx context.Context) {
    method OnExit (line 134) | func (a *App) OnExit() {
    method installCert (line 143) | func (a *App) installCert() (string, error) {
    method OpenSystemProxy (line 156) | func (a *App) OpenSystemProxy() error {
    method UnsetSystemProxy (line 168) | func (a *App) UnsetSystemProxy() error {
    method isInstall (line 180) | func (a *App) isInstall() bool {
    method lock (line 184) | func (a *App) lock() error {
    method ResetApp (line 192) | func (a *App) ResetApp() error {
  function GetApp (line 43) | func GetApp(assets embed.FS, wjs string) *App {

FILE: core/bind.go
  type Bind (line 7) | type Bind struct
    method Config (line 14) | func (b *Bind) Config() *ResponseData {
    method AppInfo (line 18) | func (b *Bind) AppInfo() *ResponseData {
    method ResetApp (line 22) | func (b *Bind) ResetApp() {
  function NewBind (line 10) | func NewBind() *Bind {

FILE: core/config.go
  type MimeInfo (line 13) | type MimeInfo struct
  type Config (line 19) | type Config struct
    method setConfig (line 209) | func (c *Config) setConfig(config Config) {
    method getConfig (line 253) | func (c *Config) getConfig(key string) interface{} {
    method typeSuffix (line 302) | func (c *Config) typeSuffix(mime string) (string, string) {
  function initConfig (line 47) | func initConfig() *Config {
  function getDefaultMimeMap (line 119) | func getDefaultMimeMap() map[string]MimeInfo {
  function getDefaultDownloadDir (line 183) | func getDefaultDownloadDir() string {

FILE: core/downloader.go
  constant MaxRetries (line 18) | MaxRetries  = 3
  constant RetryDelay (line 19) | RetryDelay  = 3 * time.Second
  constant MinPartSize (line 20) | MinPartSize = 1 * 1024 * 1024
  type ProgressCallback (line 23) | type ProgressCallback
  type ProgressChan (line 25) | type ProgressChan struct
  type DownloadTask (line 30) | type DownloadTask struct
  type FileDownloader (line 39) | type FileDownloader struct
    method buildClient (line 72) | func (fd *FileDownloader) buildClient() *http.Client {
    method setHeaders (line 109) | func (fd *FileDownloader) setHeaders(request *http.Request) {
    method init (line 126) | func (fd *FileDownloader) init() error {
    method createDownloadTasks (line 201) | func (fd *FileDownloader) createDownloadTasks() {
    method startDownload (line 241) | func (fd *FileDownloader) startDownload() error {
    method startDownloadTask (line 303) | func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progre...
    method doDownloadTask (line 334) | func (fd *FileDownloader) doDownloadTask(progressChan chan ProgressCha...
    method verifyDownload (line 400) | func (fd *FileDownloader) verifyDownload() error {
    method Start (line 417) | func (fd *FileDownloader) Start() error {
    method Cancel (line 432) | func (fd *FileDownloader) Cancel() {
  function NewFileDownloader (line 56) | func NewFileDownloader(url, filename string, totalTasks int, headers map...

FILE: core/http.go
  type respData (line 20) | type respData
  type ResponseData (line 22) | type ResponseData struct
  type HttpServer (line 28) | type HttpServer struct
    method run (line 37) | func (h *HttpServer) run() {
    method downCert (line 56) | func (h *HttpServer) downCert(w http.ResponseWriter, r *http.Request) {
    method preview (line 65) | func (h *HttpServer) preview(w http.ResponseWriter, r *http.Request) {
    method send (line 111) | func (h *HttpServer) send(t string, data interface{}) {
    method writeJson (line 123) | func (h *HttpServer) writeJson(w http.ResponseWriter, data *ResponseDa...
    method error (line 132) | func (h *HttpServer) error(w http.ResponseWriter, args ...interface{}) {
    method success (line 145) | func (h *HttpServer) success(w http.ResponseWriter, args ...interface{...
    method buildResp (line 159) | func (h *HttpServer) buildResp(code int, message string, data interfac...
    method openDirectoryDialog (line 167) | func (h *HttpServer) openDirectoryDialog(w http.ResponseWriter, r *htt...
    method openFileDialog (line 181) | func (h *HttpServer) openFileDialog(w http.ResponseWriter, r *http.Req...
    method openFolder (line 200) | func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
    method install (line 219) | func (h *HttpServer) install(w http.ResponseWriter, r *http.Request) {
    method setSystemPassword (line 240) | func (h *HttpServer) setSystemPassword(w http.ResponseWriter, r *http....
    method openSystemProxy (line 254) | func (h *HttpServer) openSystemProxy(w http.ResponseWriter, r *http.Re...
    method unsetSystemProxy (line 267) | func (h *HttpServer) unsetSystemProxy(w http.ResponseWriter, r *http.R...
    method isProxy (line 280) | func (h *HttpServer) isProxy(w http.ResponseWriter, r *http.Request) {
    method appInfo (line 286) | func (h *HttpServer) appInfo(w http.ResponseWriter, r *http.Request) {
    method getConfig (line 290) | func (h *HttpServer) getConfig(w http.ResponseWriter, r *http.Request) {
    method setConfig (line 294) | func (h *HttpServer) setConfig(w http.ResponseWriter, r *http.Request) {
    method setType (line 304) | func (h *HttpServer) setType(w http.ResponseWriter, r *http.Request) {
    method clear (line 320) | func (h *HttpServer) clear(w http.ResponseWriter, r *http.Request) {
    method delete (line 325) | func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) {
    method download (line 338) | func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
    method cancel (line 351) | func (h *HttpServer) cancel(w http.ResponseWriter, r *http.Request) {
    method wxFileDecode (line 369) | func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Reque...
    method batchExport (line 389) | func (h *HttpServer) batchExport(w http.ResponseWriter, r *http.Reques...
  function initHttpServer (line 30) | func initHttpServer() *HttpServer {

FILE: core/logger.go
  type Logger (line 12) | type Logger struct
    method Close (line 24) | func (l *Logger) Close() {
    method Err (line 28) | func (l *Logger) Err(err error) {
    method Esg (line 32) | func (l *Logger) Esg(err error, format string, v ...interface{}) {
  function initLogger (line 17) | func initLogger() *Logger {
  function NewLogger (line 37) | func NewLogger(logFile bool, logPath string) *Logger {

FILE: core/middleware.go
  function Middleware (line 8) | func Middleware(next http.Handler) http.Handler {
  function HandleApi (line 17) | func HandleApi(w http.ResponseWriter, r *http.Request) bool {

FILE: core/plugins/plugin.default.go
  type DefaultPlugin (line 14) | type DefaultPlugin struct
    method SetBridge (line 18) | func (p *DefaultPlugin) SetBridge(bridge *shared.Bridge) {
    method Domains (line 22) | func (p *DefaultPlugin) Domains() []string {
    method OnRequest (line 26) | func (p *DefaultPlugin) OnRequest(r *http.Request, ctx *goproxy.ProxyC...
    method OnResponse (line 30) | func (p *DefaultPlugin) OnResponse(resp *http.Response, ctx *goproxy.P...

FILE: core/plugins/plugin.qq.com.go
  type QqPlugin (line 20) | type QqPlugin struct
    method SetBridge (line 24) | func (p *QqPlugin) SetBridge(bridge *shared.Bridge) {
    method Domains (line 28) | func (p *QqPlugin) Domains() []string {
    method OnRequest (line 32) | func (p *QqPlugin) OnRequest(r *http.Request, ctx *goproxy.ProxyCtx) (...
    method OnResponse (line 46) | func (p *QqPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyC...
    method handleWechatRequest (line 122) | func (p *QqPlugin) handleWechatRequest(r *http.Request, ctx *goproxy.P...
    method handleMedia (line 133) | func (p *QqPlugin) handleMedia(body []byte) {
    method buildEmptyResponse (line 242) | func (p *QqPlugin) buildEmptyResponse(r *http.Request) *http.Response {
    method replaceWxJsContent (line 256) | func (p *QqPlugin) replaceWxJsContent(resp *http.Response, old, new st...
    method v (line 270) | func (p *QqPlugin) v() string {

FILE: core/proxy.go
  type Proxy (line 18) | type Proxy struct
    method Startup (line 72) | func (p *Proxy) Startup() {
    method setCa (line 95) | func (p *Proxy) setCa() error {
    method setTransport (line 111) | func (p *Proxy) setTransport() {
    method matchPlugin (line 136) | func (p *Proxy) matchPlugin(host string) shared.Plugin {
    method httpRequestEvent (line 144) | func (p *Proxy) httpRequestEvent(r *http.Request, ctx *goproxy.ProxyCt...
    method httpResponseEvent (line 159) | func (p *Proxy) httpResponseEvent(resp *http.Response, ctx *goproxy.Pr...
  function init (line 26) | func init() {
  function initProxy (line 64) | func initProxy() *Proxy {

FILE: core/resource.go
  type WxFileDecodeResult (line 19) | type WxFileDecodeResult struct
  type Resource (line 24) | type Resource struct
    method buildResType (line 39) | func (r *Resource) buildResType(mime map[string]MimeInfo) map[string]b...
    method mediaIsMarked (line 53) | func (r *Resource) mediaIsMarked(key string) bool {
    method markMedia (line 58) | func (r *Resource) markMedia(key string) {
    method getResType (line 62) | func (r *Resource) getResType(key string) (bool, bool) {
    method setResType (line 69) | func (r *Resource) setResType(n []string) {
    method clear (line 83) | func (r *Resource) clear() {
    method delete (line 87) | func (r *Resource) delete(sign string) {
    method cancel (line 91) | func (r *Resource) cancel(id string) error {
    method download (line 100) | func (r *Resource) download(mediaInfo shared.MediaInfo, decodeStr stri...
    method parseHeaders (line 183) | func (r *Resource) parseHeaders(mediaInfo shared.MediaInfo) (map[strin...
    method wxFileDecode (line 202) | func (r *Resource) wxFileDecode(mediaInfo shared.MediaInfo, fileName, ...
    method progressEventsEmit (line 227) | func (r *Resource) progressEventsEmit(mediaInfo shared.MediaInfo, args...
    method decodeWxFile (line 247) | func (r *Resource) decodeWxFile(fileName, decodeStr string) error {
  function initResource (line 31) | func initResource() *Resource {

FILE: core/rule.go
  type Rule (line 10) | type Rule struct
  type RuleSet (line 18) | type RuleSet struct
    method Load (line 35) | func (r *RuleSet) Load(rs string) error {
    method shouldMitm (line 92) | func (r *RuleSet) shouldMitm(host string) bool {
  function initRule (line 23) | func initRule() *RuleSet {

FILE: core/shared/base.go
  type MediaInfo (line 3) | type MediaInfo struct

FILE: core/shared/const.go
  constant DownloadStatusReady (line 4) | DownloadStatusReady   string = "ready"
  constant DownloadStatusRunning (line 5) | DownloadStatusRunning string = "running"
  constant DownloadStatusError (line 6) | DownloadStatusError   string = "error"
  constant DownloadStatusDone (line 7) | DownloadStatusDone    string = "done"
  constant DownloadStatusHandle (line 8) | DownloadStatusHandle  string = "handle"

FILE: core/shared/plugin.go
  type Bridge (line 8) | type Bridge struct
  type Plugin (line 18) | type Plugin interface

FILE: core/shared/utils.go
  function Md5 (line 20) | func Md5(data string) string {
  function FormatSize (line 27) | func FormatSize(size float64) string {
  function GetTopLevelDomain (line 37) | func GetTopLevelDomain(rawURL string) string {
  function FileExist (line 49) | func FileExist(file string) bool {
  function CreateDirIfNotExist (line 57) | func CreateDirIfNotExist(dir string) error {
  function IsDevelopment (line 64) | func IsDevelopment() bool {
  function GetFileNameFromURL (line 68) | func GetFileNameFromURL(rawUrl string) string {
  function GetCurrentDateTimeFormatted (line 107) | func GetCurrentDateTimeFormatted() string {
  function GetUniqueFileName (line 118) | func GetUniqueFileName(filePath string) string {
  function OpenFolder (line 136) | func OpenFolder(filePath string) error {

FILE: core/storage.go
  type Storage (line 9) | type Storage struct
    method Load (line 21) | func (l *Storage) Load() ([]byte, error) {
    method Store (line 36) | func (l *Storage) Store(data []byte) error {
  function NewStorage (line 14) | func NewStorage(filename string, def []byte) *Storage {

FILE: core/system.go
  type SystemSetup (line 10) | type SystemSetup struct
    method initCert (line 29) | func (s *SystemSetup) initCert() ([]byte, error) {
    method SetPassword (line 45) | func (s *SystemSetup) SetPassword(password string, isCache bool) {
    method checkPasswordFile (line 60) | func (s *SystemSetup) checkPasswordFile() {
  function initSystem (line 17) | func initSystem() *SystemSetup {

FILE: core/system_darwin.go
  method runCommand (line 12) | func (s *SystemSetup) runCommand(args []string) ([]byte, error) {
  method getNetworkServices (line 29) | func (s *SystemSetup) getNetworkServices() ([]string, error) {
  method setProxy (line 61) | func (s *SystemSetup) setProxy() error {
  method unsetProxy (line 90) | func (s *SystemSetup) unsetProxy() error {
  method installCert (line 119) | func (s *SystemSetup) installCert() (string, error) {

FILE: core/system_linux.go
  method getLinuxDistro (line 13) | func (s *SystemSetup) getLinuxDistro() (string, error) {
  method runCommand (line 26) | func (s *SystemSetup) runCommand(args []string, sudo bool) ([]byte, erro...
  method setProxy (line 43) | func (s *SystemSetup) setProxy() error {
  method unsetProxy (line 70) | func (s *SystemSetup) unsetProxy() error {
  method installCert (line 79) | func (s *SystemSetup) installCert() (string, error) {

FILE: core/system_windows.go
  method setProxy (line 14) | func (s *SystemSetup) setProxy() error {
  method unsetProxy (line 33) | func (s *SystemSetup) unsetProxy() error {
  method installCert (line 46) | func (s *SystemSetup) installCert() (string, error) {

FILE: core/utils.go
  function DialogErr (line 7) | func DialogErr(message string) {

FILE: frontend/components.d.ts
  type GlobalComponents (line 9) | interface GlobalComponents {

FILE: frontend/src/api/app.ts
  method install (line 4) | install() {
  method setSystemPassword (line 10) | setSystemPassword(data: object) {
  method openSystemProxy (line 17) | openSystemProxy() {
  method unsetSystemProxy (line 23) | unsetSystemProxy() {
  method openDirectoryDialog (line 29) | openDirectoryDialog() {
  method openFileDialog (line 35) | openFileDialog() {
  method openFolder (line 41) | openFolder(data: object) {
  method isProxy (line 48) | isProxy() {
  method appInfo (line 54) | appInfo() {
  method getConfig (line 60) | getConfig() {
  method setConfig (line 66) | setConfig(data: object) {
  method setType (line 73) | setType(data: string[]) {
  method clear (line 82) | clear() {
  method delete (line 88) | delete(data: object) {
  method cancel (line 95) | cancel(data: object) {
  method download (line 102) | download(data: object) {
  method wxFileDecode (line 109) | wxFileDecode(data: object) {
  method batchExport (line 116) | batchExport(data: object) {

FILE: frontend/src/api/request.ts
  type RequestOptions (line 4) | interface RequestOptions {

FILE: frontend/src/assets/js/decrypt.js
  function locateFile (line 51) | function locateFile(path) {
  function getNativeTypeSize (line 164) | function getNativeTypeSize(type) {
  function warnOnce (line 186) | function warnOnce(text) {
  function convertJsFunctionToWasm (line 198) | function convertJsFunctionToWasm(func, sig) {
  function getEmptyTableSlot (line 286) | function getEmptyTableSlot() {
  function updateTableMap (line 303) | function updateTableMap(offset, count) {
  function addFunction (line 315) | function addFunction(func, sig) {
  function removeFunction (line 348) | function removeFunction(index) {
  function asciiToBinary (line 381) | function asciiToBinary(str) {
  function decode (line 391) | function decode(encoded) {
  function setValue (line 420) | function setValue(ptr, value, type, noSafe) {
  function getValue (line 438) | function getValue(ptr, type, noSafe) {
  function assert (line 473) | function assert(condition, text) {
  function getCFunc (line 480) | function getCFunc(ident) {
  function ccall (line 491) | function ccall(ident, returnType, argTypes, args, opts) {
  function cwrap (line 556) | function cwrap(ident, returnType, argTypes, opts) {
  function allocate (line 581) | function allocate(slab, allocator) {
  function UTF8ArrayToString (line 613) | function UTF8ArrayToString(heap, idx, maxBytesToRead) {
  function UTF8ToString (line 668) | function UTF8ToString(ptr, maxBytesToRead) {
  function stringToUTF8Array (line 686) | function stringToUTF8Array(str, heap, outIdx, maxBytesToWrite) {
  function stringToUTF8 (line 731) | function stringToUTF8(str, outPtr, maxBytesToWrite) {
  function lengthBytesUTF8 (line 736) | function lengthBytesUTF8(str) {
  function AsciiToString (line 760) | function AsciiToString(ptr) {
  function stringToAscii (line 772) | function stringToAscii(str, outPtr) {
  function UTF16ToString (line 781) | function UTF16ToString(ptr, maxBytesToRead) {
  function stringToUTF16 (line 821) | function stringToUTF16(str, outPtr, maxBytesToWrite) {
  function lengthBytesUTF16 (line 843) | function lengthBytesUTF16(str) {
  function UTF32ToString (line 847) | function UTF32ToString(ptr, maxBytesToRead) {
  function stringToUTF32 (line 880) | function stringToUTF32(str, outPtr, maxBytesToWrite) {
  function lengthBytesUTF32 (line 907) | function lengthBytesUTF32(str) {
  function allocateUTF8 (line 922) | function allocateUTF8(str) {
  function allocateUTF8OnStack (line 930) | function allocateUTF8OnStack(str) {
  function writeStringToMemory (line 943) | function writeStringToMemory(string, buffer, dontAddNull) {
  function writeArrayToMemory (line 958) | function writeArrayToMemory(array, buffer) {
  function writeAsciiToMemory (line 963) | function writeAsciiToMemory(str, buffer, dontAddNull) {
  function alignUp (line 974) | function alignUp(x, multiple) {
  function updateGlobalBufferAndViews (line 1001) | function updateGlobalBufferAndViews(buf) {
  function keepRuntimeAlive (line 1041) | function keepRuntimeAlive() {
  function preRun (line 1045) | function preRun() {
  function initRuntime (line 1057) | function initRuntime() {
  function exitRuntime (line 1064) | function exitRuntime() {
  function postRun (line 1068) | function postRun() {
  function addOnPreRun (line 1080) | function addOnPreRun(cb) {
  function addOnInit (line 1084) | function addOnInit(cb) {
  function addOnExit (line 1088) | function addOnExit(cb) {
  function addOnPostRun (line 1091) | function addOnPostRun(cb) {
  function getUniqueRunDependency (line 1127) | function getUniqueRunDependency(id) {
  function addRunDependency (line 1131) | function addRunDependency(id) {
  function removeRunDependency (line 1140) | function removeRunDependency(id) {
  function abort (line 1164) | function abort(what) {
  function isDataURI (line 1205) | function isDataURI(filename) {
  function isFileURI (line 1211) | function isFileURI(filename) {
  function getBinary (line 1217) | function getBinary(file) {
  function getBinaryPromise (line 1233) | function getBinaryPromise() {
  function createWasm (line 1260) | function createWasm() {
  function __asyncjs__wasm_ffmpeg_fopen_sync (line 1365) | function __asyncjs__wasm_ffmpeg_fopen_sync(filename,filelen,acc){ return...
  function __asyncjs__wasm_ffmpeg_fread_sync (line 1366) | function __asyncjs__wasm_ffmpeg_fread_sync(fd,buf,size,ffindex){ return ...
  function callRuntimeCallbacks (line 1372) | function callRuntimeCallbacks(callbacks) {
  function withStackSave (line 1392) | function withStackSave(f) {
  function demangle (line 1398) | function demangle(func) {
  function demangleAll (line 1402) | function demangleAll(text) {
  function getWasmTableEntry (line 1413) | function getWasmTableEntry(funcPtr) {
  function handleException (line 1422) | function handleException(e) {
  function jsStackTrace (line 1434) | function jsStackTrace() {
  function setWasmTableEntry (line 1451) | function setWasmTableEntry(idx, func) {
  function stackTrace (line 1456) | function stackTrace() {
  function ___cxa_allocate_exception (line 1462) | function ___cxa_allocate_exception(size) {
  function _atexit (line 1467) | function _atexit(func, arg) {
  function ___cxa_atexit (line 1469) | function ___cxa_atexit(a0,a1
  function ExceptionInfo (line 1474) | function ExceptionInfo(excPtr) {
  function ___cxa_throw (line 1541) | function ___cxa_throw(ptr, type, destructor) {
  function _gmtime_r (line 1550) | function _gmtime_r(time, tmPtr) {
  function ___gmtime_r (line 1569) | function ___gmtime_r(a0,a1
  function _tzset_impl (line 1574) | function _tzset_impl() {
  function _tzset (line 1612) | function _tzset() {
  function _localtime_r (line 1618) | function _localtime_r(time, tmPtr) {
  function ___localtime_r (line 1645) | function ___localtime_r(a0,a1
  function ___syscall__newselect (line 1668) | function ___syscall__newselect(nfds, readfds, writefds, exceptfds, timeo...
  function setErrNo (line 1671) | function setErrNo(value) {
  function ___syscall_fcntl64 (line 1675) | function ___syscall_fcntl64(fd, cmd, varargs) {SYSCALLS.varargs = varargs;
  function ___syscall_ioctl (line 1680) | function ___syscall_ioctl(fd, op, varargs) {SYSCALLS.varargs = varargs;
  function ___syscall_mkdir (line 1685) | function ___syscall_mkdir(path, mode) {
  function ___syscall_open (line 1690) | function ___syscall_open(path, flags, varargs) {SYSCALLS.varargs = varargs;
  function ___syscall_rmdir (line 1694) | function ___syscall_rmdir(path) {
  function ___syscall_unlink (line 1697) | function ___syscall_unlink(path) {
  function runDestructors (line 1702) | function runDestructors(destructors) {
  function simpleReadValueFromPointer (line 1710) | function simpleReadValueFromPointer(pointer) {
  function makeLegalFunctionName (line 1723) | function makeLegalFunctionName(name) {
  function createNamedFunction (line 1735) | function createNamedFunction(name, body) {
  function extendError (line 1746) | function extendError(baseErrorType, errorName) {
  function throwInternalError (line 1770) | function throwInternalError(message) {
  function whenDependentTypesAreResolved (line 1773) | function whenDependentTypesAreResolved(myTypes, dependentTypes, getTypeC...
  function __embind_finalize_value_object (line 1812) | function __embind_finalize_value_object(structType) {
  function __embind_register_bigint (line 1878) | function __embind_register_bigint(primitiveType, name, size, minRange, m...
  function getShiftFromSize (line 1880) | function getShiftFromSize(size) {
  function embind_init_charCodes (line 1892) | function embind_init_charCodes() {
  function readLatin1String (line 1900) | function readLatin1String(ptr) {
  function throwBindingError (line 1910) | function throwBindingError(message) {
  function registerType (line 1914) | function registerType(rawType, registeredInstance, options) {
  function __embind_register_bool (line 1944) | function __embind_register_bool(rawType, name, size, trueValue, falseVal...
  function ClassHandle_isAliasOf (line 1977) | function ClassHandle_isAliasOf(other) {
  function shallowCopyInternalPointer (line 2003) | function shallowCopyInternalPointer(o) {
  function throwInstanceAlreadyDeleted (line 2015) | function throwInstanceAlreadyDeleted(obj) {
  function detachFinalizer (line 2024) | function detachFinalizer(handle) {}
  function runDestructor (line 2026) | function runDestructor($$) {
  function releaseClassHandle (line 2033) | function releaseClassHandle($$) {
  function attachFinalizer (line 2040) | function attachFinalizer(handle) {
  function ClassHandle_clone (line 2068) | function ClassHandle_clone() {
  function ClassHandle_delete (line 2089) | function ClassHandle_delete() {
  function ClassHandle_isDeleted (line 2107) | function ClassHandle_isDeleted() {
  function flushPendingDeletes (line 2115) | function flushPendingDeletes() {
  function ClassHandle_deleteLater (line 2122) | function ClassHandle_deleteLater() {
  function init_ClassHandle (line 2136) | function init_ClassHandle() {
  function ClassHandle (line 2143) | function ClassHandle() {
  function ensureOverloadTable (line 2148) | function ensureOverloadTable(proto, methodName, humanName) {
  function exposePublicSymbol (line 2165) | function exposePublicSymbol(name, value, numArguments) {
  function RegisteredClass (line 2189) | function RegisteredClass(
  function upcastPointer (line 2210) | function upcastPointer(ptr, ptrClass, desiredClass) {
  function constNoSmartPtrRawPointerToWireType (line 2220) | function constNoSmartPtrRawPointerToWireType(destructors, handle) {
  function genericPointerToWireType (line 2239) | function genericPointerToWireType(destructors, handle) {
  function nonConstNoSmartPtrRawPointerToWireType (line 2315) | function nonConstNoSmartPtrRawPointerToWireType(destructors, handle) {
  function RegisteredPointer_getPointee (line 2337) | function RegisteredPointer_getPointee(ptr) {
  function RegisteredPointer_destructor (line 2344) | function RegisteredPointer_destructor(ptr) {
  function RegisteredPointer_deleteObject (line 2350) | function RegisteredPointer_deleteObject(handle) {
  function downcastPointer (line 2356) | function downcastPointer(ptr, ptrClass, desiredClass) {
  function getInheritedInstanceCount (line 2371) | function getInheritedInstanceCount() {
  function getLiveInheritedInstances (line 2375) | function getLiveInheritedInstances() {
  function setDelayFunction (line 2385) | function setDelayFunction(fn) {
  function init_embind (line 2391) | function init_embind() {
  function getBasestPointer (line 2399) | function getBasestPointer(class_, ptr) {
  function getInheritedInstance (line 2409) | function getInheritedInstance(class_, ptr) {
  function makeClassHandle (line 2414) | function makeClassHandle(prototype, record) {
  function RegisteredPointer_fromWireType (line 2430) | function RegisteredPointer_fromWireType(ptr) {
  function init_RegisteredPointer (line 2505) | function init_RegisteredPointer() {
  function RegisteredPointer (line 2521) | function RegisteredPointer(
  function replacePublicSymbol (line 2568) | function replacePublicSymbol(name, value, numArguments) {
  function dynCallLegacy (line 2582) | function dynCallLegacy(sig, ptr, args) {
  function dynCall (line 2586) | function dynCall(sig, ptr, args) {
  function getDynCaller (line 2589) | function getDynCaller(sig, ptr) {
  function embind__requireFunction (line 2599) | function embind__requireFunction(signature, rawFunction) {
  function getTypeName (line 2615) | function getTypeName(type) {
  function throwUnboundTypeError (line 2621) | function throwUnboundTypeError(message, types) {
  function __embind_register_class (line 2642) | function __embind_register_class(
  function heap32VectorToArray (line 2751) | function heap32VectorToArray(count, firstElement) {
  function __embind_register_class_constructor (line 2759) | function __embind_register_class_constructor(
  function new_ (line 2797) | function new_(constructor, argumentList) {
  function runAndAbortIfError (line 2820) | function runAndAbortIfError(func) {
  function callUserCallback (line 2828) | function callUserCallback(func, synchronous) {
  function runtimeKeepalivePush (line 2844) | function runtimeKeepalivePush() {
  function runtimeKeepalivePop (line 2848) | function runtimeKeepalivePop() {
  function craftInvokerFunction (line 3023) | function craftInvokerFunction(humanName, argTypes, classType, cppInvoker...
  function __embind_register_class_function (line 3131) | function __embind_register_class_function(
  function __emval_decref (line 3197) | function __emval_decref(handle) {
  function count_emval_handles (line 3204) | function count_emval_handles() {
  function get_first_emval (line 3214) | function get_first_emval() {
  function init_emval (line 3222) | function init_emval() {
  function __embind_register_emval (line 3247) | function __embind_register_emval(rawType, name) {
  function _embind_repr (line 3268) | function _embind_repr(v) {
  function floatReadValueFromPointer (line 3280) | function floatReadValueFromPointer(name, shift) {
  function __embind_register_float (line 3292) | function __embind_register_float(rawType, name, size) {
  function __embind_register_function (line 3311) | function __embind_register_function(name, argCount, rawArgTypesAddr, sig...
  function integerReadValueFromPointer (line 3328) | function integerReadValueFromPointer(name, shift, signed) {
  function __embind_register_integer (line 3344) | function __embind_register_integer(primitiveType, name, size, minRange, ...
  function __embind_register_memory_view (line 3385) | function __embind_register_memory_view(rawType, dataTypeIndex, name) {
  function __embind_register_std_string (line 3418) | function __embind_register_std_string(rawType, name) {
  function __embind_register_std_wstring (line 3510) | function __embind_register_std_wstring(rawType, charSize, name) {
  function __embind_register_value_object (line 3578) | function __embind_register_value_object(
  function __embind_register_value_object_field (line 3594) | function __embind_register_value_object_field(
  function __embind_register_void (line 3617) | function __embind_register_void(rawType, name) {
  function _abort (line 3633) | function _abort() {
  function _clock (line 3637) | function _clock() {
  function _clock_gettime (line 3651) | function _clock_gettime(clk_id, tp) {
  function readAsmConstArgs (line 3668) | function readAsmConstArgs(sigPtr, buf) {
  function _emscripten_asm_const_int (line 3685) | function _emscripten_asm_const_int(code, sigPtr, argbuf) {
  function emscripten_realloc_buffer (line 3696) | function emscripten_realloc_buffer(size) {
  function _emscripten_resize_heap (line 3707) | function _emscripten_resize_heap(requestedSize) {
  function getExecutableName (line 3752) | function getExecutableName() {
  function getEnvStrings (line 3755) | function getEnvStrings() {
  function _environ_get (line 3785) | function _environ_get(__environ, environ_buf) {
  function _environ_sizes_get (line 3796) | function _environ_sizes_get(penviron_count, penviron_buf_size) {
  function _exit (line 3807) | function _exit(status) {
  function _fd_close (line 3813) | function _fd_close(fd) {
  function _fd_fdstat_get (line 3817) | function _fd_fdstat_get(fd, pbuf) {
  function _fd_read (line 3827) | function _fd_read(fd, iov, iovcnt, pnum) {
  function _fd_seek (line 3834) | function _fd_seek(fd, offset_low, offset_high, whence, newOffset) {
  function flush_NO_FILESYSTEM (line 3837) | function flush_NO_FILESYSTEM() {
  function _fd_write (line 3844) | function _fd_write(fd, iov, iovcnt, pnum) {
  function _gettimeofday (line 3861) | function _gettimeofday(ptr) {
  function _mktime (line 3870) | function _mktime(tmPtr) {
  function _setTempRet0 (line 3912) | function _setTempRet0(val) {
  function __isLeapYear (line 3916) | function __isLeapYear(year) {
  function __arraySum (line 3920) | function __arraySum(array, index) {
  function __addDays (line 3931) | function __addDays(date, days) {
  function _strftime (line 3957) | function _strftime(s, maxsize, format, tm) {
  function _strftime_l (line 4275) | function _strftime_l(s, maxsize, format, tm) {
  function _time (line 4279) | function _time(ptr) {
  function intArrayFromString (line 4301) | function intArrayFromString(stringy, dontAddNull, length) {
  function intArrayToString (line 4309) | function intArrayToString(array) {
  function ExitStatus (line 4636) | function ExitStatus(status) {
  function run (line 4651) | function run(args) {
  function exit (line 4697) | function exit(status, implicit) {
  function procExit (line 4708) | function procExit(code) {
  function wasm_isaac_generate (line 4731) | function wasm_isaac_generate(t, e) {
  function get_decryptor_array (line 4737) | function get_decryptor_array(seed) {

FILE: frontend/src/types/app.d.ts
  type App (line 2) | interface App {
  type MimeMap (line 9) | interface MimeMap {
  type Config (line 14) | interface Config {
  type MediaInfo (line 37) | interface MediaInfo {
  type DownloadProgress (line 54) | interface DownloadProgress {
  type Message (line 61) | interface Message {
  type Handle (line 66) | interface Handle {
  type Res (line 71) | interface Res<T = any> {

FILE: frontend/src/types/global.d.ts
  type Window (line 1) | interface Window {

FILE: frontend/wailsjs/go/core/Bind.js
  function AppInfo (line 5) | function AppInfo() {
  function Config (line 9) | function Config() {
  function ResetApp (line 13) | function ResetApp() {

FILE: frontend/wailsjs/go/models.ts
  class ResponseData (line 3) | class ResponseData {
    method createFrom (line 8) | static createFrom(source: any = {}) {
    method constructor (line 12) | constructor(source: any = {}) {

FILE: frontend/wailsjs/runtime/runtime.d.ts
  type Position (line 11) | interface Position {
  type Size (line 16) | interface Size {
  type Screen (line 21) | interface Screen {
  type EnvironmentInfo (line 29) | interface EnvironmentInfo {

FILE: frontend/wailsjs/runtime/runtime.js
  function LogPrint (line 11) | function LogPrint(message) {
  function LogTrace (line 15) | function LogTrace(message) {
  function LogDebug (line 19) | function LogDebug(message) {
  function LogInfo (line 23) | function LogInfo(message) {
  function LogWarning (line 27) | function LogWarning(message) {
  function LogError (line 31) | function LogError(message) {
  function LogFatal (line 35) | function LogFatal(message) {
  function EventsOnMultiple (line 39) | function EventsOnMultiple(eventName, callback, maxCallbacks) {
  function EventsOn (line 43) | function EventsOn(eventName, callback) {
  function EventsOff (line 47) | function EventsOff(eventName, ...additionalEventNames) {
  function EventsOnce (line 51) | function EventsOnce(eventName, callback) {
  function EventsEmit (line 55) | function EventsEmit(eventName) {
  function WindowReload (line 60) | function WindowReload() {
  function WindowReloadApp (line 64) | function WindowReloadApp() {
  function WindowSetAlwaysOnTop (line 68) | function WindowSetAlwaysOnTop(b) {
  function WindowSetSystemDefaultTheme (line 72) | function WindowSetSystemDefaultTheme() {
  function WindowSetLightTheme (line 76) | function WindowSetLightTheme() {
  function WindowSetDarkTheme (line 80) | function WindowSetDarkTheme() {
  function WindowCenter (line 84) | function WindowCenter() {
  function WindowSetTitle (line 88) | function WindowSetTitle(title) {
  function WindowFullscreen (line 92) | function WindowFullscreen() {
  function WindowUnfullscreen (line 96) | function WindowUnfullscreen() {
  function WindowIsFullscreen (line 100) | function WindowIsFullscreen() {
  function WindowGetSize (line 104) | function WindowGetSize() {
  function WindowSetSize (line 108) | function WindowSetSize(width, height) {
  function WindowSetMaxSize (line 112) | function WindowSetMaxSize(width, height) {
  function WindowSetMinSize (line 116) | function WindowSetMinSize(width, height) {
  function WindowSetPosition (line 120) | function WindowSetPosition(x, y) {
  function WindowGetPosition (line 124) | function WindowGetPosition() {
  function WindowHide (line 128) | function WindowHide() {
  function WindowShow (line 132) | function WindowShow() {
  function WindowMaximise (line 136) | function WindowMaximise() {
  function WindowToggleMaximise (line 140) | function WindowToggleMaximise() {
  function WindowUnmaximise (line 144) | function WindowUnmaximise() {
  function WindowIsMaximised (line 148) | function WindowIsMaximised() {
  function WindowMinimise (line 152) | function WindowMinimise() {
  function WindowUnminimise (line 156) | function WindowUnminimise() {
  function WindowSetBackgroundColour (line 160) | function WindowSetBackgroundColour(R, G, B, A) {
  function ScreenGetAll (line 164) | function ScreenGetAll() {
  function WindowIsMinimised (line 168) | function WindowIsMinimised() {
  function WindowIsNormal (line 172) | function WindowIsNormal() {
  function BrowserOpenURL (line 176) | function BrowserOpenURL(url) {
  function Environment (line 180) | function Environment() {
  function Quit (line 184) | function Quit() {
  function Hide (line 188) | function Hide() {
  function Show (line 192) | function Show() {
  function ClipboardGetText (line 196) | function ClipboardGetText() {
  function ClipboardSetText (line 200) | function ClipboardSetText(text) {
  function OnFileDrop (line 221) | function OnFileDrop(callback, useDropTarget) {
  function OnFileDropOff (line 228) | function OnFileDropOff() {
  function CanResolveFilePaths (line 232) | function CanResolveFilePaths() {
  function ResolveFilePaths (line 236) | function ResolveFilePaths(files) {

FILE: main.go
  function main (line 29) | func main() {
Condensed preview — 111 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (5,532K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 1289,
    "preview": "name: \"Bug Report \\\\ 问题反馈\"\ndescription: \"Create a report to help us improve \\\\ 帮助改进\"\nlabels: [\"Bug\"]\n\nbody:\n  - type: in"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 26,
    "preview": "blank_issues_enabled: true"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 1031,
    "preview": "name: \"Feature Request \\\\ 功能建议\"\ndescription: \"Suggest an idea for this project \\\\ 为这个项目提出一个新想法\"\nlabels: [\"Enhancement\"]\n"
  },
  {
    "path": ".gitignore",
    "chars": 52,
    "preview": ".idea\nbuild/bin\nnode_modules\nfrontend/dist\n.DS_Store"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README-EN.md",
    "chars": 4160,
    "preview": "<div align=\"center\">\n\n<a href=\"https://github.com/putyy/res-downloader\"><img src=\"build/appicon.png\" width=\"120\"/></a>\n<"
  },
  {
    "path": "README.md",
    "chars": 2659,
    "preview": "<div align=\"center\">\n\n<a href=\"https://github.com/putyy/res-downloader\"><img src=\"build/appicon.png\" width=\"120\"/></a>\n<"
  },
  {
    "path": "build/README.md",
    "chars": 3976,
    "preview": "## Mac\n```bash\nwails build -platform \"darwin/universal\"\ncreate-dmg 'build/bin/res-downloader.app' --overwrite ./build/bi"
  },
  {
    "path": "build/darwin/Info.dev.plist",
    "chars": 2308,
    "preview": "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1"
  },
  {
    "path": "build/darwin/Info.plist",
    "chars": 2168,
    "preview": "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1"
  },
  {
    "path": "build/linux/.gitignore",
    "chars": 93,
    "preview": "!.gitkeep\ndebian/usr/local/bin/*\ndebian/DEBIAN/control\nAppImage/usr/bin/*\nAppImage/usr/lib/*\n"
  },
  {
    "path": "build/linux/AppImage/res-downloader.desktop",
    "chars": 278,
    "preview": "[Desktop Entry]\nType=Application\nName=res-downloader\nComment=This is a high-value and high-performance and diverse resou"
  },
  {
    "path": "build/linux/AppImage/usr/bin/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "build/linux/AppImage/usr/share/applications/res-downloader.desktop",
    "chars": 278,
    "preview": "[Desktop Entry]\nType=Application\nName=res-downloader\nComment=This is a high-value and high-performance and diverse resou"
  },
  {
    "path": "build/linux/Arch/res-downloader.desktop",
    "chars": 236,
    "preview": "[Desktop Entry]\nType=Application\nName=res-downloader\nComment=This is a high-value and high-performance and diverse resou"
  },
  {
    "path": "build/linux/Debian/DEBIAN/.control",
    "chars": 324,
    "preview": "Package: res-downloader\nVersion: {{Version}}\nSection: utils\nPriority: optional\nArchitecture: {{Architecture}}\nDepends: l"
  },
  {
    "path": "build/linux/Debian/usr/local/bin/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "build/linux/Debian/usr/share/applications/res-downloader.desktop",
    "chars": 288,
    "preview": "[Desktop Entry]\nType=Application\nName=res-downloader\nComment=This is a high-value and high-performance and diverse resou"
  },
  {
    "path": "build/linux/dockerfile",
    "chars": 532,
    "preview": "FROM golang:1.24.2-bookworm\n\nWORKDIR /\n\nRUN apt-get update && \\\n    apt-get install -y --fix-missing \\\n    build-essenti"
  },
  {
    "path": "build/windows/info.json",
    "chars": 356,
    "preview": "{\n\t\"fixed\": {\n\t\t\"file_version\": \"{{.Info.ProductVersion}}\"\n\t},\n\t\"info\": {\n\t\t\"0000\": {\n\t\t\t\"ProductVersion\": \"{{.Info.Prod"
  },
  {
    "path": "build/windows/installer/project.nsi",
    "chars": 4862,
    "preview": "Unicode true\n\n####\n## Please note: Template replacements don't work in this file. They are provided with default defines"
  },
  {
    "path": "build/windows/installer/wails_tools.nsh",
    "chars": 7569,
    "preview": "# DO NOT EDIT - Generated automatically by `wails build`\n\n!include \"x64.nsh\"\n!include \"WinVer.nsh\"\n!include \"FileFunc.ns"
  },
  {
    "path": "build/windows/wails.exe.manifest",
    "chars": 1036,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly manifestVersion=\"1.0\" xmlns=\"urn:schemas-microsoft-com"
  },
  {
    "path": "core/aes.go",
    "chars": 1676,
    "preview": "package core\n\nimport (\n\t\"bytes\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"io\"\n)\n\ntype"
  },
  {
    "path": "core/app.go",
    "chars": 6353,
    "preview": "package core\n\nimport (\n\t\"context\"\n\t\"embed\"\n\t\"fmt\"\n\t\"github.com/vrischmann/userdir\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"r"
  },
  {
    "path": "core/bind.go",
    "chars": 409,
    "preview": "package core\n\nimport (\n\t\"github.com/wailsapp/wails/v2/pkg/runtime\"\n)\n\ntype Bind struct {\n}\n\nfunc NewBind() *Bind {\n\tretu"
  },
  {
    "path": "core/config.go",
    "chars": 10151,
    "preview": "package core\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n)\n\ntype MimeInfo"
  },
  {
    "path": "core/downloader.go",
    "chars": 10587,
    "preview": "package core\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"res-downloader/core/share"
  },
  {
    "path": "core/http.go",
    "chars": 9424,
    "preview": "package core\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath"
  },
  {
    "path": "core/logger.go",
    "chars": 1321,
    "preview": "package core\n\nimport (\n\t\"fmt\"\n\t\"github.com/rs/zerolog\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"res-downloader/core/shared\"\n)\n\ntyp"
  },
  {
    "path": "core/middleware.go",
    "chars": 1950,
    "preview": "package core\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n)\n\nfunc Middleware(next http.Handler) http.Handler {\n\treturn http.HandlerF"
  },
  {
    "path": "core/plugins/plugin.default.go",
    "chars": 2139,
    "preview": "package plugins\n\nimport (\n\t\"encoding/json\"\n\t\"github.com/elazarl/goproxy\"\n\tgonanoid \"github.com/matoous/go-nanoid/v2\"\n\t\"n"
  },
  {
    "path": "core/plugins/plugin.qq.com.go",
    "chars": 6928,
    "preview": "package plugins\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/elazarl/goproxy\"\n\tgonanoid \"github.com/matoous/g"
  },
  {
    "path": "core/proxy.go",
    "chars": 4344,
    "preview": "package core\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"res-downloader/core/plugi"
  },
  {
    "path": "core/resource.go",
    "chars": 6771,
    "preview": "package core\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"re"
  },
  {
    "path": "core/rule.go",
    "chars": 2213,
    "preview": "package core\n\nimport (\n\t\"bufio\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n)\n\ntype Rule struct {\n\traw        string\n\tisNeg      bool // 是"
  },
  {
    "path": "core/shared/base.go",
    "chars": 334,
    "preview": "package shared\n\ntype MediaInfo struct {\n\tId          string\n\tUrl         string\n\tUrlSign     string\n\tCoverUrl    string\n"
  },
  {
    "path": "core/shared/const.go",
    "chars": 257,
    "preview": "package shared\n\nconst (\n\tDownloadStatusReady   string = \"ready\" // task create but not start\n\tDownloadStatusRunning stri"
  },
  {
    "path": "core/shared/plugin.go",
    "chars": 584,
    "preview": "package shared\n\nimport (\n\t\"github.com/elazarl/goproxy\"\n\t\"net/http\"\n)\n\ntype Bridge struct {\n\tGetVersion    func() string\n"
  },
  {
    "path": "core/shared/utils.go",
    "chars": 3224,
    "preview": "package shared\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"golang.org/x/net/publicsuffix\"\n\t\"net/url\"\n\t\"os"
  },
  {
    "path": "core/storage.go",
    "chars": 694,
    "preview": "package core\n\nimport (\n\t\"os\"\n\t\"path\"\n\t\"res-downloader/core/shared\"\n)\n\ntype Storage struct {\n\tfileName string\n\tdef      ["
  },
  {
    "path": "core/system.go",
    "chars": 1675,
    "preview": "package core\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n)\n\ntype SystemSetup struct {\n\tCertFile  string\n\tCacheFile s"
  },
  {
    "path": "core/system_darwin.go",
    "chars": 3220,
    "preview": "//go:build darwin\n\npackage core\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\nfunc (s *SystemSetup) runCommand(args"
  },
  {
    "path": "core/system_linux.go",
    "chars": 3856,
    "preview": "//go:build linux\n\npackage core\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\nfunc (s *SystemSetup) getLinuxDi"
  },
  {
    "path": "core/system_windows.go",
    "chars": 2219,
    "preview": "//go:build windows\n\npackage core\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"golang.org/x/sys/windows\"\n\t\"golang"
  },
  {
    "path": "core/utils.go",
    "chars": 299,
    "preview": "package core\n\nimport (\n\t\"github.com/wailsapp/wails/v2/pkg/runtime\"\n)\n\nfunc DialogErr(message string) {\n\t_, _ = runtime.M"
  },
  {
    "path": "docs/.nojekyll",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/_coverpage.md",
    "chars": 261,
    "preview": "<div align=\"center\">\n<a href=\"https://github.com/putyy/res-downloader\"><img src=\"images/logo.png\" width=\"120\"/></a>\n<h1>"
  },
  {
    "path": "docs/_navbar.md",
    "chars": 224,
    "preview": "* [论坛](https://s.gowas.cn/d/4089)\n* [反馈](https://github.com/putyy/res-downloader/issues)\n* [日志](https://github.com/putyy"
  },
  {
    "path": "docs/_sidebar.md",
    "chars": 142,
    "preview": "* [简介](readme.md)\n* [快速开始](getting-started.md)\n* [安装指南](installation.md)\n* [功能演示](examples.md)\n* [更多说明](more.md)\n* [常见问题"
  },
  {
    "path": "docs/examples.md",
    "chars": 392,
    "preview": "## 开启代理  \n- 安装完成后开启代理 (最新版本为“开启抓取”),如图:  \n![](images/examples-1.png ':size=50%')\n\n## 拦截资源\n### 视频号  \n- 打开视频号即可看到本软件中拦截到的资"
  },
  {
    "path": "docs/getting-started.md",
    "chars": 387,
    "preview": "## 软件下载\n🆕 [github下载](https://github.com/putyy/res-downloader/releases)  \n🆕 [蓝奏云下载 密码:9vs5](https://wwjv.lanzoum.com/b04w"
  },
  {
    "path": "docs/index.html",
    "chars": 1849,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>res-downloader</title>\n  <meta http-equiv=\"X-U"
  },
  {
    "path": "docs/installation.md",
    "chars": 515,
    "preview": "## 下载安装文件\n- windows下载.exe结尾的,根据自己的系统架构下载合适的安装文件,通常下载带有“win_amd64.exe”或“x64-installer.exe”结尾的文件  \n- Mac下载.dmg结尾即可  \n- Lin"
  },
  {
    "path": "docs/more.md",
    "chars": 524,
    "preview": "## 清空列表、类型筛选\n- 当资源列表过大时,无法快速找到需要的资源,这时可以先清空列表再去刷新需要的资源页面  \n- 资源列表过多,可以快速根据需要的资源类型进行筛选  \n![more-4.png](images/more-1.png "
  },
  {
    "path": "docs/readme.md",
    "chars": 2033,
    "preview": "<div align=\"center\">\n\n<a href=\"https://github.com/putyy/res-downloader\"><img src=\"images/logo.png\" width=\"120\"/></a>\n<h1"
  },
  {
    "path": "docs/troubleshooting.md",
    "chars": 2036,
    "preview": "## 视频号拦截了一大堆 找不到想要的\n> 设置里面关闭全量拦截,将视频转发好友后打开\n\n## 某某网址拦截不了?\n> 本软件并非万能的,所以有一些应用拦截不了很正常,实现原理 & 初衷如下,\n```\n本工具通过代理方式实现网络抓包,并筛选"
  },
  {
    "path": "frontend/READ-THIS.md",
    "chars": 236,
    "preview": "This template uses a work around as the default template does not compile due to this issue:\nhttps://github.com/vuejs/co"
  },
  {
    "path": "frontend/README.md",
    "chars": 1383,
    "preview": "# Vue 3 + TypeScript + Vite\n\nThis template should help get you started developing with Vue 3 and TypeScript in Vite. The"
  },
  {
    "path": "frontend/auto-imports.d.ts",
    "chars": 3904,
    "preview": "/* eslint-disable */\n/* prettier-ignore */\n// @ts-nocheck\n// noinspection JSUnusedGlobalSymbols\n// Generated by unplugin"
  },
  {
    "path": "frontend/components.d.ts",
    "chars": 2922,
    "preview": "/* eslint-disable */\n// @ts-nocheck\n// Generated by unplugin-vue-components\n// Read more: https://github.com/vuejs/core/"
  },
  {
    "path": "frontend/env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "frontend/index.html",
    "chars": 314,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <meta content=\"width=device-width, initial-scale"
  },
  {
    "path": "frontend/package.json",
    "chars": 916,
    "preview": "{\n  \"name\": \"frontend\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n "
  },
  {
    "path": "frontend/package.json.md5",
    "chars": 32,
    "preview": "853e4a476a4f41b58875469cfb541133"
  },
  {
    "path": "frontend/postcss.config.js",
    "chars": 80,
    "preview": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "frontend/src/App.vue",
    "chars": 1413,
    "preview": "<template>\n  <NConfigProvider class=\"h-full\" :theme=\"theme\" :locale=\"uiLocale\">\n    <NaiveProvider>\n      <RouterView/>\n"
  },
  {
    "path": "frontend/src/api/app.ts",
    "chars": 2668,
    "preview": "import request from '@/api/request'\n\nexport default {\n    install() {\n        return request({\n            url: '/api/in"
  },
  {
    "path": "frontend/src/api/request.ts",
    "chars": 856,
    "preview": "import type {AxiosResponse, InternalAxiosRequestConfig} from 'axios'\nimport axios from 'axios'\n\ninterface RequestOptions"
  },
  {
    "path": "frontend/src/assets/css/base.css",
    "chars": 2067,
    "preview": "/* color palette from <https://github.com/vuejs/theme> */\n:root {\n  --vt-c-white: #ffffff;\n  --vt-c-white-soft: #f8f8f8;"
  },
  {
    "path": "frontend/src/assets/css/main.css",
    "chars": 267,
    "preview": "@import 'base.css';\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n#app {\n  width: 100vw;\n  height: 100vh;"
  },
  {
    "path": "frontend/src/assets/js/decrypt.js",
    "chars": 5223048,
    "preview": "\n\n// The Module object: Our interface to the outside world. We import\n// and export values on it. There are various ways"
  },
  {
    "path": "frontend/src/components/Action.vue",
    "chars": 4238,
    "preview": "<template>\n  <div style=\"--wails-draggable:no-drag\" class=\"grid grid-cols-3 gap-1.5\">\n    <n-icon\n        size=\"30\"\n    "
  },
  {
    "path": "frontend/src/components/ActionDesc.vue",
    "chars": 4103,
    "preview": "<template>\n  <div class=\"flex items-center\">\n    <span>\n      {{ t('index.operation') }}\n    </span>\n    <NPopover trigg"
  },
  {
    "path": "frontend/src/components/Footer.vue",
    "chars": 2584,
    "preview": "<template>\n  <NModal\n      :show=\"showModal\"\n      :on-update:show=\"changeShow\"\n      style=\"--wails-draggable:no-drag\"\n"
  },
  {
    "path": "frontend/src/components/ImportJson.vue",
    "chars": 1096,
    "preview": "<template>\n  <NModal\n      :show=\"showModal\"\n      :on-update:show=\"changeShow\"\n      style=\"--wails-draggable:no-drag\"\n"
  },
  {
    "path": "frontend/src/components/NaiveProvider.vue",
    "chars": 800,
    "preview": "<template>\n  <NLoadingBarProvider>\n    <NDialogProvider>\n      <NNotificationProvider>\n        <NMessageProvider>\n      "
  },
  {
    "path": "frontend/src/components/Password.vue",
    "chars": 1628,
    "preview": "<template>\n  <n-modal\n      :show=\"showModal\"\n      :on-update:show=\"changeShow\"\n      style=\"--wails-draggable:no-drag\""
  },
  {
    "path": "frontend/src/components/Preview.vue",
    "chars": 5714,
    "preview": "<template>\n  <NModal\n      style=\"--wails-draggable:no-drag\"\n      :show=\"showModal\"\n      :on-update:show=\"changeShow\"\n"
  },
  {
    "path": "frontend/src/components/Screen.vue",
    "chars": 3399,
    "preview": "<template>\r\n  <div class=\"flex justify-around px-2 pt-3\">\r\n    <button\r\n        class=\"w-4 h-4 rounded-full bg-[#ff5c57]"
  },
  {
    "path": "frontend/src/components/ShowLoading.vue",
    "chars": 727,
    "preview": "<template>\n  <div class=\"fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50\" v-if=\"isLoading\">\n "
  },
  {
    "path": "frontend/src/components/ShowOrEdit.vue",
    "chars": 1193,
    "preview": "<template>\n  <div\n      class=\"min-h-6\"\n      @click=\"handleOnClick\"\n  >\n    <n-input\n        v-if=\"isEdit\"\n        ref="
  },
  {
    "path": "frontend/src/components/layout/Index.vue",
    "chars": 725,
    "preview": "<template>\r\n<div class=\"w-full h-full transition-all p-0\">\r\n  <div class=\"h-full overflow-hidden flex justify-center ite"
  },
  {
    "path": "frontend/src/components/layout/Sider.vue",
    "chars": 6183,
    "preview": "<template>\r\n  <div class=\"flex pb-2 flex-col h-full min-w-[80px] border-r border-slate-100 dark:border-slate-900\">\r\n    "
  },
  {
    "path": "frontend/src/func.ts",
    "chars": 1255,
    "preview": "const ipv4Regex = /^(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)){3}$/\nconst domainRegex = /^"
  },
  {
    "path": "frontend/src/i18n.ts",
    "chars": 245,
    "preview": "import {createI18n} from 'vue-i18n'\nimport en from './locales/en.json'\nimport zh from './locales/zh.json'\n\nconst i18n = "
  },
  {
    "path": "frontend/src/locales/en.json",
    "chars": 6404,
    "preview": "{\n  \"common\": {\n    \"copy_success\": \"Copy Success\",\n    \"copy_fail\": \"Copy Failed\",\n    \"file_path\": \"File Path\",\n    \"l"
  },
  {
    "path": "frontend/src/locales/zh.json",
    "chars": 4200,
    "preview": "{\n  \"common\": {\n    \"copy_success\": \"复制成功\",\n    \"copy_fail\": \"复制失败\",\n    \"file_path\": \"文件路径\",\n    \"loading\": \"loading\",\n"
  },
  {
    "path": "frontend/src/main.ts",
    "chars": 272,
    "preview": "import './assets/css/main.css'\n\nimport {createApp} from 'vue'\nimport {createPinia} from 'pinia'\nimport i18n from './i18n"
  },
  {
    "path": "frontend/src/router/index.ts",
    "chars": 644,
    "preview": "import {createRouter, createWebHashHistory} from 'vue-router'\n\nconst routes = [\n  {\n    path: \"/\",\n    name: \"layout\",\n "
  },
  {
    "path": "frontend/src/stores/event.ts",
    "chars": 707,
    "preview": "import {defineStore} from \"pinia\"\nimport {ref} from \"vue\"\nimport {EventsOn} from \"../../wailsjs/runtime\"\nimport {appType"
  },
  {
    "path": "frontend/src/stores/index.ts",
    "chars": 2519,
    "preview": "import {defineStore} from 'pinia'\nimport {ref} from \"vue\"\nimport type {appType} from \"@/types/app\"\nimport appApi from \"@"
  },
  {
    "path": "frontend/src/types/app.d.ts",
    "chars": 1611,
    "preview": "export namespace appType {\n    interface App {\n        AppName: string\n        Version: string\n        Description: stri"
  },
  {
    "path": "frontend/src/types/global.d.ts",
    "chars": 473,
    "preview": "interface Window {\n    $loadingBar?: import('naive-ui').LoadingBarProviderInst\n    $dialog?: import('naive-ui').DialogPr"
  },
  {
    "path": "frontend/src/views/index.vue",
    "chars": 30454,
    "preview": "<template>\n  <div class=\"h-full flex flex-col px-5 pt-5 overflow-y-auto [&::-webkit-scrollbar]:hidden\">\n    <div class=\""
  },
  {
    "path": "frontend/src/views/setting.vue",
    "chars": 11466,
    "preview": "<template>\n  <div class=\"h-full relative p-5 overflow-y-auto [&::-webkit-scrollbar]:hidden\" :key=\"renderKey\">\n    <NTabs"
  },
  {
    "path": "frontend/tailwind.config.js",
    "chars": 242,
    "preview": "/** @type {import('tailwindcss').Config} */\nexport default {\n  darkMode: 'selector',\n  content: [\n    \"./index.html\",\n  "
  },
  {
    "path": "frontend/tsconfig.app.json",
    "chars": 464,
    "preview": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.dom.json\",\n  \"include\": [\n    \"src/**/*.ts\",\n    \"src/**/*.d.ts\",\n    \"src/**/*.t"
  },
  {
    "path": "frontend/tsconfig.json",
    "chars": 627,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"ESNext\",\n    \"modul"
  },
  {
    "path": "frontend/tsconfig.node.json",
    "chars": 192,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"allowSynthe"
  },
  {
    "path": "frontend/vite.config.ts",
    "chars": 1021,
    "preview": "import {defineConfig, loadEnv} from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport path from 'path'\nimport AutoImpor"
  },
  {
    "path": "frontend/wailsjs/go/core/Bind.d.ts",
    "chars": 296,
    "preview": "// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL\n// This file is automatically generated. DO NOT EDIT\nimport {"
  },
  {
    "path": "frontend/wailsjs/go/core/Bind.js",
    "chars": 374,
    "preview": "// @ts-check\n// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL\n// This file is automatically generated. DO NOT "
  },
  {
    "path": "frontend/wailsjs/go/models.ts",
    "chars": 446,
    "preview": "export namespace core {\n\t\n\texport class ResponseData {\n\t    code: number;\n\t    message: string;\n\t    data: any;\n\t\n\t    s"
  },
  {
    "path": "frontend/wailsjs/runtime/package.json",
    "chars": 538,
    "preview": "{\n  \"name\": \"@wailsapp/runtime\",\n  \"version\": \"2.0.0\",\n  \"description\": \"Wails Javascript runtime library\",\n  \"main\": \"r"
  },
  {
    "path": "frontend/wailsjs/runtime/runtime.d.ts",
    "chars": 11115,
    "preview": "/*\n _       __      _ __\n| |     / /___ _(_) /____\n| | /| / / __ `/ / / ___/\n| |/ |/ / /_/ / / (__  )\n|__/|__/\\__,_/_/_/"
  },
  {
    "path": "frontend/wailsjs/runtime/runtime.js",
    "chars": 5539,
    "preview": "/*\n _       __      _ __\n| |     / /___ _(_) /____\n| | /| / / __ `/ / / ___/\n| |/ |/ / /_/ / / (__  )\n|__/|__/\\__,_/_/_/"
  },
  {
    "path": "go.mod",
    "chars": 1528,
    "preview": "module res-downloader\n\ngo 1.22.0\n\ntoolchain go1.23.2\n\nrequire (\n\tgithub.com/elazarl/goproxy v1.7.2\n\tgithub.com/matoous/g"
  },
  {
    "path": "go.sum",
    "chars": 8144,
    "preview": "github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=\ngithub.com/bep/debounce v1.2.1/go.mod h1:"
  },
  {
    "path": "main.go",
    "chars": 2782,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"embed\"\n\t\"fmt\"\n\t\"github.com/wailsapp/wails/v2/pkg/menu\"\n\t\"github.com/wailsapp/wails/v"
  },
  {
    "path": "wails.json",
    "chars": 614,
    "preview": "{\n  \"$schema\": \"https://wails.io/schemas/config.v2.json\",\n  \"name\": \"res-downloader\",\n  \"outputfilename\": \"res-downloade"
  }
]

About this extraction

This page contains the full source code of the putyy/res-downloader GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 111 files (5.2 MB), approximately 1.4M tokens, and a symbol index with 465 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!