├── 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 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/settings/ExportDirectoryInput.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 33 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 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 | 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 | 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 | ![download.gif](md/download.gif) 27 | 28 | ### 导出 29 | 30 | 默认导出目录为`C:/Users/[你的用户名]/Downloads/漫画导出` 31 | 32 | ![download.gif](md/export.gif) 33 | 34 | ### 注意 35 | 36 | 中国大陆访问 [漫画柜](https://www.manhuagui.com) 是需要代理的,每次打开软件时会自动检测并使用系统代理 37 | 38 | 可以前往 **设置** -> **代理地址** 调整,清空则不使用代理 39 | 40 | ![image-20240519005528352](md/settings.png) 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 | 55 | -------------------------------------------------------------------------------- /frontend/src/components/export/ExportPane.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 48 | -------------------------------------------------------------------------------- /frontend/src/components/download/DownloadSearchList.vue: -------------------------------------------------------------------------------- 1 | 33 | 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 | 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 | 85 | -------------------------------------------------------------------------------- /frontend/src/components/download/DownloadButton.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 107 | 108 | -------------------------------------------------------------------------------- /frontend/src/components/export/ExportRefreshButton.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 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 | 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 | 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 | 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 | --------------------------------------------------------------------------------