├── frontend
├── package.json.md5
├── src
│ ├── assets
│ │ └── fonts
│ │ │ ├── nunito-v16-latin-regular.woff2
│ │ │ └── OFL.txt
│ ├── constants
│ │ ├── download-constant.ts
│ │ └── export-constant.ts
│ ├── vite-env.d.ts
│ ├── main.ts
│ ├── style.css
│ ├── components
│ │ ├── settings
│ │ │ ├── CacheDirectoryInput.vue
│ │ │ ├── ExportDirectoryInput.vue
│ │ │ └── SettingsPane.vue
│ │ ├── download
│ │ │ ├── DownloadTree.vue
│ │ │ ├── DownloadList.vue
│ │ │ ├── DownloadSearchList.vue
│ │ │ ├── DownloadProgress.vue
│ │ │ ├── DownloadButton.vue
│ │ │ ├── DownloadPane.vue
│ │ │ └── DownloadSearchBar.vue
│ │ └── export
│ │ │ ├── ExportTree.vue
│ │ │ ├── ExportPane.vue
│ │ │ ├── ExportRefreshButton.vue
│ │ │ └── ExportBar.vue
│ ├── App.vue
│ └── stores
│ │ └── downloader.ts
├── READ-THIS.md
├── tsconfig.node.json
├── index.html
├── wailsjs
│ ├── go
│ │ ├── api
│ │ │ ├── SettingsApi.d.ts
│ │ │ ├── SettingsApi.js
│ │ │ ├── UtilsApi.d.ts
│ │ │ ├── PathApi.d.ts
│ │ │ ├── UtilsApi.js
│ │ │ ├── ExportApi.d.ts
│ │ │ ├── ExportApi.js
│ │ │ ├── PathApi.js
│ │ │ ├── DownloadApi.d.ts
│ │ │ └── DownloadApi.js
│ │ └── models.ts
│ └── runtime
│ │ ├── package.json
│ │ ├── runtime.js
│ │ └── runtime.d.ts
├── tsconfig.json
├── package.json
├── uno.config.ts
├── vite.config.ts
├── README.md
├── components.d.ts
└── auto-imports.d.ts
├── .github
└── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature_request.yml
│ └── bug_report.yml
├── md
├── export.gif
├── download.gif
└── settings.png
├── backend
├── types
│ ├── response.go
│ └── tree_node.go
├── http_client
│ └── http_client.go
├── api
│ ├── utils_api.go
│ ├── settings_api.go
│ ├── path_api.go
│ ├── export_api.go
│ └── download_api.go
├── export_pdf
│ ├── merge.go
│ └── create.go
├── utils
│ └── utils.go
├── scan_cache
│ └── scan_cache.go
├── decoder
│ └── decoder.go
├── download
│ └── download.go
└── search
│ └── search.go
├── .gitignore
├── wails.json
├── LICENSE
├── main.go
├── README.md
├── go.mod
└── go.sum
/frontend/package.json.md5:
--------------------------------------------------------------------------------
1 | e2bd12b120f25215adabf5fdffdfe523
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
--------------------------------------------------------------------------------
/md/export.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lanyeeee/manhuagui-downloader-wails/HEAD/md/export.gif
--------------------------------------------------------------------------------
/md/download.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lanyeeee/manhuagui-downloader-wails/HEAD/md/download.gif
--------------------------------------------------------------------------------
/md/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lanyeeee/manhuagui-downloader-wails/HEAD/md/settings.png
--------------------------------------------------------------------------------
/backend/types/response.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type Response struct {
4 | Code int `json:"code"`
5 | Msg string `json:"msg"`
6 | Data any `json:"data"`
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lanyeeee/manhuagui-downloader-wails/HEAD/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
--------------------------------------------------------------------------------
/frontend/src/constants/download-constant.ts:
--------------------------------------------------------------------------------
1 | export const DownloadStatus = {
2 | COMPLETED: "✅已下载",
3 | DOWNLOADING: "⬇️下载中",
4 | FAILED: "❌下载失败",
5 | WAITING: "⏳等待下载",
6 | };
--------------------------------------------------------------------------------
/frontend/src/constants/export-constant.ts:
--------------------------------------------------------------------------------
1 | export const ExportStatus = {
2 | COMPLETED: "✅已导出",
3 | CREATING: "🔨生成中",
4 | MERGING: "🔄合并中",
5 | FAILED: "❌导出失败",
6 | WAITING: "⏳等待导出",
7 | };
--------------------------------------------------------------------------------
/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module "*.vue" {
4 | import type {DefineComponent} from "vue";
5 | const component: DefineComponent<{}, {}, any>;
6 | export default component;
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/READ-THIS.md:
--------------------------------------------------------------------------------
1 | This template uses a work around as the default template does not compile due to this issue:
2 | https://github.com/vuejs/core/issues/1228
3 |
4 | In `tsconfig.json`, `isolatedModules` is set to `false` rather than `true` to work around the issue.
--------------------------------------------------------------------------------
/frontend/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": [
9 | "vite.config.ts"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/main.ts:
--------------------------------------------------------------------------------
1 | import {createApp} from "vue";
2 | import {createPinia} from "pinia";
3 | import App from "./App.vue";
4 | import "./style.css";
5 | import "virtual:uno.css";
6 |
7 | const pinia = createPinia();
8 | const app = createApp(App);
9 |
10 | app.use(pinia);
11 | app.mount("#app");
12 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | manhuagui-downloader
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/api/SettingsApi.d.ts:
--------------------------------------------------------------------------------
1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2 | // This file is automatically generated. DO NOT EDIT
3 | import {types} from '../models';
4 | import {context} from '../models';
5 |
6 | export function ChooseDirectory(arg1:string):Promise;
7 |
8 | export function Startup(arg1:context.Context):Promise;
9 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/api/SettingsApi.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
3 | // This file is automatically generated. DO NOT EDIT
4 |
5 | export function ChooseDirectory(arg1) {
6 | return window['go']['api']['SettingsApi']['ChooseDirectory'](arg1);
7 | }
8 |
9 | export function Startup(arg1) {
10 | return window['go']['api']['SettingsApi']['Startup'](arg1);
11 | }
12 |
--------------------------------------------------------------------------------
/backend/types/tree_node.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type TreeNode struct {
4 | Label string `json:"label"`
5 | Key string `json:"key"`
6 | Children []TreeNode `json:"children"`
7 | IsLeaf bool `json:"isLeaf"`
8 | Disabled bool `json:"disabled"`
9 | DefaultChecked bool `json:"defaultChecked"`
10 | DefaultExpand bool `json:"defaultExpand"`
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/api/UtilsApi.d.ts:
--------------------------------------------------------------------------------
1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2 | // This file is automatically generated. DO NOT EDIT
3 | import {context} from '../models';
4 |
5 | export function GetCpuNum():Promise;
6 |
7 | export function GetUserDownloadPath():Promise;
8 |
9 | export function GetUserProxy():Promise;
10 |
11 | export function Startup(arg1:context.Context):Promise;
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
17 | # Go workspace file
18 | go.work
19 |
20 | build/bin
21 | node_modules
22 | frontend/dist
23 | .idea
24 |
25 | 漫画缓存
26 | 漫画导出
--------------------------------------------------------------------------------
/wails.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://wails.io/schemas/config.v2.json",
3 | "name": "manhuagui-downloader",
4 | "outputfilename": "manhuagui-downloader",
5 | "frontend:install": "pnpm install",
6 | "frontend:build": "pnpm run build",
7 | "frontend:dev:watcher": "pnpm run dev",
8 | "frontend:dev:serverUrl": "auto",
9 | "author": {
10 | "name": "lanyeeee",
11 | "email": ""
12 | },
13 | "info": {
14 | "productVersion": "0.9.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/api/PathApi.d.ts:
--------------------------------------------------------------------------------
1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2 | // This file is automatically generated. DO NOT EDIT
3 | import {context} from '../models';
4 |
5 | export function GetAbsPath(arg1:string):Promise;
6 |
7 | export function GetRelPath(arg1:string,arg2:string):Promise;
8 |
9 | export function Join(arg1:Array):Promise;
10 |
11 | export function MkDirAll(arg1:string):Promise;
12 |
13 | export function PathExists(arg1:string):Promise;
14 |
15 | export function Startup(arg1:context.Context):Promise;
16 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/api/UtilsApi.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
3 | // This file is automatically generated. DO NOT EDIT
4 |
5 | export function GetCpuNum() {
6 | return window['go']['api']['UtilsApi']['GetCpuNum']();
7 | }
8 |
9 | export function GetUserDownloadPath() {
10 | return window['go']['api']['UtilsApi']['GetUserDownloadPath']();
11 | }
12 |
13 | export function GetUserProxy() {
14 | return window['go']['api']['UtilsApi']['GetUserProxy']();
15 | }
16 |
17 | export function Startup(arg1) {
18 | return window['go']['api']['UtilsApi']['Startup'](arg1);
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/wailsjs/runtime/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@wailsapp/runtime",
3 | "version": "2.0.0",
4 | "description": "Wails Javascript runtime library",
5 | "main": "runtime.js",
6 | "types": "runtime.d.ts",
7 | "scripts": {
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/wailsapp/wails.git"
12 | },
13 | "keywords": [
14 | "Wails",
15 | "Javascript",
16 | "Go"
17 | ],
18 | "author": "Lea Anthony ",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/wailsapp/wails/issues"
22 | },
23 | "homepage": "https://github.com/wailsapp/wails#readme"
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/src/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | text-align: center;
3 | color: white;
4 | }
5 |
6 | body {
7 | margin: 0;
8 | color: white;
9 | font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
10 | "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
11 | sans-serif;
12 | }
13 |
14 | @font-face {
15 | font-family: "Nunito";
16 | font-style: normal;
17 | font-weight: 400;
18 | src: local(""),
19 | url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
20 | }
21 |
22 | #app {
23 | height: 100vh;
24 | }
25 |
26 | .n-tabs-pane-wrapper {
27 | @apply h-full
28 | }
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/api/ExportApi.d.ts:
--------------------------------------------------------------------------------
1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2 | // This file is automatically generated. DO NOT EDIT
3 | import {export_pdf} from '../models';
4 | import {types} from '../models';
5 | import {context} from '../models';
6 |
7 | export function CreatePdfs(arg1:export_pdf.CreatePdfsRequest):Promise;
8 |
9 | export function MergePdfs(arg1:string,arg2:string):Promise;
10 |
11 | export function ScanCacheDir(arg1:string,arg2:string,arg3:number):Promise;
12 |
13 | export function Startup(arg1:context.Context):Promise;
14 |
15 | export function TreeOptionModel():Promise;
16 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "isolatedModules": true,
12 | "esModuleInterop": true,
13 | "lib": [
14 | "ESNext",
15 | "DOM"
16 | ],
17 | "skipLibCheck": true
18 | },
19 | "include": [
20 | "src/**/*.ts",
21 | "src/**/*.d.ts",
22 | "src/**/*.tsx",
23 | "src/**/*.vue"
24 | ],
25 | "references": [
26 | {
27 | "path": "./tsconfig.node.json"
28 | }
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vue-tsc --noEmit && vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@vicons/ionicons5": "^0.12.0",
13 | "pinia": "^2.1.7",
14 | "vue": "^3.2.37"
15 | },
16 | "devDependencies": {
17 | "@babel/types": "^7.18.10",
18 | "@vitejs/plugin-vue": "^3.0.3",
19 | "naive-ui": "^2.38.2",
20 | "typescript": "^4.6.4",
21 | "unocss": "^0.61.0",
22 | "unplugin-auto-import": "^0.17.6",
23 | "unplugin-vue-components": "^0.27.0",
24 | "vite": "^3.0.7",
25 | "vue-tsc": "^1.8.27"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/api/ExportApi.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
3 | // This file is automatically generated. DO NOT EDIT
4 |
5 | export function CreatePdfs(arg1) {
6 | return window['go']['api']['ExportApi']['CreatePdfs'](arg1);
7 | }
8 |
9 | export function MergePdfs(arg1, arg2) {
10 | return window['go']['api']['ExportApi']['MergePdfs'](arg1, arg2);
11 | }
12 |
13 | export function ScanCacheDir(arg1, arg2, arg3) {
14 | return window['go']['api']['ExportApi']['ScanCacheDir'](arg1, arg2, arg3);
15 | }
16 |
17 | export function Startup(arg1) {
18 | return window['go']['api']['ExportApi']['Startup'](arg1);
19 | }
20 |
21 | export function TreeOptionModel() {
22 | return window['go']['api']['ExportApi']['TreeOptionModel']();
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/uno.config.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defineConfig,
3 | presetAttributify,
4 | presetIcons,
5 | presetTypography,
6 | presetUno,
7 | presetWebFonts,
8 | transformerDirectives,
9 | transformerVariantGroup
10 | } from 'unocss'
11 |
12 | export default defineConfig({
13 | shortcuts: [
14 | // ...
15 | ],
16 | theme: {
17 | colors: {
18 | // ...
19 | }
20 | },
21 | presets: [
22 | presetUno(),
23 | presetAttributify(),
24 | presetIcons(),
25 | presetTypography(),
26 | presetWebFonts({
27 | fonts: {
28 | // ...
29 | },
30 | }),
31 | ],
32 | transformers: [
33 | transformerDirectives(),
34 | transformerVariantGroup(),
35 | ],
36 | })
--------------------------------------------------------------------------------
/backend/http_client/http_client.go:
--------------------------------------------------------------------------------
1 | package http_client
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/url"
7 | "sync"
8 | )
9 |
10 | var httpClientInst *http.Client
11 | var once sync.Once
12 |
13 | func initHttpClient() {
14 | httpClientInst = &http.Client{}
15 | }
16 |
17 | // HttpClientInst 返回httpClient的单例实例
18 | func HttpClientInst() *http.Client {
19 | once.Do(initHttpClient)
20 | return httpClientInst
21 | }
22 |
23 | // UpdateProxy 设置httpClient的代理地址
24 | func UpdateProxy(proxyUrl string) error {
25 | if proxyUrl == "" {
26 | return nil
27 | }
28 | pUrl, err := url.Parse(proxyUrl)
29 | if err != nil {
30 | return fmt.Errorf("parse proxy url failed: %w", err)
31 | }
32 |
33 | httpClient := HttpClientInst()
34 | httpClient.Transport = &http.Transport{Proxy: http.ProxyURL(pUrl)}
35 | return nil
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/api/PathApi.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
3 | // This file is automatically generated. DO NOT EDIT
4 |
5 | export function GetAbsPath(arg1) {
6 | return window['go']['api']['PathApi']['GetAbsPath'](arg1);
7 | }
8 |
9 | export function GetRelPath(arg1, arg2) {
10 | return window['go']['api']['PathApi']['GetRelPath'](arg1, arg2);
11 | }
12 |
13 | export function Join(arg1) {
14 | return window['go']['api']['PathApi']['Join'](arg1);
15 | }
16 |
17 | export function MkDirAll(arg1) {
18 | return window['go']['api']['PathApi']['MkDirAll'](arg1);
19 | }
20 |
21 | export function PathExists(arg1) {
22 | return window['go']['api']['PathApi']['PathExists'](arg1);
23 | }
24 |
25 | export function Startup(arg1) {
26 | return window['go']['api']['PathApi']['Startup'](arg1);
27 | }
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: 功能请求
2 | description: 想要请求添加某个功能
3 | labels: [enhancement]
4 | title: "[功能请求] 修改我!"
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | 为了使我更好地帮助你,请提供以下信息。以及上方的标题
10 | - type: textarea
11 | id: reason
12 | attributes:
13 | label: 原因
14 | description: 为什么想要这个功能
15 | validations:
16 | required: true
17 | - type: textarea
18 | id: desc
19 | attributes:
20 | label: 功能简述
21 | description: 想要个怎样的功能
22 | validations:
23 | required: true
24 | - type: textarea
25 | id: logic
26 | attributes:
27 | label: 功能逻辑
28 | description: 如何互交、如何使用等
29 | validations:
30 | required: true
31 | - type: textarea
32 | id: ref
33 | attributes:
34 | label: 实现参考
35 | description: 该功能可能的实现方式,或者其他已经实现该功能的应用等
--------------------------------------------------------------------------------
/frontend/wailsjs/go/api/DownloadApi.d.ts:
--------------------------------------------------------------------------------
1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2 | // This file is automatically generated. DO NOT EDIT
3 | import {search} from '../models';
4 | import {types} from '../models';
5 | import {context} from '../models';
6 |
7 | export function ComicInfoModel():Promise;
8 |
9 | export function ComicSearchInfoModel():Promise;
10 |
11 | export function ComicSearchResultModel():Promise;
12 |
13 | export function DownloadChapter(arg1:string,arg2:string,arg3:number,arg4:string):Promise;
14 |
15 | export function SearchComicById(arg1:string,arg2:string,arg3:string):Promise;
16 |
17 | export function SearchComicByKeyword(arg1:string,arg2:number,arg3:string):Promise;
18 |
19 | export function Startup(arg1:context.Context):Promise;
20 |
--------------------------------------------------------------------------------
/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from "vite";
2 | import vue from "@vitejs/plugin-vue";
3 | import AutoImport from "unplugin-auto-import/vite";
4 | import Components from "unplugin-vue-components/vite";
5 | import {NaiveUiResolver} from "unplugin-vue-components/resolvers";
6 | import UnoCSS from "unocss/vite";
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig({
10 | plugins: [vue(), UnoCSS(),
11 | AutoImport({
12 | imports: [
13 | "vue",
14 | {
15 | "naive-ui": [
16 | "useDialog",
17 | "useMessage",
18 | "useNotification",
19 | "useLoadingBar"
20 | ]
21 | }
22 | ]
23 | }),
24 | Components({
25 | resolvers: [NaiveUiResolver()]
26 | })]
27 | });
28 |
--------------------------------------------------------------------------------
/backend/api/utils_api.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | "github.com/rapid7/go-get-proxied/proxy"
6 | "os"
7 | "path"
8 | "path/filepath"
9 | "runtime"
10 | )
11 |
12 | type UtilsApi struct {
13 | ctx context.Context
14 | }
15 |
16 | func NewUtilsApi() *UtilsApi {
17 | return &UtilsApi{}
18 | }
19 |
20 | func (u *UtilsApi) Startup(ctx context.Context) {
21 | u.ctx = ctx
22 | }
23 |
24 | func (u *UtilsApi) GetCpuNum() int {
25 | return runtime.NumCPU()
26 | }
27 |
28 | func (u *UtilsApi) GetUserDownloadPath() (string, error) {
29 | homeDir, err := os.UserHomeDir()
30 | if err != nil {
31 | return "", err
32 | }
33 |
34 | downloadPath := path.Join(homeDir, "Downloads")
35 | downloadPath = filepath.ToSlash(downloadPath)
36 |
37 | return downloadPath, nil
38 | }
39 |
40 | func (u *UtilsApi) GetUserProxy() string {
41 | proxies := proxy.NewProvider("").GetProxies("", "")
42 | if len(proxies) == 0 {
43 | return ""
44 | }
45 |
46 | return proxies[0].URL().String()
47 | }
48 |
--------------------------------------------------------------------------------
/backend/api/settings_api.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/wailsapp/wails/v2/pkg/runtime"
7 | "manhuagui-downloader/backend/types"
8 | "manhuagui-downloader/backend/utils"
9 | "path/filepath"
10 | )
11 |
12 | type SettingsApi struct {
13 | ctx context.Context
14 | }
15 |
16 | func NewSettingsApi() *SettingsApi {
17 | return &SettingsApi{}
18 | }
19 |
20 | func (s *SettingsApi) Startup(ctx context.Context) {
21 | s.ctx = ctx
22 | }
23 |
24 | func (s *SettingsApi) ChooseDirectory(dirPath string) types.Response {
25 | resp := types.Response{}
26 | // 如果目录不存在,则打开默认目录
27 | if !utils.PathExists(dirPath) {
28 | dirPath = ""
29 | }
30 |
31 | option := runtime.OpenDialogOptions{
32 | DefaultDirectory: dirPath,
33 | Title: "选择目录",
34 | }
35 | // 打开目录选择对话框
36 | chosenDir, err := runtime.OpenDirectoryDialog(s.ctx, option)
37 | if err != nil {
38 | resp.Code = -1
39 | resp.Msg = fmt.Sprintf("ChooseDirectory: %s", err.Error())
40 | return resp
41 | }
42 |
43 | resp.Data = filepath.ToSlash(chosenDir)
44 | return resp
45 | }
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 kurisu_u
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/api/DownloadApi.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
3 | // This file is automatically generated. DO NOT EDIT
4 |
5 | export function ComicInfoModel() {
6 | return window['go']['api']['DownloadApi']['ComicInfoModel']();
7 | }
8 |
9 | export function ComicSearchInfoModel() {
10 | return window['go']['api']['DownloadApi']['ComicSearchInfoModel']();
11 | }
12 |
13 | export function ComicSearchResultModel() {
14 | return window['go']['api']['DownloadApi']['ComicSearchResultModel']();
15 | }
16 |
17 | export function DownloadChapter(arg1, arg2, arg3, arg4) {
18 | return window['go']['api']['DownloadApi']['DownloadChapter'](arg1, arg2, arg3, arg4);
19 | }
20 |
21 | export function SearchComicById(arg1, arg2, arg3) {
22 | return window['go']['api']['DownloadApi']['SearchComicById'](arg1, arg2, arg3);
23 | }
24 |
25 | export function SearchComicByKeyword(arg1, arg2, arg3) {
26 | return window['go']['api']['DownloadApi']['SearchComicByKeyword'](arg1, arg2, arg3);
27 | }
28 |
29 | export function Startup(arg1) {
30 | return window['go']['api']['DownloadApi']['Startup'](arg1);
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/src/components/settings/CacheDirectoryInput.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
28 | 缓存目录:
29 |
30 |
31 | {{ store.cacheDirectory }}
32 |
33 |
34 |
--------------------------------------------------------------------------------
/frontend/src/components/settings/ExportDirectoryInput.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
27 | 导出目录:
28 |
29 |
30 | {{ store.exportDirectory }}
31 |
32 |
33 |
--------------------------------------------------------------------------------
/frontend/src/App.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 反馈 Bug
2 | description: 反馈遇到的问题
3 | labels: [bug]
4 | title: "[Bug] 修改我!"
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | 为了使我更好地帮助你,请提供以下信息。以及修改上方的标题
10 | - type: textarea
11 | id: desc
12 | attributes:
13 | label: 问题描述
14 | description: 发生了什么情况?复现条件(哪部漫画、哪个章节)是什么?问题能稳定触发吗?
15 | validations:
16 | required: true
17 | - type: textarea
18 | id: expected
19 | attributes:
20 | label: 预期行为
21 | description: 正常情况下应该发生什么
22 | validations:
23 | required: true
24 | - type: textarea
25 | id: actual
26 | attributes:
27 | label: 实际行为
28 | description: 实际上发生了什么
29 | validations:
30 | required: true
31 | - type: textarea
32 | id: media
33 | attributes:
34 | label: 截图或录屏
35 | description: 问题复现时候的截图或录屏
36 | placeholder: 点击文本框下面小长条可以上传文件
37 | - type: input
38 | id: version
39 | attributes:
40 | label: 工具版本号
41 | placeholder: v0.1
42 | validations:
43 | required: true
44 | - type: textarea
45 | id: other
46 | attributes:
47 | label: 其他
48 | description: 其他要补充的内容
49 | placeholder: 其他要补充的内容
50 | validations:
51 | required: false
52 |
--------------------------------------------------------------------------------
/backend/api/path_api.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | "manhuagui-downloader/backend/utils"
6 | "os"
7 | "path"
8 | "path/filepath"
9 | )
10 |
11 | type PathApi struct {
12 | ctx context.Context
13 | }
14 |
15 | func NewPathApi() *PathApi {
16 | return &PathApi{}
17 | }
18 |
19 | func (p *PathApi) Startup(ctx context.Context) {
20 | p.ctx = ctx
21 | }
22 |
23 | func (p *PathApi) GetAbsPath(path string) (string, error) {
24 | abs, err := filepath.Abs(path)
25 | abs = filepath.ToSlash(abs)
26 | return abs, err
27 | }
28 |
29 | func (p *PathApi) PathExists(path string) bool {
30 | return utils.PathExists(path)
31 | }
32 |
33 | func (p *PathApi) GetRelPath(cacheDir string, path string) (string, error) {
34 | rel, err := filepath.Rel(cacheDir, path)
35 | if err != nil {
36 | return "", err
37 | }
38 | rel = filepath.ToSlash(rel)
39 | return rel, nil
40 | }
41 |
42 | func (p *PathApi) Join(args ...interface{}) string {
43 | params := args[0].([]interface{})
44 | ss := make([]string, len(params))
45 |
46 | for _, param := range params {
47 | ss = append(ss, param.(string))
48 | }
49 | return filepath.ToSlash(path.Join(ss...))
50 | }
51 |
52 | func (p *PathApi) MkDirAll(path string) error {
53 | return os.MkdirAll(path, 0777)
54 | }
55 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "embed"
6 | "github.com/wailsapp/wails/v2"
7 | "github.com/wailsapp/wails/v2/pkg/options"
8 | "github.com/wailsapp/wails/v2/pkg/options/assetserver"
9 | "manhuagui-downloader/backend/api"
10 | )
11 |
12 | //go:embed all:frontend/dist
13 | var assets embed.FS
14 |
15 | func main() {
16 | // Create an instance of the app structure
17 | downloadApi := api.NewDownloadApi()
18 | exportApi := api.NewExportApi()
19 | pathApi := api.NewPathApi()
20 | settingsApi := api.NewSettingsApi()
21 | utilsApi := api.NewUtilsApi()
22 |
23 | // Create application with options
24 | err := wails.Run(&options.App{
25 | Title: "manhuagui-downloader",
26 | Width: 1024,
27 | Height: 768,
28 | AssetServer: &assetserver.Options{
29 | Assets: assets,
30 | },
31 | OnStartup: func(ctx context.Context) {
32 | downloadApi.Startup(ctx)
33 | exportApi.Startup(ctx)
34 | pathApi.Startup(ctx)
35 | settingsApi.Startup(ctx)
36 | utilsApi.Startup(ctx)
37 | },
38 | Bind: []interface{}{
39 | downloadApi,
40 | exportApi,
41 | pathApi,
42 | settingsApi,
43 | utilsApi,
44 | },
45 | Debug: options.Debug{
46 | OpenInspectorOnStartup: true,
47 | },
48 | })
49 |
50 | if err != nil {
51 | println("Error:", err.Error())
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/src/components/download/DownloadTree.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
34 |
35 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Vue 3 + TypeScript + Vite
2 |
3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue
4 | 3 `
25 |
26 |
27 |
28 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/backend/api/export_api.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "manhuagui-downloader/backend/export_pdf"
7 | "manhuagui-downloader/backend/scan_cache"
8 | "manhuagui-downloader/backend/types"
9 | )
10 |
11 | type ExportApi struct {
12 | ctx context.Context
13 | }
14 |
15 | func NewExportApi() *ExportApi {
16 | return &ExportApi{}
17 | }
18 |
19 | func (e *ExportApi) Startup(ctx context.Context) {
20 | e.ctx = ctx
21 | }
22 |
23 | func (e *ExportApi) ScanCacheDir(cacheDir string, exportDir string, maxDepth int64) types.Response {
24 | resp := types.Response{}
25 | treeOption, err := scan_cache.ScanCacheDir(cacheDir, exportDir, maxDepth)
26 | if err != nil {
27 | resp.Code = -1
28 | resp.Msg = fmt.Sprintf("ScanCacheDir: %s", err.Error())
29 | return resp
30 | }
31 |
32 | resp.Data = treeOption
33 | return resp
34 | }
35 |
36 | func (e *ExportApi) CreatePdfs(request export_pdf.CreatePdfsRequest) types.Response {
37 | resp := types.Response{}
38 | err := export_pdf.CreatePdfs(e.ctx, request)
39 |
40 | if err != nil {
41 | resp.Code = -1
42 | resp.Msg = fmt.Sprintf("CreatePdfs: %s", err.Error())
43 | }
44 |
45 | return resp
46 | }
47 |
48 | func (e *ExportApi) MergePdfs(pdfDir string, outputPath string) types.Response {
49 | resp := types.Response{}
50 | err := export_pdf.MergePdfs(pdfDir, outputPath)
51 |
52 | if err != nil {
53 | resp.Code = -1
54 | resp.Msg = fmt.Sprintf("MergePdfs: %s", err.Error())
55 | }
56 |
57 | return resp
58 | }
59 |
60 | func (e *ExportApi) TreeOptionModel() types.TreeNode {
61 | return types.TreeNode{}
62 | }
63 |
--------------------------------------------------------------------------------
/frontend/src/stores/downloader.ts:
--------------------------------------------------------------------------------
1 | import {defineStore} from "pinia";
2 | import * as path from "../../wailsjs/go/api/PathApi";
3 | import {GetCpuNum, GetUserDownloadPath, GetUserProxy} from "../../wailsjs/go/api/UtilsApi";
4 |
5 | export const useDownloaderStore = defineStore("downloader", {
6 | state: () => ({
7 | proxyUrl: "",
8 | downloadConcurrentCount: 3,
9 | exportConcurrentCount: 1,
10 | cacheDirectory: "",
11 | exportDirectory: "",
12 | downloadInterval: 0,
13 | exportTreeMaxDepth: 3,
14 | }),
15 | getters: {},
16 | actions: {
17 | async init() {
18 | try {
19 | this.proxyUrl = await GetUserProxy();
20 |
21 | const userDownloadPath = await GetUserDownloadPath();
22 | const exportDirectory = await path.Join([userDownloadPath, "漫画导出"]);
23 | if (!await path.PathExists(exportDirectory)) {
24 | await path.MkDirAll(exportDirectory);
25 | }
26 | const cacheDirectory = await path.Join([userDownloadPath, "漫画缓存"]);
27 | if (!await path.PathExists(cacheDirectory)) {
28 | await path.MkDirAll(cacheDirectory);
29 | }
30 | this.exportDirectory = await path.Join([userDownloadPath, "漫画导出"]);
31 | this.cacheDirectory = await path.Join([userDownloadPath, "漫画缓存"]);
32 |
33 | this.exportConcurrentCount = await GetCpuNum() / 2;
34 | } catch (e) {
35 | console.error(e);
36 | }
37 | },
38 | },
39 | });
40 |
--------------------------------------------------------------------------------
/backend/export_pdf/merge.go:
--------------------------------------------------------------------------------
1 | package export_pdf
2 |
3 | import (
4 | "fmt"
5 | "github.com/pdfcpu/pdfcpu/pkg/api"
6 | "manhuagui-downloader/backend/utils"
7 | "os"
8 | "path"
9 | "path/filepath"
10 | "sort"
11 | )
12 |
13 | func MergePdfs(pdfDir string, outputPath string) error {
14 | // 获取目录下的 PDF 文件列表
15 | pdfEntries, err := getPdfEntries(pdfDir)
16 | if err != nil {
17 | return fmt.Errorf("get pdf entries failed: %w", err)
18 | }
19 | // 如果目录下没有 PDF 文件,则返回错误
20 | if len(pdfEntries) == 0 {
21 | return fmt.Errorf("dir %s has no pdf files", pdfDir)
22 | }
23 | // 从 PDF 文件列表中提取 PDF 文件路径
24 | pdfPaths := make([]string, len(pdfEntries))
25 | for i, entry := range pdfEntries {
26 | pdfPaths[i] = path.Join(pdfDir, entry.Name())
27 | }
28 | // 合并 PDF 文件
29 | if err = api.MergeCreateFile(pdfPaths, outputPath, false, nil); err != nil {
30 | // 删除已经创建的 PDF 文件
31 | _ = os.Remove(outputPath)
32 | return fmt.Errorf("merge pdfs failed: %w", err)
33 | }
34 |
35 | return nil
36 | }
37 |
38 | func getPdfEntries(pdfDir string) ([]os.DirEntry, error) {
39 | // 读取目录下的文件列表
40 | entries, err := os.ReadDir(pdfDir)
41 | if err != nil {
42 | return nil, fmt.Errorf("read dir failed: %w", err)
43 | }
44 | // 过滤出 PDF 文件
45 | var pdfEntries []os.DirEntry
46 | for _, entry := range entries {
47 | // 忽略目录或非 PDF 文件
48 | if entry.IsDir() || filepath.Ext(entry.Name()) != ".pdf" {
49 | continue
50 | }
51 |
52 | pdfEntries = append(pdfEntries, entry)
53 | }
54 | // 按文件名排序
55 | sort.Slice(pdfEntries, func(i, j int) bool {
56 | return utils.FilenameComparer(pdfEntries[i].Name(), pdfEntries[j].Name())
57 | })
58 |
59 | return pdfEntries, nil
60 | }
61 |
--------------------------------------------------------------------------------
/backend/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 | "regexp"
6 | "strconv"
7 | "strings"
8 | "unicode"
9 | )
10 |
11 | // Sanitize 去掉非法字符,保证目录或文件名合法
12 | func Sanitize(dirOrFileName string) string {
13 | illegalChars := `<>:"/\|?*`
14 | var stringBuilder strings.Builder
15 |
16 | for _, r := range dirOrFileName {
17 | // 如果字符是非法字符,跳过
18 | if !strings.ContainsRune(illegalChars, r) && !unicode.IsControl(r) {
19 | stringBuilder.WriteRune(r)
20 | }
21 | }
22 |
23 | // 去掉开头和结尾的空格
24 | return strings.TrimSpace(stringBuilder.String())
25 | }
26 |
27 | func PathExists(path string) bool {
28 | _, err := os.Stat(path)
29 | return err == nil
30 | }
31 |
32 | func FilenameComparer(a, b string) bool {
33 | // 分割数字和非数字
34 | splitRegexp := regexp.MustCompile(`(\d+\.\d+|\d+|\D+)`)
35 |
36 | aMatches := splitRegexp.FindAllString(a, -1)
37 | bMatches := splitRegexp.FindAllString(b, -1)
38 |
39 | // 比较每个分割后的部分
40 | for i := 0; i < len(aMatches) && i < len(bMatches); i++ {
41 | aMatch, bMatch := aMatches[i], bMatches[i]
42 |
43 | aNumber, aErr := strconv.ParseFloat(aMatch, 64)
44 | bNumber, bErr := strconv.ParseFloat(bMatch, 64)
45 | // 如果两部分都是数字,则按数字大小进行比较
46 | if aErr == nil && bErr == nil {
47 | if aNumber != bNumber {
48 | return aNumber < bNumber
49 | }
50 | // 如果数字相等,继续比较
51 | continue
52 | }
53 |
54 | // 如果一个是数字而另一个不是,则数字视为较小
55 | if aErr == nil {
56 | return true
57 | }
58 | if bErr == nil {
59 | return false
60 | }
61 |
62 | // 如果都不是数字,则按字符串比较
63 | if aMatch != bMatch {
64 | return aMatch < bMatch
65 | }
66 | }
67 |
68 | // 如果所有匹配的部分都相等,那么比较它们的长度
69 | return len(aMatches) < len(bMatches)
70 | }
71 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > # ⚠️ 重要通知
2 | >
3 | > **本项目已完成重写,并迁移至新仓库:[manhuagui-downloader](https://github.com/lanyeeee/manhuagui-downloader)**
4 | >
5 | > 这个仓库将不再维护,新版本提供了更好的用户体验,建议所有用户迁移到新版本
6 |
7 |
8 | # 漫画柜下载器
9 |
10 |
11 |
12 |
13 |
14 | 一个用于 manhuagui.com 看漫画 漫画柜 的下载器,带图形界面,支持下载隐藏内容、导出PDF,免安装版(portable)解压后可以直接运行。
15 |
16 | 在[Release页面](https://github.com/lanyeeee/manhuagui-downloader-wails/releases)可以直接下载
17 |
18 | **如果本项目对你有帮助,欢迎点个 Star⭐ 支持!你的支持是我持续更新维护的动力🙏**
19 |
20 | # 图形界面
21 |
22 | ### 下载
23 |
24 | 默认下载目录为 `C:/Users/[你的用户名]/Downloads/漫画缓存`
25 |
26 | 
27 |
28 | ### 导出
29 |
30 | 默认导出目录为`C:/Users/[你的用户名]/Downloads/漫画导出`
31 |
32 | 
33 |
34 | ### 注意
35 |
36 | 中国大陆访问 [漫画柜](https://www.manhuagui.com) 是需要代理的,每次打开软件时会自动检测并使用系统代理
37 |
38 | 可以前往 **设置** -> **代理地址** 调整,清空则不使用代理
39 |
40 | 
41 |
42 | # 关于被杀毒软件误判为病毒
43 |
44 | 这个问题几乎是无解的(~~需要数字证书给软件签名,甚至给杀毒软件交保护费~~)
45 | 我能想到的解决办法只有:
46 | 1. 根据下面的**如何构建(build)**,自行编译
47 | 2. 希望你相信我的承诺,我承诺你在[Release页面](https://github.com/lanyeeee/manhuagui-downloader-wails/releases)下载到的所有东西都是安全的
48 |
49 | # 如何构建(build)
50 |
51 | 构建非常简单,一共就3条命令
52 | ~~前提是你已经安装了Go和Node~~
53 |
54 | ### 前提
55 |
56 | - [Go 1.18+](https://go.dev/dl/)
57 | - [NPM (Node 15+)](https://nodejs.org/en)
58 |
59 | ### 步骤
60 |
61 | #### 1. 安装Wails
62 |
63 | ```
64 | go install github.com/wailsapp/wails/v2/cmd/wails@latest
65 | ```
66 |
67 | #### 2. 克隆本仓库
68 |
69 | ```
70 | git clone https://github.com/lanyeeee/manhuagui-downloader-wails.git
71 | ```
72 |
73 | #### 3. 构建(build)
74 |
75 | ```
76 | cd manhuagui-downloader-wails
77 | wails build
78 | ```
79 | # 其他
80 | 任何使用中遇到的问题、任何希望添加的功能,都欢迎提issue,我会尽力解决
81 |
82 | # License 许可证
83 |
84 | [MIT](LICENSE)
85 |
--------------------------------------------------------------------------------
/frontend/src/components/download/DownloadList.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 |
34 | 下载队列
35 |
40 |
41 |
42 |
43 |
44 |
51 |
打开缓存目录
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/frontend/src/components/export/ExportPane.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
38 |
39 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/frontend/src/components/download/DownloadSearchList.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
36 |
37 |
41 | 《{{ info.title }}》
作者:{{ info.authors }}
42 |
43 |
44 |
45 |
52 |
53 | 跳至
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module manhuagui-downloader
2 |
3 | go 1.21
4 |
5 | toolchain go1.22.1
6 |
7 | require (
8 | github.com/PuerkitoBio/goquery v1.9.2
9 | github.com/daku10/go-lz-string v0.0.6
10 | github.com/pdfcpu/pdfcpu v0.8.0
11 | github.com/rapid7/go-get-proxied v0.0.0-20240311092404-798791728c56
12 | github.com/wailsapp/wails/v2 v2.9.1
13 | golang.org/x/net v0.28.0
14 | golang.org/x/sync v0.8.0
15 | )
16 |
17 | require (
18 | github.com/andybalholm/cascadia v1.3.2 // indirect
19 | github.com/bep/debounce v1.2.1 // indirect
20 | github.com/go-ole/go-ole v1.3.0 // indirect
21 | github.com/godbus/dbus/v5 v5.1.0 // indirect
22 | github.com/google/uuid v1.6.0 // indirect
23 | github.com/hhrutter/lzw v1.0.0 // indirect
24 | github.com/hhrutter/tiff v1.0.1 // indirect
25 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
26 | github.com/labstack/echo/v4 v4.12.0 // indirect
27 | github.com/labstack/gommon v0.4.2 // indirect
28 | github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
29 | github.com/leaanthony/gosod v1.0.4 // indirect
30 | github.com/leaanthony/slicer v1.6.0 // indirect
31 | github.com/leaanthony/u v1.1.1 // indirect
32 | github.com/mattn/go-colorable v0.1.13 // indirect
33 | github.com/mattn/go-isatty v0.0.20 // indirect
34 | github.com/mattn/go-runewidth v0.0.16 // indirect
35 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
36 | github.com/pkg/errors v0.9.1 // indirect
37 | github.com/rivo/uniseg v0.4.7 // indirect
38 | github.com/samber/lo v1.47.0 // indirect
39 | github.com/tkrajina/go-reflector v0.5.6 // indirect
40 | github.com/valyala/bytebufferpool v1.0.0 // indirect
41 | github.com/valyala/fasttemplate v1.2.2 // indirect
42 | github.com/wailsapp/go-webview2 v1.0.10 // indirect
43 | github.com/wailsapp/mimetype v1.4.1 // indirect
44 | golang.org/x/crypto v0.26.0 // indirect
45 | golang.org/x/image v0.19.0 // indirect
46 | golang.org/x/sys v0.24.0 // indirect
47 | golang.org/x/text v0.17.0 // indirect
48 | gopkg.in/yaml.v2 v2.4.0 // indirect
49 | )
50 |
51 | // replace github.com/wailsapp/wails/v2 v2.8.1 => C:\Users\lanye\go\pkg\mod
52 |
--------------------------------------------------------------------------------
/frontend/components.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | // @ts-nocheck
3 | // Generated by unplugin-vue-components
4 | // Read more: https://github.com/vuejs/core/pull/3399
5 | export {}
6 |
7 | /* prettier-ignore */
8 | declare module 'vue' {
9 | export interface GlobalComponents {
10 | CacheDirectoryInput: typeof import('./src/components/settings/CacheDirectoryInput.vue')['default']
11 | DownloadButton: typeof import('./src/components/download/DownloadButton.vue')['default']
12 | DownloadList: typeof import('./src/components/download/DownloadList.vue')['default']
13 | DownloadPane: typeof import('./src/components/download/DownloadPane.vue')['default']
14 | DownloadProgress: typeof import('./src/components/download/DownloadProgress.vue')['default']
15 | DownloadSearchBar: typeof import('./src/components/download/DownloadSearchBar.vue')['default']
16 | DownloadSearchList: typeof import('./src/components/download/DownloadSearchList.vue')['default']
17 | DownloadTree: typeof import('./src/components/download/DownloadTree.vue')['default']
18 | ExportBar: typeof import('./src/components/export/ExportBar.vue')['default']
19 | ExportDirectoryInput: typeof import('./src/components/settings/ExportDirectoryInput.vue')['default']
20 | ExportPane: typeof import('./src/components/export/ExportPane.vue')['default']
21 | ExportRefreshButton: typeof import('./src/components/export/ExportRefreshButton.vue')['default']
22 | ExportTree: typeof import('./src/components/export/ExportTree.vue')['default']
23 | NButton: typeof import('naive-ui')['NButton']
24 | NH3: typeof import('naive-ui')['NH3']
25 | NIcon: typeof import('naive-ui')['NIcon']
26 | NInput: typeof import('naive-ui')['NInput']
27 | NInputNumber: typeof import('naive-ui')['NInputNumber']
28 | NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
29 | NPagination: typeof import('naive-ui')['NPagination']
30 | NPopover: typeof import('naive-ui')['NPopover']
31 | NProgress: typeof import('naive-ui')['NProgress']
32 | NResult: typeof import('naive-ui')['NResult']
33 | NScrollbar: typeof import('naive-ui')['NScrollbar']
34 | NTabPane: typeof import('naive-ui')['NTabPane']
35 | NTabs: typeof import('naive-ui')['NTabs']
36 | NText: typeof import('naive-ui')['NText']
37 | SettingsPane: typeof import('./src/components/settings/SettingsPane.vue')['default']
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/backend/api/download_api.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "manhuagui-downloader/backend/download"
7 | "manhuagui-downloader/backend/http_client"
8 | "manhuagui-downloader/backend/search"
9 | "manhuagui-downloader/backend/types"
10 | )
11 |
12 | type DownloadApi struct {
13 | ctx context.Context
14 | }
15 |
16 | func NewDownloadApi() *DownloadApi {
17 | return &DownloadApi{}
18 | }
19 |
20 | func (d *DownloadApi) Startup(ctx context.Context) {
21 | d.ctx = ctx
22 | }
23 |
24 | func (d *DownloadApi) SearchComicById(comicId string, proxyUrl string, cacheDir string) types.Response {
25 | resp := types.Response{}
26 |
27 | err := http_client.UpdateProxy(proxyUrl)
28 | if err != nil {
29 | resp.Code = -1
30 | resp.Msg = fmt.Sprintf("SearchComicById: %s", err.Error())
31 | return resp
32 | }
33 |
34 | comicInfo, err := search.ComicByComicId(comicId, cacheDir)
35 | if err != nil {
36 | resp.Code = -1
37 | resp.Msg = fmt.Sprintf("SearchComicById: %s", err.Error())
38 | return resp
39 | }
40 |
41 | resp.Data = comicInfo
42 | return resp
43 | }
44 |
45 | func (d *DownloadApi) SearchComicByKeyword(keyword string, pageNum int, proxyUrl string) types.Response {
46 | resp := types.Response{}
47 |
48 | err := http_client.UpdateProxy(proxyUrl)
49 | if err != nil {
50 | resp.Code = -1
51 | resp.Msg = fmt.Sprintf("SearchComicByKeyword: %s", err.Error())
52 | return resp
53 | }
54 |
55 | result, err := search.ComicByKeyword(keyword, pageNum)
56 | if err != nil {
57 | resp.Code = -1
58 | resp.Msg = fmt.Sprintf("SearchComicByKeyword: %s", err.Error())
59 | return resp
60 | }
61 |
62 | resp.Data = result
63 | return resp
64 | }
65 |
66 | func (d *DownloadApi) DownloadChapter(chapterUrl string, saveDir string, concurrentCount int64, proxyUrl string) types.Response {
67 | resp := types.Response{}
68 | err := http_client.UpdateProxy(proxyUrl)
69 | if err != nil {
70 | resp.Code = -1
71 | resp.Msg = fmt.Sprintf("DownloadChapter: %s", err.Error())
72 | return resp
73 | }
74 |
75 | err = download.ComicChapter(d.ctx, chapterUrl, saveDir, concurrentCount)
76 | if err != nil {
77 | resp.Code = -1
78 | resp.Msg = fmt.Sprintf("DownloadChapter: %s", err.Error())
79 | return resp
80 | }
81 |
82 | return resp
83 | }
84 |
85 | func (d *DownloadApi) ComicInfoModel() search.ComicInfo {
86 | return search.ComicInfo{}
87 | }
88 |
89 | func (d *DownloadApi) ComicSearchInfoModel() search.ComicSearchInfo {
90 | return search.ComicSearchInfo{}
91 | }
92 |
93 | func (d *DownloadApi) ComicSearchResultModel() search.ComicSearchResult {
94 | return search.ComicSearchResult{}
95 | }
96 |
--------------------------------------------------------------------------------
/frontend/src/components/settings/SettingsPane.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 代理地址:
25 |
26 |
27 | 如果不使用代理,请清空这个输入框
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | 下载间隔:
36 |
37 |
38 | 秒
39 |
40 |
41 |
42 | 每章漫画下载完成后暂停的时间,不设置间隔或间隔太短则容易被ban IP
43 |
44 |
45 |
46 |
47 |
48 | 下载并发数:
49 |
50 |
51 |
52 | 下载某个章节时同时下载的图片数量。章节不支持并发下载(并发下载章节容易被ban IP)
53 |
54 |
55 |
56 |
57 |
58 | 导出并发数:
59 |
60 |
61 |
62 | 生成PDF时的并发数
63 |
64 |
65 |
66 |
67 |
68 | 导出树最大深度:
69 |
70 |
71 |
72 | (如果你完全看不懂这个参数的描述,请使用默认值 3)
此参数用于限制导出页面中文件树的最大深度,防止扫描缓存目录的时间过长
例如选择C盘根目录作为缓存目录,如果不对深度加以限制,则会扫描整个C盘下的所有文件
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/backend/scan_cache/scan_cache.go:
--------------------------------------------------------------------------------
1 | package scan_cache
2 |
3 | import (
4 | "fmt"
5 | "manhuagui-downloader/backend/types"
6 | "manhuagui-downloader/backend/utils"
7 | "os"
8 | "path"
9 | "path/filepath"
10 | "sort"
11 | "strings"
12 | )
13 |
14 | func ScanCacheDir(cacheDir string, exportDir string, maxDepth int64) ([]types.TreeNode, error) {
15 | // 将路径中的反斜杠转换为正斜杠
16 | cacheDir = filepath.ToSlash(cacheDir)
17 |
18 | root := types.TreeNode{
19 | Key: cacheDir,
20 | Children: []types.TreeNode{},
21 | }
22 | if err := buildTree(&root, cacheDir, exportDir, 0, maxDepth); err != nil {
23 | return []types.TreeNode{}, fmt.Errorf("build tree failed: %w", err)
24 | }
25 |
26 | return root.Children, nil
27 | }
28 |
29 | func buildTree(node *types.TreeNode, cacheDir string, exportDir string, depth int64, maxDeep int64) error {
30 | if depth > maxDeep {
31 | return nil
32 | }
33 |
34 | entries, err := os.ReadDir(node.Key)
35 | if err != nil {
36 | return fmt.Errorf("read dir failed: %w", err)
37 | }
38 |
39 | //给 entries 按照更合理的文件名排序
40 | sort.Slice(entries, func(i, j int) bool { return utils.FilenameComparer(entries[i].Name(), entries[j].Name()) })
41 |
42 | for _, entry := range entries {
43 | // 忽略非目录和隐藏文件
44 | if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
45 | continue
46 | }
47 |
48 | key := path.Join(node.Key, entry.Name())
49 | isLeaf := isLeafNode(key)
50 | exported := isExported(key, cacheDir, exportDir)
51 | disabled := false
52 | if isLeaf { // 只有叶子节点才能disable
53 | disabled = exported
54 | }
55 | defaultExpand := false
56 | if depth < 1 { // 默认展开第一层
57 | defaultExpand = true
58 | }
59 |
60 | childNode := types.TreeNode{
61 | Label: entry.Name(),
62 | Key: key,
63 | Children: []types.TreeNode{},
64 | DefaultExpand: defaultExpand,
65 | IsLeaf: isLeaf,
66 | DefaultChecked: exported,
67 | Disabled: disabled,
68 | }
69 | //fmt.Printf("childNode: %v\n", childNode)
70 | if err = buildTree(&childNode, cacheDir, exportDir, depth+1, maxDeep); err != nil {
71 | return fmt.Errorf("build tree failed: %w", err)
72 | }
73 |
74 | node.Children = append(node.Children, childNode)
75 | }
76 |
77 | return nil
78 | }
79 |
80 | func isLeafNode(key string) bool {
81 | // 如果无法读取目录,则认为是叶子节点
82 | entries, err := os.ReadDir(key)
83 | if err != nil {
84 | return true
85 | }
86 |
87 | // 如果有子目录,则不是叶子节点
88 | for _, entry := range entries {
89 | if entry.IsDir() {
90 | return false
91 | }
92 | }
93 |
94 | return true
95 | }
96 |
97 | func isExported(key string, cacheDir string, exportDir string) bool {
98 | relPath, err := filepath.Rel(cacheDir, key)
99 | if err != nil {
100 | return false
101 | }
102 | pdfPath := path.Join(exportDir, relPath+".pdf")
103 |
104 | return utils.PathExists(pdfPath)
105 | }
106 |
--------------------------------------------------------------------------------
/frontend/src/components/download/DownloadProgress.vue:
--------------------------------------------------------------------------------
1 |
71 |
72 |
73 |
74 | {{ treeOption?.label }}
75 | {{ chapterProgressIndicator }}
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/frontend/src/components/download/DownloadButton.vue:
--------------------------------------------------------------------------------
1 |
77 |
78 |
79 |
82 | 开始下载
83 |
84 |
85 |
86 |
87 |
88 |
89 |
92 | 取消下载
93 |
94 |
95 |
96 |
97 |
98 |
99 |
102 | 将在本章节下载完成后取消
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/frontend/src/components/export/ExportRefreshButton.vue:
--------------------------------------------------------------------------------
1 |
85 |
86 |
87 |
88 |
94 | 重新扫描
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/frontend/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* prettier-ignore */
3 | // @ts-nocheck
4 | // noinspection JSUnusedGlobalSymbols
5 | // Generated by unplugin-auto-import
6 | export {}
7 | declare global {
8 | const EffectScope: typeof import('vue')['EffectScope']
9 | const computed: typeof import('vue')['computed']
10 | const createApp: typeof import('vue')['createApp']
11 | const customRef: typeof import('vue')['customRef']
12 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
13 | const defineComponent: typeof import('vue')['defineComponent']
14 | const effectScope: typeof import('vue')['effectScope']
15 | const getCurrentInstance: typeof import('vue')['getCurrentInstance']
16 | const getCurrentScope: typeof import('vue')['getCurrentScope']
17 | const h: typeof import('vue')['h']
18 | const inject: typeof import('vue')['inject']
19 | const isProxy: typeof import('vue')['isProxy']
20 | const isReactive: typeof import('vue')['isReactive']
21 | const isReadonly: typeof import('vue')['isReadonly']
22 | const isRef: typeof import('vue')['isRef']
23 | const markRaw: typeof import('vue')['markRaw']
24 | const nextTick: typeof import('vue')['nextTick']
25 | const onActivated: typeof import('vue')['onActivated']
26 | const onBeforeMount: typeof import('vue')['onBeforeMount']
27 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
28 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
29 | const onDeactivated: typeof import('vue')['onDeactivated']
30 | const onErrorCaptured: typeof import('vue')['onErrorCaptured']
31 | const onMounted: typeof import('vue')['onMounted']
32 | const onRenderTracked: typeof import('vue')['onRenderTracked']
33 | const onRenderTriggered: typeof import('vue')['onRenderTriggered']
34 | const onScopeDispose: typeof import('vue')['onScopeDispose']
35 | const onServerPrefetch: typeof import('vue')['onServerPrefetch']
36 | const onUnmounted: typeof import('vue')['onUnmounted']
37 | const onUpdated: typeof import('vue')['onUpdated']
38 | const provide: typeof import('vue')['provide']
39 | const reactive: typeof import('vue')['reactive']
40 | const readonly: typeof import('vue')['readonly']
41 | const ref: typeof import('vue')['ref']
42 | const resolveComponent: typeof import('vue')['resolveComponent']
43 | const shallowReactive: typeof import('vue')['shallowReactive']
44 | const shallowReadonly: typeof import('vue')['shallowReadonly']
45 | const shallowRef: typeof import('vue')['shallowRef']
46 | const toRaw: typeof import('vue')['toRaw']
47 | const toRef: typeof import('vue')['toRef']
48 | const toRefs: typeof import('vue')['toRefs']
49 | const toValue: typeof import('vue')['toValue']
50 | const triggerRef: typeof import('vue')['triggerRef']
51 | const unref: typeof import('vue')['unref']
52 | const useAttrs: typeof import('vue')['useAttrs']
53 | const useCssModule: typeof import('vue')['useCssModule']
54 | const useCssVars: typeof import('vue')['useCssVars']
55 | const useDialog: typeof import('naive-ui')['useDialog']
56 | const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
57 | const useMessage: typeof import('naive-ui')['useMessage']
58 | const useNotification: typeof import('naive-ui')['useNotification']
59 | const useSlots: typeof import('vue')['useSlots']
60 | const watch: typeof import('vue')['watch']
61 | const watchEffect: typeof import('vue')['watchEffect']
62 | const watchPostEffect: typeof import('vue')['watchPostEffect']
63 | const watchSyncEffect: typeof import('vue')['watchSyncEffect']
64 | }
65 | // for type re-export
66 | declare global {
67 | // @ts-ignore
68 | export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
69 | import('vue')
70 | }
71 |
--------------------------------------------------------------------------------
/frontend/src/components/download/DownloadPane.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
49 |
50 |
58 |
59 |
66 |
71 |
72 |
73 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/backend/decoder/decoder.go:
--------------------------------------------------------------------------------
1 | package decoder
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | lzstring "github.com/daku10/go-lz-string"
7 | "regexp"
8 | "strconv"
9 | "strings"
10 | )
11 |
12 | type DecodeResult struct {
13 | Bid int `json:"bid"`
14 | Bname string `json:"bname"`
15 | Bpic string `json:"bpic"`
16 | Cid int `json:"cid"`
17 | Cname string `json:"cname"`
18 | Files []string `json:"files"`
19 | Finished bool `json:"finished"`
20 | Len int `json:"len"`
21 | Path string `json:"path"`
22 | Status int `json:"status"`
23 | BlockCc string `json:"block_cc"`
24 | NextId int `json:"nextId"`
25 | PrevId int `json:"prevId"`
26 | Sl struct {
27 | E int `json:"e"`
28 | M string `json:"m"`
29 | } `json:"sl"`
30 | }
31 |
32 | func Decode(htmlContent *string) (DecodeResult, error) {
33 | function, a, c, data, err := decodeHtmlContent(htmlContent)
34 | if err != nil {
35 | return DecodeResult{}, fmt.Errorf("decode html content failed: %w", err)
36 | }
37 |
38 | dict := generateDict(a, c, data)
39 |
40 | js := generateJs(function, dict)
41 |
42 | return generateDecodeResult(js)
43 | }
44 |
45 | func decodeHtmlContent(htmlContent *string) (string, int, int, []string, error) {
46 | re := regexp.MustCompile(`^.*}\('(.*)',(\d*),(\d*),'([\w|+/=]*)'.*$`)
47 | matches := re.FindStringSubmatch(*htmlContent)
48 | if len(matches) != 5 {
49 | return "", 0, 0, nil, fmt.Errorf("invalid html content: %s", *htmlContent)
50 | }
51 | function := matches[1]
52 |
53 | a, err := strconv.Atoi(matches[2])
54 | if err != nil {
55 | return "", 0, 0, nil, fmt.Errorf("convert a to int failed: %w", err)
56 | }
57 |
58 | c, err := strconv.Atoi(matches[3])
59 | if err != nil {
60 | return "", 0, 0, nil, fmt.Errorf("convert c to int failed: %w", err)
61 | }
62 |
63 | decompress, err := lzstring.DecompressFromBase64(matches[4])
64 | if err != nil {
65 | return "", 0, 0, nil, fmt.Errorf("decompress data failed: %w", err)
66 | }
67 | data := strings.Split(decompress, "|")
68 |
69 | return function, a, c, data, nil
70 | }
71 |
72 | func generateJs(function string, dict map[string]string) string {
73 | re := regexp.MustCompile(`(\b\w+\b)`)
74 | splits := re.Split(function, -1)
75 | matches := re.FindAllString(function, -1)
76 | var pieces []string
77 | for i := 0; i < len(splits); i++ {
78 | if i < len(matches) {
79 | pieces = append(pieces, splits[i], matches[i])
80 | } else {
81 | pieces = append(pieces, splits[i])
82 | }
83 | }
84 | js := ""
85 | for _, x := range pieces {
86 | val, ok := dict[x]
87 | if ok {
88 | js += val
89 | } else {
90 | js += x
91 | }
92 | }
93 | return js
94 | }
95 |
96 | func generateDict(a int, c int, data []string) map[string]string {
97 | var itr func(value int, num int) string
98 | itr = func(value int, num int) string {
99 | const d = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
100 | if value <= 0 {
101 | return ""
102 | }
103 | return itr(value/num, num) + string(d[value%a])
104 |
105 | }
106 |
107 | tr := func(value int, num int, a int) string {
108 | tmp := itr(value, num)
109 | if tmp == "" {
110 | return "0"
111 | }
112 | return tmp
113 | }
114 |
115 | var e func(c int) string
116 | e = func(c int) string {
117 | return func() string {
118 | if c < a {
119 | return ""
120 | }
121 | return e(c / a)
122 | }() + func() string {
123 | if c%a > 35 {
124 | return string(rune(c%a + 29))
125 | }
126 | return tr(c%a, 36, a)
127 | }()
128 | }
129 |
130 | dict := make(map[string]string)
131 | for c -= 1; c+1 > 0; c-- {
132 | if data[c] == "" {
133 | dict[e(c)] = e(c)
134 | } else {
135 | dict[e(c)] = data[c]
136 | }
137 | }
138 |
139 | return dict
140 | }
141 |
142 | func generateDecodeResult(js string) (DecodeResult, error) {
143 | re := regexp.MustCompile(`^.*\((\{.*})\).*$`)
144 | matches := re.FindStringSubmatch(js)
145 |
146 | var result DecodeResult
147 | err := json.Unmarshal([]byte(matches[1]), &result)
148 | if err != nil {
149 | return DecodeResult{}, fmt.Errorf("unmarshal decode result failed: %w", err)
150 | }
151 |
152 | return result, nil
153 | }
154 |
--------------------------------------------------------------------------------
/backend/export_pdf/create.go:
--------------------------------------------------------------------------------
1 | package export_pdf
2 |
3 | import (
4 | "fmt"
5 | "github.com/pdfcpu/pdfcpu/pkg/api"
6 | "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types"
7 | "github.com/wailsapp/wails/v2/pkg/runtime"
8 | "golang.org/x/net/context"
9 | "golang.org/x/sync/semaphore"
10 | "manhuagui-downloader/backend/utils"
11 | "os"
12 | "path"
13 | "path/filepath"
14 | "sort"
15 | "sync"
16 | )
17 |
18 | type CreatePdfsRequest struct {
19 | Tasks []CreatePdfTask `json:"tasks"`
20 | ConcurrentCount int64 `json:"concurrentCount"`
21 | }
22 |
23 | type CreatePdfTask struct {
24 | ImgDir string `json:"imgDir"`
25 | OutputPath string `json:"outputPath"`
26 | OptionKey string `json:"optionKey"`
27 | }
28 |
29 | type createPdfResult struct {
30 | completedOptionKey string
31 | err error
32 | }
33 |
34 | func CreatePdfs(ctx context.Context, request CreatePdfsRequest) error {
35 | concurrentCount := request.ConcurrentCount
36 | // 创建一个通道,用于传输创建PDF的结果
37 | pdfResultCh := make(chan createPdfResult, concurrentCount)
38 | // 启动一个生产者goroutine,创建PDF
39 | go func() {
40 | wg := sync.WaitGroup{}
41 | sem := semaphore.NewWeighted(concurrentCount)
42 | for i := range request.Tasks {
43 | wg.Add(1)
44 | go createPdf(ctx, &request.Tasks[i], pdfResultCh, sem, &wg)
45 | }
46 | wg.Wait()
47 | close(pdfResultCh)
48 | }()
49 | // 当前goroutine作为消费者,等待生产者goroutine创建PDF
50 | totalTaskCount := len(request.Tasks) // 总共需要创建的 PDF 数量
51 | completedTaskCount := 0 // 已经成功创建的 PDF 数量
52 | for result := range pdfResultCh {
53 | // 某个任务创建 PDF 失败,返回错误
54 | if result.err != nil {
55 | return fmt.Errorf("create pdf failed: %w", result.err)
56 | }
57 | // 某个任务创建 PDF 成功,更新进度
58 | completedTaskCount++
59 |
60 | msg := fmt.Sprintf("(%d/%d)", completedTaskCount, totalTaskCount)
61 | percentage := float64(completedTaskCount) / float64(totalTaskCount) * 100
62 |
63 | runtime.EventsEmit(ctx, "create_pdf", result.completedOptionKey, msg, percentage)
64 | }
65 |
66 | return nil
67 | }
68 |
69 | // TODO: 支持控制纸张大小
70 | func createPdf(ctx context.Context, task *CreatePdfTask, pdfResultCh chan<- createPdfResult, sem *semaphore.Weighted, wg *sync.WaitGroup) {
71 | defer wg.Done()
72 | // 获取图片文件列表
73 | imgEntries, err := getImgEntries(task.ImgDir)
74 | if err != nil {
75 | pdfResultCh <- createPdfResult{"", fmt.Errorf("get img entries failed: %w", err)}
76 | return
77 | }
78 | // 如果没有图片文件,则返回错误
79 | if len(imgEntries) == 0 {
80 | err = fmt.Errorf("dir '%s' has no image files", task.ImgDir)
81 | pdfResultCh <- createPdfResult{"", err}
82 | return
83 | }
84 | // 从文件列表中获取图片文件路径
85 | imgPaths := make([]string, len(imgEntries))
86 | for i, entry := range imgEntries {
87 | imgPaths[i] = path.Join(task.ImgDir, entry.Name())
88 | }
89 |
90 | if err = sem.Acquire(ctx, 1); err != nil {
91 | pdfResultCh <- createPdfResult{"", fmt.Errorf("acquire semaphore failed: %w", err)}
92 | return
93 | }
94 | defer sem.Release(1)
95 | // 获取导出的目录,并创建目录
96 | dir, _ := filepath.Split(task.OutputPath)
97 | if err = os.MkdirAll(dir, 0777); err != nil {
98 | pdfResultCh <- createPdfResult{"", fmt.Errorf("create dir failed: %w", err)}
99 | return
100 | }
101 | // 创建 PDF 导出选项
102 | imp, err := api.Import("form:A4, pos:c, scale:1.0", types.POINTS)
103 | if err != nil {
104 | pdfResultCh <- createPdfResult{"", fmt.Errorf("pdfcup import failed: %w", err)}
105 | return
106 | }
107 | // 将图片文件导入到PDF文件中
108 | if err = api.ImportImagesFile(imgPaths, task.OutputPath, imp, nil); err != nil {
109 | // 删除导出的文件
110 | _ = os.Remove(task.OutputPath)
111 | pdfResultCh <- createPdfResult{"", fmt.Errorf("pdfcup import images file failed: %w", err)}
112 | return
113 | }
114 |
115 | pdfResultCh <- createPdfResult{task.OptionKey, nil}
116 | }
117 |
118 | func getImgEntries(imgDir string) ([]os.DirEntry, error) {
119 | // 读取目录下的文件列表
120 | entries, err := os.ReadDir(imgDir)
121 | if err != nil {
122 | return []os.DirEntry{}, fmt.Errorf("read dir failed: %w", err)
123 | }
124 | // 过滤出图片文件
125 | imgEntries := make([]os.DirEntry, 0, len(entries))
126 | for _, entry := range entries {
127 | // 忽略目录
128 | if entry.IsDir() {
129 | continue
130 | }
131 | // 忽略非图片文件
132 | if path.Ext(entry.Name()) != ".jpg" && path.Ext(entry.Name()) != ".jpeg" && path.Ext(entry.Name()) != ".png" {
133 | continue
134 | }
135 |
136 | imgEntries = append(imgEntries, entry)
137 | }
138 | // 按文件名排序
139 | sort.Slice(imgEntries, func(i, j int) bool {
140 | return utils.FilenameComparer(imgEntries[i].Name(), imgEntries[j].Name())
141 | })
142 |
143 | return imgEntries, nil
144 | }
145 |
--------------------------------------------------------------------------------
/frontend/src/assets/fonts/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/frontend/wailsjs/runtime/runtime.js:
--------------------------------------------------------------------------------
1 | /*
2 | _ __ _ __
3 | | | / /___ _(_) /____
4 | | | /| / / __ `/ / / ___/
5 | | |/ |/ / /_/ / / (__ )
6 | |__/|__/\__,_/_/_/____/
7 | The electron alternative for Go
8 | (c) Lea Anthony 2019-present
9 | */
10 |
11 | export function LogPrint(message) {
12 | window.runtime.LogPrint(message);
13 | }
14 |
15 | export function LogTrace(message) {
16 | window.runtime.LogTrace(message);
17 | }
18 |
19 | export function LogDebug(message) {
20 | window.runtime.LogDebug(message);
21 | }
22 |
23 | export function LogInfo(message) {
24 | window.runtime.LogInfo(message);
25 | }
26 |
27 | export function LogWarning(message) {
28 | window.runtime.LogWarning(message);
29 | }
30 |
31 | export function LogError(message) {
32 | window.runtime.LogError(message);
33 | }
34 |
35 | export function LogFatal(message) {
36 | window.runtime.LogFatal(message);
37 | }
38 |
39 | export function EventsOnMultiple(eventName, callback, maxCallbacks) {
40 | return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
41 | }
42 |
43 | export function EventsOn(eventName, callback) {
44 | return EventsOnMultiple(eventName, callback, -1);
45 | }
46 |
47 | export function EventsOff(eventName, ...additionalEventNames) {
48 | return window.runtime.EventsOff(eventName, ...additionalEventNames);
49 | }
50 |
51 | export function EventsOnce(eventName, callback) {
52 | return EventsOnMultiple(eventName, callback, 1);
53 | }
54 |
55 | export function EventsEmit(eventName) {
56 | let args = [eventName].slice.call(arguments);
57 | return window.runtime.EventsEmit.apply(null, args);
58 | }
59 |
60 | export function WindowReload() {
61 | window.runtime.WindowReload();
62 | }
63 |
64 | export function WindowReloadApp() {
65 | window.runtime.WindowReloadApp();
66 | }
67 |
68 | export function WindowSetAlwaysOnTop(b) {
69 | window.runtime.WindowSetAlwaysOnTop(b);
70 | }
71 |
72 | export function WindowSetSystemDefaultTheme() {
73 | window.runtime.WindowSetSystemDefaultTheme();
74 | }
75 |
76 | export function WindowSetLightTheme() {
77 | window.runtime.WindowSetLightTheme();
78 | }
79 |
80 | export function WindowSetDarkTheme() {
81 | window.runtime.WindowSetDarkTheme();
82 | }
83 |
84 | export function WindowCenter() {
85 | window.runtime.WindowCenter();
86 | }
87 |
88 | export function WindowSetTitle(title) {
89 | window.runtime.WindowSetTitle(title);
90 | }
91 |
92 | export function WindowFullscreen() {
93 | window.runtime.WindowFullscreen();
94 | }
95 |
96 | export function WindowUnfullscreen() {
97 | window.runtime.WindowUnfullscreen();
98 | }
99 |
100 | export function WindowIsFullscreen() {
101 | return window.runtime.WindowIsFullscreen();
102 | }
103 |
104 | export function WindowGetSize() {
105 | return window.runtime.WindowGetSize();
106 | }
107 |
108 | export function WindowSetSize(width, height) {
109 | window.runtime.WindowSetSize(width, height);
110 | }
111 |
112 | export function WindowSetMaxSize(width, height) {
113 | window.runtime.WindowSetMaxSize(width, height);
114 | }
115 |
116 | export function WindowSetMinSize(width, height) {
117 | window.runtime.WindowSetMinSize(width, height);
118 | }
119 |
120 | export function WindowSetPosition(x, y) {
121 | window.runtime.WindowSetPosition(x, y);
122 | }
123 |
124 | export function WindowGetPosition() {
125 | return window.runtime.WindowGetPosition();
126 | }
127 |
128 | export function WindowHide() {
129 | window.runtime.WindowHide();
130 | }
131 |
132 | export function WindowShow() {
133 | window.runtime.WindowShow();
134 | }
135 |
136 | export function WindowMaximise() {
137 | window.runtime.WindowMaximise();
138 | }
139 |
140 | export function WindowToggleMaximise() {
141 | window.runtime.WindowToggleMaximise();
142 | }
143 |
144 | export function WindowUnmaximise() {
145 | window.runtime.WindowUnmaximise();
146 | }
147 |
148 | export function WindowIsMaximised() {
149 | return window.runtime.WindowIsMaximised();
150 | }
151 |
152 | export function WindowMinimise() {
153 | window.runtime.WindowMinimise();
154 | }
155 |
156 | export function WindowUnminimise() {
157 | window.runtime.WindowUnminimise();
158 | }
159 |
160 | export function WindowSetBackgroundColour(R, G, B, A) {
161 | window.runtime.WindowSetBackgroundColour(R, G, B, A);
162 | }
163 |
164 | export function ScreenGetAll() {
165 | return window.runtime.ScreenGetAll();
166 | }
167 |
168 | export function WindowIsMinimised() {
169 | return window.runtime.WindowIsMinimised();
170 | }
171 |
172 | export function WindowIsNormal() {
173 | return window.runtime.WindowIsNormal();
174 | }
175 |
176 | export function BrowserOpenURL(url) {
177 | window.runtime.BrowserOpenURL(url);
178 | }
179 |
180 | export function Environment() {
181 | return window.runtime.Environment();
182 | }
183 |
184 | export function Quit() {
185 | window.runtime.Quit();
186 | }
187 |
188 | export function Hide() {
189 | window.runtime.Hide();
190 | }
191 |
192 | export function Show() {
193 | window.runtime.Show();
194 | }
195 |
196 | export function ClipboardGetText() {
197 | return window.runtime.ClipboardGetText();
198 | }
199 |
200 | export function ClipboardSetText(text) {
201 | return window.runtime.ClipboardSetText(text);
202 | }
203 |
204 | /**
205 | * Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
206 | *
207 | * @export
208 | * @callback OnFileDropCallback
209 | * @param {number} x - x coordinate of the drop
210 | * @param {number} y - y coordinate of the drop
211 | * @param {string[]} paths - A list of file paths.
212 | */
213 |
214 | /**
215 | * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
216 | *
217 | * @export
218 | * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
219 | * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
220 | */
221 | export function OnFileDrop(callback, useDropTarget) {
222 | return window.runtime.OnFileDrop(callback, useDropTarget);
223 | }
224 |
225 | /**
226 | * OnFileDropOff removes the drag and drop listeners and handlers.
227 | */
228 | export function OnFileDropOff() {
229 | return window.runtime.OnFileDropOff();
230 | }
231 |
232 | export function CanResolveFilePaths() {
233 | return window.runtime.CanResolveFilePaths();
234 | }
235 |
236 | export function ResolveFilePaths(files) {
237 | return window.runtime.ResolveFilePaths(files);
238 | }
--------------------------------------------------------------------------------
/frontend/src/components/download/DownloadSearchBar.vue:
--------------------------------------------------------------------------------
1 |
139 |
140 |
141 |
142 |
143 |
149 |
150 | 漫画名:
151 |
152 |
153 | 搜索
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
174 |
175 | 漫画ID:
176 |
177 |
178 |
179 |
直达
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
--------------------------------------------------------------------------------
/backend/download/download.go:
--------------------------------------------------------------------------------
1 | package download
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/wailsapp/wails/v2/pkg/runtime"
7 | "golang.org/x/sync/semaphore"
8 | "io"
9 | "manhuagui-downloader/backend/decoder"
10 | "manhuagui-downloader/backend/http_client"
11 | "manhuagui-downloader/backend/utils"
12 | "net/http"
13 | "os"
14 | "path"
15 | "strconv"
16 | "strings"
17 | "sync"
18 | "time"
19 | )
20 |
21 | type downloadResult struct {
22 | imgData *[]byte
23 | err error
24 | }
25 |
26 | func ComicChapter(ctx context.Context, chapterUrl string, saveDir string, concurrentCount int64) error {
27 | // 处理saveDir,去掉特殊字符
28 | dir, file := path.Split(saveDir)
29 | file = utils.Sanitize(file)
30 | // 获取保存目录和临时目录
31 | saveDir = path.Join(dir, file)
32 | tempDir := path.Join(dir, "."+file)
33 | // 创建临时目录
34 | if err := os.MkdirAll(tempDir, 0777); err != nil {
35 | return fmt.Errorf("create temp dir failed: %w", err)
36 | }
37 |
38 | // 最多重试3次
39 | const MaxRetry = 3
40 | var decodeResult decoder.DecodeResult
41 | var err error
42 | // 带重试下载图片
43 | for i := 0; i < MaxRetry; i++ {
44 | // 解析章节页面,获取图片列表
45 | decodeResult, err = requestDecodeResult(chapterUrl)
46 | if err == nil {
47 | break
48 | }
49 | // 如果是EOF错误,说明IP被ban,等待1分钟后重试
50 | if strings.HasSuffix(err.Error(), "EOF") {
51 | for secs := 60; secs > 0; secs-- {
52 | runtime.EventsEmit(ctx, "download", "IP被ban,将在"+strconv.Itoa(secs)+"秒后重试", 0)
53 | time.Sleep(1 * time.Second)
54 | }
55 | } else {
56 | // 其他错误
57 | runtime.EventsEmit(ctx, "download", "解析章节页面失败,将在1秒后重试", 0)
58 | time.Sleep(1 * time.Second)
59 | }
60 | }
61 | if err != nil {
62 | return fmt.Errorf("request decode result failed: %w", err)
63 | }
64 | // 没有图片就直接返回
65 | if len(decodeResult.Files) == 0 {
66 | return nil
67 | }
68 | // 通过解析结果获取图片url列表
69 | imgUrls := make([]string, len(decodeResult.Files))
70 | for i, fileName := range decodeResult.Files {
71 | // 去掉.webp后缀,剩下的就是.jpg的文件名
72 | fileName = strings.TrimSuffix(fileName, ".webp")
73 | imgUrls[i] = "https://i.hamreus.com" + decodeResult.Path + fileName
74 | }
75 | // 创建一个通道,用于传输下载的图片数据
76 | downloadResultCh := make(chan downloadResult, concurrentCount)
77 | // 启动一个生产者goroutine,下载图片
78 | go func() {
79 | wg := sync.WaitGroup{}
80 | sem := semaphore.NewWeighted(concurrentCount)
81 | // 并发下载
82 | for i, imgUrl := range imgUrls {
83 | indexLength := len(strconv.Itoa(len(imgUrls)))
84 | filename := fmt.Sprintf("%0*d", indexLength, i) + ".jpg"
85 | dstPath := path.Join(tempDir, filename)
86 | // 已经存在的文件不再下载
87 | if utils.PathExists(dstPath) {
88 | fmt.Printf("%s 已存在,跳过下载\n", dstPath)
89 | continue
90 | }
91 |
92 | wg.Add(1)
93 | go downloadImage(ctx, imgUrl, dstPath, downloadResultCh, sem, &wg)
94 | }
95 | wg.Wait()
96 | close(downloadResultCh)
97 | }()
98 |
99 | // 当前goroutine是消费者,接收下载的图片数据
100 | start := time.Now()
101 | downloadedBytes := 0 // 已下载的字节数
102 | imgDownloadedCount := 0 // 已下载的图片数量
103 | totalImgCount := len(imgUrls) // 总共需要下载的图片数量
104 | for result := range downloadResultCh {
105 | // 某个图片下载失败
106 | if result.err != nil {
107 | return fmt.Errorf("download image failed: %w", result.err)
108 | }
109 | // 某个图片下载成功,更新进度
110 | imgDownloadedCount++
111 | downloadedBytes += len(*result.imgData)
112 | elapsed := time.Since(start)
113 | mbPerSecond := float64(downloadedBytes) / 1024 / 1024 / elapsed.Seconds()
114 |
115 | msg := fmt.Sprintf("%s (%d/%d):%.2f MB/s", file, imgDownloadedCount, totalImgCount, mbPerSecond)
116 | percentage := float64(imgDownloadedCount) / float64(totalImgCount) * 100
117 |
118 | runtime.EventsEmit(ctx, "download", msg, percentage)
119 | }
120 |
121 | // 如果saveDir已存在,删除
122 | if err = os.RemoveAll(saveDir); err != nil {
123 | return fmt.Errorf("remove save dir failed: %w", err)
124 | }
125 | // 将临时目录改名为saveDir
126 | if err = os.Rename(tempDir, saveDir); err != nil {
127 | return fmt.Errorf("rename temp dir to save dir failed: %w", err)
128 | }
129 |
130 | return nil
131 | }
132 |
133 | func downloadImage(ctx context.Context, imgUrl string, dstPath string, downloadResultCh chan<- downloadResult, sem *semaphore.Weighted, wg *sync.WaitGroup) {
134 | defer wg.Done()
135 | if err := sem.Acquire(ctx, 1); err != nil {
136 | downloadResultCh <- downloadResult{imgData: nil, err: fmt.Errorf("acquire semaphore failed: %w", err)}
137 | return
138 | }
139 | defer sem.Release(1)
140 | // 最多重试3次
141 | const MaxRetry = 3
142 | var imgData *[]byte
143 | var err error
144 | // 带重试下载图片
145 | for i := 0; i < MaxRetry; i++ {
146 | imgData, err = requestImageData(imgUrl)
147 | // 下载成功则退出循环
148 | if err == nil {
149 | break
150 | }
151 | // 如果是EOF错误,说明IP被ban,等待1分钟后重试
152 | if strings.HasSuffix(err.Error(), "EOF") {
153 | for secs := 60; secs > 0; secs-- {
154 | runtime.EventsEmit(ctx, "download", "IP被ban,将在"+strconv.Itoa(secs)+"秒后重试", 0)
155 | time.Sleep(1 * time.Second)
156 | }
157 | } else {
158 | // 其他错误,等待1秒后重试
159 | runtime.EventsEmit(ctx, "download", "下载图片失败,将在1秒后重试", 0)
160 | time.Sleep(1 * time.Second)
161 | }
162 | }
163 | // 下载失败
164 | if err != nil {
165 | fmt.Printf("下载图片 %s 失败,错误信息:\n%s\n", imgUrl, err)
166 | downloadResultCh <- downloadResult{imgData: nil, err: fmt.Errorf("download image failed: %w", err)}
167 | return
168 | }
169 | // 下载失败
170 | if imgData == nil {
171 | downloadResultCh <- downloadResult{imgData: nil, err: fmt.Errorf("download image failed: imgData is nil")}
172 | return
173 | }
174 | // 下载成功,保存图片
175 | if err = os.WriteFile(dstPath, *imgData, 0644); err != nil {
176 | fmt.Printf("保存图片 %s 失败,错误信息:\n%s\n", dstPath, err)
177 | downloadResultCh <- downloadResult{imgData: nil, err: fmt.Errorf("save image failed: %w", err)}
178 | return
179 | }
180 |
181 | downloadResultCh <- downloadResult{imgData: imgData, err: nil}
182 | }
183 |
184 | func requestImageData(imgUrl string) (*[]byte, error) {
185 | req, err := http.NewRequest("GET", imgUrl, nil)
186 | if err != nil {
187 | return nil, fmt.Errorf("create request failed: %w", err)
188 | }
189 | req.Header.Set("Referer", "https://www.manhuagui.com/")
190 |
191 | resp, err := http_client.HttpClientInst().Do(req)
192 | if err != nil {
193 | return nil, fmt.Errorf("do request failed: %w", err)
194 | }
195 | if resp.StatusCode != http.StatusOK {
196 | return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
197 | }
198 | defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body)
199 |
200 | respBody, err := io.ReadAll(resp.Body)
201 | if err != nil {
202 | return nil, fmt.Errorf("read response body failed: %w", err)
203 | }
204 |
205 | return &respBody, nil
206 | }
207 |
208 | func requestDecodeResult(chapterUrl string) (decoder.DecodeResult, error) {
209 | resp, err := http_client.HttpClientInst().Get(chapterUrl)
210 | if err != nil {
211 | return decoder.DecodeResult{}, fmt.Errorf("do request failed: %w", err)
212 | }
213 | if resp.StatusCode != http.StatusOK {
214 | return decoder.DecodeResult{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
215 | }
216 | defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body)
217 |
218 | respBody, err := io.ReadAll(resp.Body)
219 | if err != nil {
220 | return decoder.DecodeResult{}, fmt.Errorf("read response body failed: %w", err)
221 | }
222 |
223 | htmlContent := string(respBody)
224 |
225 | result, err := decoder.Decode(&htmlContent)
226 | if err != nil {
227 | return decoder.DecodeResult{}, fmt.Errorf("decode failed: %w", err)
228 | }
229 |
230 | return result, nil
231 | }
232 |
--------------------------------------------------------------------------------
/frontend/src/components/export/ExportBar.vue:
--------------------------------------------------------------------------------
1 |
166 |
167 |
168 |
169 |
170 | 生成进度:
171 | {{ createProgressIndicator }}
178 |
179 |
180 |
181 | 合并进度:
182 | {{ mergeProgressIndicator }}
189 |
190 |
191 |
192 |
193 |
开始导出
198 |
199 |
200 |
201 |
202 |
203 |
204 |
打开导出目录
205 |
206 |
207 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/models.ts:
--------------------------------------------------------------------------------
1 | export namespace export_pdf {
2 |
3 | export class CreatePdfTask {
4 | imgDir: string;
5 | outputPath: string;
6 | optionKey: string;
7 |
8 | static createFrom(source: any = {}) {
9 | return new CreatePdfTask(source);
10 | }
11 |
12 | constructor(source: any = {}) {
13 | if ('string' === typeof source) source = JSON.parse(source);
14 | this.imgDir = source["imgDir"];
15 | this.outputPath = source["outputPath"];
16 | this.optionKey = source["optionKey"];
17 | }
18 | }
19 | export class CreatePdfsRequest {
20 | tasks: CreatePdfTask[];
21 | concurrentCount: number;
22 |
23 | static createFrom(source: any = {}) {
24 | return new CreatePdfsRequest(source);
25 | }
26 |
27 | constructor(source: any = {}) {
28 | if ('string' === typeof source) source = JSON.parse(source);
29 | this.tasks = this.convertValues(source["tasks"], CreatePdfTask);
30 | this.concurrentCount = source["concurrentCount"];
31 | }
32 |
33 | convertValues(a: any, classs: any, asMap: boolean = false): any {
34 | if (!a) {
35 | return a;
36 | }
37 | if (a.slice && a.map) {
38 | return (a as any[]).map(elem => this.convertValues(elem, classs));
39 | } else if ("object" === typeof a) {
40 | if (asMap) {
41 | for (const key of Object.keys(a)) {
42 | a[key] = new classs(a[key]);
43 | }
44 | return a;
45 | }
46 | return new classs(a);
47 | }
48 | return a;
49 | }
50 | }
51 |
52 | }
53 |
54 | export namespace search {
55 |
56 | export class Chapter {
57 | title: string;
58 | href: string;
59 |
60 | static createFrom(source: any = {}) {
61 | return new Chapter(source);
62 | }
63 |
64 | constructor(source: any = {}) {
65 | if ('string' === typeof source) source = JSON.parse(source);
66 | this.title = source["title"];
67 | this.href = source["href"];
68 | }
69 | }
70 | export class ChapterPage {
71 | title: string;
72 | chapters: Chapter[];
73 |
74 | static createFrom(source: any = {}) {
75 | return new ChapterPage(source);
76 | }
77 |
78 | constructor(source: any = {}) {
79 | if ('string' === typeof source) source = JSON.parse(source);
80 | this.title = source["title"];
81 | this.chapters = this.convertValues(source["chapters"], Chapter);
82 | }
83 |
84 | convertValues(a: any, classs: any, asMap: boolean = false): any {
85 | if (!a) {
86 | return a;
87 | }
88 | if (a.slice && a.map) {
89 | return (a as any[]).map(elem => this.convertValues(elem, classs));
90 | } else if ("object" === typeof a) {
91 | if (asMap) {
92 | for (const key of Object.keys(a)) {
93 | a[key] = new classs(a[key]);
94 | }
95 | return a;
96 | }
97 | return new classs(a);
98 | }
99 | return a;
100 | }
101 | }
102 | export class ChapterType {
103 | title: string;
104 | chapterPages: ChapterPage[];
105 |
106 | static createFrom(source: any = {}) {
107 | return new ChapterType(source);
108 | }
109 |
110 | constructor(source: any = {}) {
111 | if ('string' === typeof source) source = JSON.parse(source);
112 | this.title = source["title"];
113 | this.chapterPages = this.convertValues(source["chapterPages"], ChapterPage);
114 | }
115 |
116 | convertValues(a: any, classs: any, asMap: boolean = false): any {
117 | if (!a) {
118 | return a;
119 | }
120 | if (a.slice && a.map) {
121 | return (a as any[]).map(elem => this.convertValues(elem, classs));
122 | } else if ("object" === typeof a) {
123 | if (asMap) {
124 | for (const key of Object.keys(a)) {
125 | a[key] = new classs(a[key]);
126 | }
127 | return a;
128 | }
129 | return new classs(a);
130 | }
131 | return a;
132 | }
133 | }
134 | export class ComicInfo {
135 | title: string;
136 | chapterTypes: ChapterType[];
137 |
138 | static createFrom(source: any = {}) {
139 | return new ComicInfo(source);
140 | }
141 |
142 | constructor(source: any = {}) {
143 | if ('string' === typeof source) source = JSON.parse(source);
144 | this.title = source["title"];
145 | this.chapterTypes = this.convertValues(source["chapterTypes"], ChapterType);
146 | }
147 |
148 | convertValues(a: any, classs: any, asMap: boolean = false): any {
149 | if (!a) {
150 | return a;
151 | }
152 | if (a.slice && a.map) {
153 | return (a as any[]).map(elem => this.convertValues(elem, classs));
154 | } else if ("object" === typeof a) {
155 | if (asMap) {
156 | for (const key of Object.keys(a)) {
157 | a[key] = new classs(a[key]);
158 | }
159 | return a;
160 | }
161 | return new classs(a);
162 | }
163 | return a;
164 | }
165 | }
166 | export class ComicSearchInfo {
167 | title: string;
168 | authors: string[];
169 | comicId: string;
170 |
171 | static createFrom(source: any = {}) {
172 | return new ComicSearchInfo(source);
173 | }
174 |
175 | constructor(source: any = {}) {
176 | if ('string' === typeof source) source = JSON.parse(source);
177 | this.title = source["title"];
178 | this.authors = source["authors"];
179 | this.comicId = source["comicId"];
180 | }
181 | }
182 | export class ComicSearchResult {
183 | infos: ComicSearchInfo[];
184 | currentPage: number;
185 | totalPage: number;
186 |
187 | static createFrom(source: any = {}) {
188 | return new ComicSearchResult(source);
189 | }
190 |
191 | constructor(source: any = {}) {
192 | if ('string' === typeof source) source = JSON.parse(source);
193 | this.infos = this.convertValues(source["infos"], ComicSearchInfo);
194 | this.currentPage = source["currentPage"];
195 | this.totalPage = source["totalPage"];
196 | }
197 |
198 | convertValues(a: any, classs: any, asMap: boolean = false): any {
199 | if (!a) {
200 | return a;
201 | }
202 | if (a.slice && a.map) {
203 | return (a as any[]).map(elem => this.convertValues(elem, classs));
204 | } else if ("object" === typeof a) {
205 | if (asMap) {
206 | for (const key of Object.keys(a)) {
207 | a[key] = new classs(a[key]);
208 | }
209 | return a;
210 | }
211 | return new classs(a);
212 | }
213 | return a;
214 | }
215 | }
216 |
217 | }
218 |
219 | export namespace types {
220 |
221 | export class Response {
222 | code: number;
223 | msg: string;
224 | data: any;
225 |
226 | static createFrom(source: any = {}) {
227 | return new Response(source);
228 | }
229 |
230 | constructor(source: any = {}) {
231 | if ('string' === typeof source) source = JSON.parse(source);
232 | this.code = source["code"];
233 | this.msg = source["msg"];
234 | this.data = source["data"];
235 | }
236 | }
237 | export class TreeNode {
238 | label: string;
239 | key: string;
240 | children: TreeNode[];
241 | isLeaf: boolean;
242 | disabled: boolean;
243 | defaultChecked: boolean;
244 | defaultExpand: boolean;
245 |
246 | static createFrom(source: any = {}) {
247 | return new TreeNode(source);
248 | }
249 |
250 | constructor(source: any = {}) {
251 | if ('string' === typeof source) source = JSON.parse(source);
252 | this.label = source["label"];
253 | this.key = source["key"];
254 | this.children = this.convertValues(source["children"], TreeNode);
255 | this.isLeaf = source["isLeaf"];
256 | this.disabled = source["disabled"];
257 | this.defaultChecked = source["defaultChecked"];
258 | this.defaultExpand = source["defaultExpand"];
259 | }
260 |
261 | convertValues(a: any, classs: any, asMap: boolean = false): any {
262 | if (!a) {
263 | return a;
264 | }
265 | if (a.slice && a.map) {
266 | return (a as any[]).map(elem => this.convertValues(elem, classs));
267 | } else if ("object" === typeof a) {
268 | if (asMap) {
269 | for (const key of Object.keys(a)) {
270 | a[key] = new classs(a[key]);
271 | }
272 | return a;
273 | }
274 | return new classs(a);
275 | }
276 | return a;
277 | }
278 | }
279 |
280 | }
281 |
282 |
--------------------------------------------------------------------------------
/backend/search/search.go:
--------------------------------------------------------------------------------
1 | package search
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/PuerkitoBio/goquery"
7 | lzstring "github.com/daku10/go-lz-string"
8 | "io"
9 | "manhuagui-downloader/backend/http_client"
10 | "manhuagui-downloader/backend/types"
11 | "manhuagui-downloader/backend/utils"
12 | "net/http"
13 | "path"
14 | "path/filepath"
15 | "slices"
16 | "strconv"
17 | "strings"
18 | )
19 |
20 | // ComicInfo 漫画信息,包含 漫画标题 和 章节类型(单话、单行本、番外)
21 | type ComicInfo struct {
22 | Title string `json:"title"`
23 | ChapterTypes []ChapterType `json:"chapterTypes"`
24 | }
25 |
26 | // ChapterType 章节类型,包含 章节类型标题 和 章节分页(第1-10页、第11-20页)
27 | type ChapterType struct {
28 | Title string `json:"title"`
29 | ChapterPages []ChapterPage `json:"chapterPages"`
30 | }
31 |
32 | // ChapterPage 分页信息,包含分页标题(第1-10页、第11-20页) 和 章节列表
33 | type ChapterPage struct {
34 | Title string `json:"title"`
35 | Chapters []Chapter `json:"chapters"`
36 | }
37 |
38 | // Chapter 章节信息,包含 章节标题 和 章节链接
39 | type Chapter struct {
40 | Title string `json:"title"`
41 | Href string `json:"href"`
42 | }
43 |
44 | // ChapterTreeNodeKey 章节树节点的Key,包含 章节链接 和 保存目录
45 | type ChapterTreeNodeKey struct {
46 | Href string `json:"href"`
47 | SaveDir string `json:"saveDir"`
48 | }
49 |
50 | // ComicSearchInfo 漫画搜索信息,包含 漫画标题、作者 和 漫画ID
51 | type ComicSearchInfo struct {
52 | Title string `json:"title"`
53 | Authors []string `json:"authors"`
54 | ComicId string `json:"comicId"`
55 | }
56 |
57 | type ComicSearchResult struct {
58 | Infos []ComicSearchInfo `json:"infos"`
59 | CurrentPage int `json:"currentPage"`
60 | TotalPage int `json:"totalPage"`
61 | }
62 |
63 | func ComicByComicId(comicId string, cacheDir string) (types.TreeNode, error) {
64 | resp, err := http_client.HttpClientInst().Get("https://www.manhuagui.com/comic/" + comicId)
65 | if err != nil {
66 | return types.TreeNode{}, fmt.Errorf("do request failed: %w", err)
67 | }
68 | // 处理HTTP错误
69 | switch resp.StatusCode {
70 | case http.StatusOK:
71 | // ignore
72 | case http.StatusNotFound:
73 | return types.TreeNode{}, fmt.Errorf("can't find comic with id: %s", comicId)
74 | default:
75 | return types.TreeNode{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
76 | }
77 | defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body)
78 | respBody, err := io.ReadAll(resp.Body)
79 | if err != nil {
80 | return types.TreeNode{}, fmt.Errorf("read response body failed: %w", err)
81 | }
82 |
83 | htmlContent := string(respBody)
84 | doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
85 | if err != nil {
86 | return types.TreeNode{}, fmt.Errorf("parse html failed: %w", err)
87 | }
88 |
89 | title, err := getTitle(doc)
90 | if err != nil {
91 | return types.TreeNode{}, fmt.Errorf("get title failed: %w", err)
92 | }
93 | warningBar := doc.Find("div[class=warning-bar]")
94 | // 如果是带警告的漫画
95 | if warningBar.Length() > 0 {
96 | // 获取id为__VIEWSTATE的input标签的value属性
97 | val, exists := doc.Find("input[id=__VIEWSTATE]").First().Attr("value")
98 | if !exists {
99 | return types.TreeNode{}, fmt.Errorf("can't find __VIEWSTATE")
100 | }
101 | // 解码得到隐藏的html内容
102 | hiddenContent, err := lzstring.DecompressFromBase64(val)
103 | if err != nil {
104 | return types.TreeNode{}, fmt.Errorf("decompress __VIEWSTATE failed: %w", err)
105 | }
106 | // 重新解析隐藏的html内容
107 | doc, err = goquery.NewDocumentFromReader(strings.NewReader(hiddenContent))
108 | if err != nil {
109 | return types.TreeNode{}, fmt.Errorf("parse hidden html failed: %w", err)
110 | }
111 | }
112 |
113 | chapterTypes, err := getChapterTypes(doc)
114 | if err != nil {
115 | return types.TreeNode{}, fmt.Errorf("get chapter types failed: %w", err)
116 | }
117 |
118 | comicInfo := ComicInfo{
119 | Title: title,
120 | ChapterTypes: chapterTypes,
121 | }
122 | // 构建树
123 | root, err := buildTree(&comicInfo, cacheDir)
124 | if err != nil {
125 | return types.TreeNode{}, fmt.Errorf("build tree failed: %w", err)
126 | }
127 |
128 | return root, nil
129 | }
130 |
131 | func ComicByKeyword(keyword string, pageNum int) (ComicSearchResult, error) {
132 | // 根据keyword和pageNum构造搜索url
133 | searchUrl := fmt.Sprintf("https://www.manhuagui.com/s/%s_p%d.html", keyword, pageNum)
134 | resp, err := http_client.HttpClientInst().Get(searchUrl)
135 | if err != nil {
136 | return ComicSearchResult{}, fmt.Errorf("do request failed: %w", err)
137 | }
138 | if resp.StatusCode != http.StatusOK {
139 | return ComicSearchResult{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
140 | }
141 | defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body)
142 | respBody, err := io.ReadAll(resp.Body)
143 | if err != nil {
144 | return ComicSearchResult{}, fmt.Errorf("read response body failed: %w", err)
145 | }
146 | // 将html内容转换为goquery.Document
147 | htmlContent := string(respBody)
148 | doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
149 | if err != nil {
150 | return ComicSearchResult{}, fmt.Errorf("parse html failed: %w", err)
151 | }
152 | // 构造搜索结果
153 | var result ComicSearchResult
154 | // 获取当前页和总页数
155 | result.CurrentPage, result.TotalPage, err = getCurrentPageAndTotalPage(doc)
156 | if err != nil {
157 | return ComicSearchResult{}, fmt.Errorf("get current page and last page failed: %w", err)
158 | }
159 | // 获取每部漫画的搜索信息
160 | doc.Find(".book-detail").Each(func(_ int, div *goquery.Selection) {
161 | var info ComicSearchInfo
162 | // 获取书名和漫画ID
163 | a := div.Find("dt a").First()
164 | title, titleExists := a.Attr("title")
165 | if titleExists {
166 | info.Title = title
167 | }
168 | href, hrefExists := a.Attr("href")
169 | if hrefExists {
170 | parts := strings.Split(href, "/")
171 | info.ComicId = parts[2]
172 | }
173 |
174 | // 获取作者名
175 | div.Find("dd.tags span a").Each(func(_ int, s *goquery.Selection) {
176 | // 跳过非作者链接
177 | href, hrefExists := s.Attr("href")
178 | if !hrefExists || !strings.HasPrefix(href, "/author/") {
179 | return
180 | }
181 |
182 | author, authorExist := s.Attr("title")
183 | if authorExist {
184 | info.Authors = append(info.Authors, author)
185 | }
186 | })
187 |
188 | result.Infos = append(result.Infos, info)
189 | })
190 | return result, nil
191 | }
192 |
193 | func getCurrentPageAndTotalPage(doc *goquery.Document) (int, int, error) {
194 | // 获取总结果数
195 | totalResultText := doc.Find("div.result-count strong").Eq(1).Text()
196 | totalResult, err := strconv.Atoi(totalResultText)
197 | if err != nil {
198 | return 0, 0, fmt.Errorf("convert total result count failed: %w", err)
199 | }
200 | // 如果没有结果
201 | if totalResult == 0 {
202 | return 0, 0, nil
203 | }
204 |
205 | currentPageString := doc.Find("span.current").Text()
206 | // 如果只有一页
207 | if currentPageString == "" {
208 | return 1, 1, nil
209 | }
210 | currentPage, err := strconv.Atoi(currentPageString)
211 | if err != nil {
212 | return 0, 0, fmt.Errorf("convert current page failed: %w", err)
213 | }
214 | // 计算总页数
215 | totalPage := totalResult / 10
216 | if totalResult%10 != 0 {
217 | totalPage++
218 | }
219 |
220 | return currentPage, totalPage, nil
221 | }
222 |
223 | func getTitle(doc *goquery.Document) (string, error) {
224 | title := doc.Find("h1").Text()
225 | return title, nil
226 | }
227 |
228 | func getChapterTypes(doc *goquery.Document) ([]ChapterType, error) {
229 | var chapterTypes []ChapterType
230 |
231 | doc.Find("h4").Each(func(i int, h4 *goquery.Selection) {
232 | chapterType := ChapterType{Title: h4.Find("span").Text()}
233 |
234 | // class中包含chapter-page的div表示这个章节类型有分页
235 | if h4.Next().Is("div[class~=chapter-page]") {
236 | chapterPageDiv := h4.Next()
237 | chapterPageDiv.Find("a").Each(func(_ int, a *goquery.Selection) {
238 | title, exist := a.Attr("title")
239 | if exist {
240 | chapterType.ChapterPages = append(chapterType.ChapterPages, ChapterPage{Title: title})
241 | }
242 | })
243 |
244 | chapterListDiv := chapterPageDiv.Next()
245 | chapterListDiv.Find("ul").Each(func(pageIndex int, ul *goquery.Selection) {
246 | // 每个ul表示一个分页
247 | chapterType.ChapterPages[pageIndex].Chapters = getChaptersFromUl(ul)
248 | })
249 |
250 | } else { // 这个章节类型没有分页
251 | chapterListDiv := h4.Next()
252 | ul := chapterListDiv.Find("ul").First()
253 | chapters := getChaptersFromUl(ul)
254 | page := ChapterPage{Chapters: chapters}
255 | chapterType.ChapterPages = []ChapterPage{page}
256 | }
257 |
258 | chapterTypes = append(chapterTypes, chapterType)
259 | })
260 |
261 | return chapterTypes, nil
262 | }
263 |
264 | func getChaptersFromUl(ul *goquery.Selection) []Chapter {
265 | var chapters []Chapter
266 |
267 | ul.Find("a").Each(func(_ int, a *goquery.Selection) {
268 | href, hrefExist := a.Attr("href")
269 | title, titleExist := a.Attr("title")
270 | if hrefExist && titleExist {
271 | chapter := Chapter{Title: title, Href: href}
272 | chapters = append(chapters, chapter)
273 | }
274 | })
275 |
276 | slices.Reverse(chapters)
277 | return chapters
278 | }
279 |
280 | func buildTree(comicInfo *ComicInfo, cacheDir string) (types.TreeNode, error) {
281 | root := types.TreeNode{
282 | Label: comicInfo.Title,
283 | Key: filepath.ToSlash(path.Join(cacheDir, comicInfo.Title)),
284 | Children: []types.TreeNode{},
285 | DefaultExpand: true,
286 | }
287 |
288 | for _, chapterType := range comicInfo.ChapterTypes {
289 | chapterTypeNode := types.TreeNode{
290 | Label: chapterType.Title,
291 | Key: filepath.ToSlash(path.Join(root.Key, chapterType.Title)),
292 | Children: []types.TreeNode{},
293 | DefaultExpand: true,
294 | }
295 |
296 | // FIXME: 连载中的漫画更新后,pageTitle会发生变化
297 | // 例如本来pageTitle为(1-88, 89-178)的漫画,更新179话后,pageTitle变为(1-89, 90-179),这会导致之前下载的章节被重复下载
298 | // 目前没有想到太好的解决方案
299 | for _, chapterPage := range chapterType.ChapterPages {
300 | chapterPageNode := types.TreeNode{
301 | Label: chapterPage.Title,
302 | Key: filepath.ToSlash(path.Join(chapterTypeNode.Key, chapterPage.Title)),
303 | Children: []types.TreeNode{},
304 | }
305 |
306 | for _, chapter := range chapterPage.Chapters {
307 | saveDir := filepath.ToSlash(path.Join(chapterPageNode.Key, chapter.Title))
308 | saveDirExists := utils.PathExists(saveDir)
309 | keyJsonBytes, err := json.Marshal(ChapterTreeNodeKey{
310 | Href: chapter.Href,
311 | SaveDir: saveDir,
312 | })
313 | if err != nil {
314 | return types.TreeNode{}, fmt.Errorf("marshal key failed: %w", err)
315 | }
316 |
317 | chapterNode := types.TreeNode{
318 | Label: chapter.Title,
319 | Key: string(keyJsonBytes),
320 | IsLeaf: true,
321 | Disabled: saveDirExists,
322 | Children: []types.TreeNode{},
323 | DefaultChecked: saveDirExists,
324 | }
325 | chapterPageNode.Children = append(chapterPageNode.Children, chapterNode)
326 | }
327 |
328 | chapterTypeNode.Children = append(chapterTypeNode.Children, chapterPageNode)
329 | }
330 |
331 | // 如果只有一个分页,就不要显示分页了,直接显示章节
332 | if len(chapterTypeNode.Children) == 1 {
333 | page := chapterTypeNode.Children[0]
334 | chapterTypeNode.Children = page.Children
335 | }
336 |
337 | root.Children = append(root.Children, chapterTypeNode)
338 | }
339 |
340 | return root, nil
341 | }
342 |
--------------------------------------------------------------------------------
/frontend/wailsjs/runtime/runtime.d.ts:
--------------------------------------------------------------------------------
1 | /*
2 | _ __ _ __
3 | | | / /___ _(_) /____
4 | | | /| / / __ `/ / / ___/
5 | | |/ |/ / /_/ / / (__ )
6 | |__/|__/\__,_/_/_/____/
7 | The electron alternative for Go
8 | (c) Lea Anthony 2019-present
9 | */
10 |
11 | export interface Position {
12 | x: number;
13 | y: number;
14 | }
15 |
16 | export interface Size {
17 | w: number;
18 | h: number;
19 | }
20 |
21 | export interface Screen {
22 | isCurrent: boolean;
23 | isPrimary: boolean;
24 | width : number
25 | height : number
26 | }
27 |
28 | // Environment information such as platform, buildtype, ...
29 | export interface EnvironmentInfo {
30 | buildType: string;
31 | platform: string;
32 | arch: string;
33 | }
34 |
35 | // [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
36 | // emits the given event. Optional data may be passed with the event.
37 | // This will trigger any event listeners.
38 | export function EventsEmit(eventName: string, ...data: any): void;
39 |
40 | // [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
41 | export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
42 |
43 | // [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
44 | // sets up a listener for the given event name, but will only trigger a given number times.
45 | export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
46 |
47 | // [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
48 | // sets up a listener for the given event name, but will only trigger once.
49 | export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
50 |
51 | // [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
52 | // unregisters the listener for the given event name.
53 | export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
54 |
55 | // [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
56 | // unregisters all listeners.
57 | export function EventsOffAll(): void;
58 |
59 | // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
60 | // logs the given message as a raw message
61 | export function LogPrint(message: string): void;
62 |
63 | // [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
64 | // logs the given message at the `trace` log level.
65 | export function LogTrace(message: string): void;
66 |
67 | // [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
68 | // logs the given message at the `debug` log level.
69 | export function LogDebug(message: string): void;
70 |
71 | // [LogError](https://wails.io/docs/reference/runtime/log#logerror)
72 | // logs the given message at the `error` log level.
73 | export function LogError(message: string): void;
74 |
75 | // [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
76 | // logs the given message at the `fatal` log level.
77 | // The application will quit after calling this method.
78 | export function LogFatal(message: string): void;
79 |
80 | // [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
81 | // logs the given message at the `info` log level.
82 | export function LogInfo(message: string): void;
83 |
84 | // [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
85 | // logs the given message at the `warning` log level.
86 | export function LogWarning(message: string): void;
87 |
88 | // [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
89 | // Forces a reload by the main application as well as connected browsers.
90 | export function WindowReload(): void;
91 |
92 | // [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
93 | // Reloads the application frontend.
94 | export function WindowReloadApp(): void;
95 |
96 | // [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
97 | // Sets the window AlwaysOnTop or not on top.
98 | export function WindowSetAlwaysOnTop(b: boolean): void;
99 |
100 | // [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
101 | // *Windows only*
102 | // Sets window theme to system default (dark/light).
103 | export function WindowSetSystemDefaultTheme(): void;
104 |
105 | // [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
106 | // *Windows only*
107 | // Sets window to light theme.
108 | export function WindowSetLightTheme(): void;
109 |
110 | // [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
111 | // *Windows only*
112 | // Sets window to dark theme.
113 | export function WindowSetDarkTheme(): void;
114 |
115 | // [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
116 | // Centers the window on the monitor the window is currently on.
117 | export function WindowCenter(): void;
118 |
119 | // [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
120 | // Sets the text in the window title bar.
121 | export function WindowSetTitle(title: string): void;
122 |
123 | // [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
124 | // Makes the window full screen.
125 | export function WindowFullscreen(): void;
126 |
127 | // [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
128 | // Restores the previous window dimensions and position prior to full screen.
129 | export function WindowUnfullscreen(): void;
130 |
131 | // [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
132 | // Returns the state of the window, i.e. whether the window is in full screen mode or not.
133 | export function WindowIsFullscreen(): Promise;
134 |
135 | // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
136 | // Sets the width and height of the window.
137 | export function WindowSetSize(width: number, height: number): Promise;
138 |
139 | // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
140 | // Gets the width and height of the window.
141 | export function WindowGetSize(): Promise;
142 |
143 | // [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
144 | // Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
145 | // Setting a size of 0,0 will disable this constraint.
146 | export function WindowSetMaxSize(width: number, height: number): void;
147 |
148 | // [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
149 | // Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
150 | // Setting a size of 0,0 will disable this constraint.
151 | export function WindowSetMinSize(width: number, height: number): void;
152 |
153 | // [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
154 | // Sets the window position relative to the monitor the window is currently on.
155 | export function WindowSetPosition(x: number, y: number): void;
156 |
157 | // [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
158 | // Gets the window position relative to the monitor the window is currently on.
159 | export function WindowGetPosition(): Promise;
160 |
161 | // [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
162 | // Hides the window.
163 | export function WindowHide(): void;
164 |
165 | // [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
166 | // Shows the window, if it is currently hidden.
167 | export function WindowShow(): void;
168 |
169 | // [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
170 | // Maximises the window to fill the screen.
171 | export function WindowMaximise(): void;
172 |
173 | // [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
174 | // Toggles between Maximised and UnMaximised.
175 | export function WindowToggleMaximise(): void;
176 |
177 | // [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
178 | // Restores the window to the dimensions and position prior to maximising.
179 | export function WindowUnmaximise(): void;
180 |
181 | // [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
182 | // Returns the state of the window, i.e. whether the window is maximised or not.
183 | export function WindowIsMaximised(): Promise;
184 |
185 | // [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
186 | // Minimises the window.
187 | export function WindowMinimise(): void;
188 |
189 | // [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
190 | // Restores the window to the dimensions and position prior to minimising.
191 | export function WindowUnminimise(): void;
192 |
193 | // [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
194 | // Returns the state of the window, i.e. whether the window is minimised or not.
195 | export function WindowIsMinimised(): Promise;
196 |
197 | // [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
198 | // Returns the state of the window, i.e. whether the window is normal or not.
199 | export function WindowIsNormal(): Promise;
200 |
201 | // [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
202 | // Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
203 | export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
204 |
205 | // [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
206 | // Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
207 | export function ScreenGetAll(): Promise;
208 |
209 | // [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
210 | // Opens the given URL in the system browser.
211 | export function BrowserOpenURL(url: string): void;
212 |
213 | // [Environment](https://wails.io/docs/reference/runtime/intro#environment)
214 | // Returns information about the environment
215 | export function Environment(): Promise;
216 |
217 | // [Quit](https://wails.io/docs/reference/runtime/intro#quit)
218 | // Quits the application.
219 | export function Quit(): void;
220 |
221 | // [Hide](https://wails.io/docs/reference/runtime/intro#hide)
222 | // Hides the application.
223 | export function Hide(): void;
224 |
225 | // [Show](https://wails.io/docs/reference/runtime/intro#show)
226 | // Shows the application.
227 | export function Show(): void;
228 |
229 | // [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
230 | // Returns the current text stored on clipboard
231 | export function ClipboardGetText(): Promise;
232 |
233 | // [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
234 | // Sets a text on the clipboard
235 | export function ClipboardSetText(text: string): Promise;
236 |
237 | // [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
238 | // OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
239 | export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
240 |
241 | // [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
242 | // OnFileDropOff removes the drag and drop listeners and handlers.
243 | export function OnFileDropOff() :void
244 |
245 | // Check if the file path resolver is available
246 | export function CanResolveFilePaths(): boolean;
247 |
248 | // Resolves file paths for an array of files
249 | export function ResolveFilePaths(files: File[]): void
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
2 | github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
3 | github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
4 | github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
5 | github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
6 | github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
7 | github.com/daku10/go-lz-string v0.0.6 h1:aO8FFp4QPuNp7+WNyh1DyNjGF3UbZu95tUv9xOZNsYQ=
8 | github.com/daku10/go-lz-string v0.0.6/go.mod h1:Vk++rSG3db8HXJaHEAbxiy/ukjTmPBw/iI+SrVZDzfs=
9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
11 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
12 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
13 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
14 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
15 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
16 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
17 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
18 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
19 | github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
20 | github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
21 | github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0=
22 | github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc=
23 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
24 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
25 | github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
26 | github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
27 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
28 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
29 | github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
30 | github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
31 | github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
32 | github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
33 | github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
34 | github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
35 | github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
36 | github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
37 | github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
38 | github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
39 | github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
40 | github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
41 | github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
42 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
43 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
44 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
45 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
46 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
47 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
48 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
49 | github.com/pdfcpu/pdfcpu v0.8.0 h1:SuEB4uVsPFz1nb802r38YpFpj9TtZh/oB0bGG34IRZw=
50 | github.com/pdfcpu/pdfcpu v0.8.0/go.mod h1:jj03y/KKrwigt5xCi8t7px2mATcKuOzkIOoCX62yMho=
51 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
52 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
53 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
54 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
55 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
56 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
57 | github.com/rapid7/go-get-proxied v0.0.0-20240311092404-798791728c56 h1:NMFnJUxI7m/To0on5bGzxyqZbFQBIK6yfacNj+JP1dg=
58 | github.com/rapid7/go-get-proxied v0.0.0-20240311092404-798791728c56/go.mod h1:ELOKvSUbHx1oVeecsknc02S0eEAFD+TdV3rTt3BcNzM=
59 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
60 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
61 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
62 | github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
63 | github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
64 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
65 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
66 | github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
67 | github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
68 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
69 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
70 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
71 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
72 | github.com/wailsapp/go-webview2 v1.0.10 h1:PP5Hug6pnQEAhfRzLCoOh2jJaPdrqeRgJKZhyYyDV/w=
73 | github.com/wailsapp/go-webview2 v1.0.10/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
74 | github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
75 | github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
76 | github.com/wailsapp/wails/v2 v2.9.1 h1:irsXnoQrCpeKzKTYZ2SUVlRRyeMR6I0vCO9Q1cvlEdc=
77 | github.com/wailsapp/wails/v2 v2.9.1/go.mod h1:7maJV2h+Egl11Ak8QZN/jlGLj2wg05bsQS+ywJPT0gI=
78 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
79 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
80 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
81 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
82 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
83 | golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
84 | golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
85 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
86 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
87 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
88 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
89 | golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
90 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
91 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
92 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
93 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
94 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
95 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
96 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
97 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
98 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
99 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
100 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
101 | golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
102 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
103 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
104 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
105 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
106 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
107 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
108 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
109 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
110 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
111 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
112 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
113 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
114 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
115 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
116 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
117 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
118 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
119 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
120 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
121 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
122 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
123 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
124 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
125 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
126 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
127 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
128 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
129 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
130 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
131 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
132 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
133 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
134 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
135 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
136 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
137 |
--------------------------------------------------------------------------------