mirror of
https://github.com/putyy/res-downloader.git
synced 2026-01-12 22:34:56 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
046cbb2b83 | ||
|
|
b562f76c69 | ||
|
|
8aaf95fd36 | ||
|
|
983d72d65a | ||
|
|
86378b9fba | ||
|
|
6b18e7fba1 | ||
|
|
ec11132240 | ||
|
|
dc877bd634 | ||
|
|
00b4bf4068 | ||
|
|
51c43564b6 |
@@ -14,7 +14,7 @@
|
||||
!define INFO_PRODUCTNAME "res-downloader"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "3.1.1"
|
||||
!define INFO_PRODUCTVERSION "3.1.3"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "Copyright © 2023"
|
||||
|
||||
@@ -37,6 +37,7 @@ var (
|
||||
systemOnce *SystemSetup
|
||||
proxyOnce *Proxy
|
||||
httpServerOnce *HttpServer
|
||||
ruleOnce *RuleSet
|
||||
)
|
||||
|
||||
func GetApp(assets embed.FS, wjs string) *App {
|
||||
@@ -120,6 +121,7 @@ ILKEQKmPPzKs7kp/7Nz+2cT3
|
||||
initResource()
|
||||
initHttpServer()
|
||||
initSystem()
|
||||
initRule()
|
||||
}
|
||||
return appOnce
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ type Config struct {
|
||||
UseHeaders string `json:"UseHeaders"`
|
||||
InsertTail bool `json:"InsertTail"`
|
||||
MimeMap map[string]MimeInfo `json:"MimeMap"`
|
||||
Rule string `json:"Rule"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -65,9 +66,10 @@ func initConfig() *Config {
|
||||
TaskNumber: runtime.NumCPU() * 2,
|
||||
DownNumber: 3,
|
||||
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
|
||||
UseHeaders: "User-Agent,Referer,Authorization,Cookie",
|
||||
UseHeaders: "default",
|
||||
InsertTail: true,
|
||||
MimeMap: getDefaultMimeMap(),
|
||||
Rule: "*",
|
||||
}
|
||||
|
||||
rawDefaults, err := json.Marshal(defaultConfig)
|
||||
@@ -161,6 +163,7 @@ func getDefaultMimeMap() map[string]MimeInfo {
|
||||
"application/vnd.apple.mpegurl": {Type: "m3u8", Suffix: ".m3u8"},
|
||||
"application/x-mpegurl": {Type: "m3u8", Suffix: ".m3u8"},
|
||||
"application/x-mpeg": {Type: "m3u8", Suffix: ".m3u8"},
|
||||
"audio/x-mpegurl": {Type: "m3u8", Suffix: ".m3u8"},
|
||||
"application/pdf": {Type: "pdf", Suffix: ".pdf"},
|
||||
"application/vnd.ms-powerpoint": {Type: "ppt", Suffix: ".ppt"},
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": {Type: "ppt", Suffix: ".pptx"},
|
||||
@@ -172,7 +175,8 @@ func getDefaultMimeMap() map[string]MimeInfo {
|
||||
"text/rtf": {Type: "doc", Suffix: ".rtf"},
|
||||
"application/vnd.oasis.opendocument.text": {Type: "doc", Suffix: ".odt"},
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {Type: "doc", Suffix: ".docx"},
|
||||
"font/woff": {Type: "font", Suffix: ".woff"},
|
||||
"font/woff": {Type: "font", Suffix: ".woff"},
|
||||
"application/octet-stream": {Type: "stream", Suffix: "default"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +209,7 @@ func getDefaultDownloadDir() string {
|
||||
func (c *Config) setConfig(config Config) {
|
||||
oldProxy := c.UpstreamProxy
|
||||
openProxy := c.OpenProxy
|
||||
oldRule := c.Rule
|
||||
c.Host = config.Host
|
||||
c.Port = config.Port
|
||||
c.Theme = config.Theme
|
||||
@@ -223,10 +228,18 @@ func (c *Config) setConfig(config Config) {
|
||||
c.WxAction = config.WxAction
|
||||
c.UseHeaders = config.UseHeaders
|
||||
c.InsertTail = config.InsertTail
|
||||
c.Rule = config.Rule
|
||||
if oldProxy != c.UpstreamProxy || openProxy != c.OpenProxy {
|
||||
proxyOnce.setTransport()
|
||||
}
|
||||
|
||||
if oldRule != c.Rule {
|
||||
err := ruleOnce.Load(c.Rule)
|
||||
if err != nil {
|
||||
globalLogger.Esg(err, "set rule failed")
|
||||
}
|
||||
}
|
||||
|
||||
mimeMux.Lock()
|
||||
c.MimeMap = config.MimeMap
|
||||
mimeMux.Unlock()
|
||||
@@ -279,6 +292,8 @@ func (c *Config) getConfig(key string) interface{} {
|
||||
mimeMux.RLock()
|
||||
defer mimeMux.RUnlock()
|
||||
return c.MimeMap
|
||||
case "Rule":
|
||||
return c.Rule
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -82,8 +82,41 @@ func (fd *FileDownloader) buildClient() *http.Client {
|
||||
}
|
||||
}
|
||||
|
||||
var forbiddenDownloadHeaders = map[string]struct{}{
|
||||
"accept-encoding": {},
|
||||
"content-length": {},
|
||||
"host": {},
|
||||
"connection": {},
|
||||
"keep-alive": {},
|
||||
"proxy-connection": {},
|
||||
"transfer-encoding": {},
|
||||
|
||||
"sec-fetch-site": {},
|
||||
"sec-fetch-mode": {},
|
||||
"sec-fetch-dest": {},
|
||||
"sec-fetch-user": {},
|
||||
"sec-ch-ua": {},
|
||||
"sec-ch-ua-mobile": {},
|
||||
"sec-ch-ua-platform": {},
|
||||
|
||||
"if-none-match": {},
|
||||
"if-modified-since": {},
|
||||
|
||||
"x-forwarded-for": {},
|
||||
"x-real-ip": {},
|
||||
}
|
||||
|
||||
func (fd *FileDownloader) setHeaders(request *http.Request) {
|
||||
for key, value := range fd.Headers {
|
||||
if globalConfig.UseHeaders == "default" {
|
||||
lk := strings.ToLower(key)
|
||||
if _, forbidden := forbiddenDownloadHeaders[lk]; forbidden {
|
||||
continue
|
||||
}
|
||||
request.Header.Set(key, value)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(globalConfig.UseHeaders, key) {
|
||||
request.Header.Set(key, value)
|
||||
}
|
||||
|
||||
@@ -324,11 +324,13 @@ func (h *HttpServer) clear(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) {
|
||||
var data struct {
|
||||
Sign string `json:"sign"`
|
||||
Sign []string `json:"sign"`
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(&data)
|
||||
if err == nil && data.Sign != "" {
|
||||
resourceOnce.delete(data.Sign)
|
||||
if err == nil && len(data.Sign) > 0 {
|
||||
for _, v := range data.Sign {
|
||||
resourceOnce.delete(v)
|
||||
}
|
||||
}
|
||||
h.success(w)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"github.com/elazarl/goproxy"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"res-downloader/core/shared"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DefaultPlugin struct {
|
||||
@@ -26,7 +28,7 @@ func (p *DefaultPlugin) OnRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http
|
||||
}
|
||||
|
||||
func (p *DefaultPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
|
||||
if resp == nil || resp.Request == nil || (resp.StatusCode != 200 && resp.StatusCode != 206) {
|
||||
if resp == nil || resp.Request == nil || (resp.StatusCode != 200 && resp.StatusCode != 206 && resp.StatusCode != 304) {
|
||||
return resp
|
||||
}
|
||||
|
||||
@@ -39,6 +41,13 @@ func (p *DefaultPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *
|
||||
isAll, _ := p.bridge.GetResType("all")
|
||||
isClassify, _ := p.bridge.GetResType(classify)
|
||||
|
||||
if suffix == "default" {
|
||||
ext := filepath.Ext(filepath.Base(strings.Split(strings.Split(rawUrl, "?")[0], "#")[0]))
|
||||
if ext != "" {
|
||||
suffix = ext
|
||||
}
|
||||
}
|
||||
|
||||
urlSign := shared.Md5(rawUrl)
|
||||
if ok := p.bridge.MediaIsMarked(urlSign); !ok && (isAll || isClassify) {
|
||||
value, _ := strconv.ParseFloat(resp.Header.Get("content-length"), 64)
|
||||
|
||||
@@ -80,7 +80,14 @@ func (p *Proxy) Startup() {
|
||||
//p.Proxy.KeepDestinationHeaders = true
|
||||
//p.Proxy.Verbose = false
|
||||
p.setTransport()
|
||||
p.Proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
|
||||
//p.Proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
|
||||
p.Proxy.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
||||
if ruleOnce.shouldMitm(host) {
|
||||
return goproxy.MitmConnect, host
|
||||
}
|
||||
return goproxy.OkConnect, host
|
||||
})
|
||||
|
||||
p.Proxy.OnRequest().DoFunc(p.httpRequestEvent)
|
||||
p.Proxy.OnResponse().DoFunc(p.httpResponseEvent)
|
||||
}
|
||||
@@ -113,10 +120,14 @@ func (p *Proxy) setTransport() {
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
p.Proxy.ConnectDial = nil
|
||||
p.Proxy.ConnectDialWithReq = nil
|
||||
|
||||
if globalConfig.UpstreamProxy != "" && globalConfig.OpenProxy && !strings.Contains(globalConfig.UpstreamProxy, globalConfig.Port) {
|
||||
proxyURL, err := url.Parse(globalConfig.UpstreamProxy)
|
||||
if err == nil {
|
||||
transport.Proxy = http.ProxyURL(proxyURL)
|
||||
p.Proxy.ConnectDial = p.Proxy.NewConnectDialToProxy(globalConfig.UpstreamProxy)
|
||||
}
|
||||
}
|
||||
p.Proxy.Tr = transport
|
||||
|
||||
126
core/rule.go
Normal file
126
core/rule.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Rule struct {
|
||||
raw string
|
||||
isNeg bool // 是否否定规则(以 ! 开头)
|
||||
isWildcard bool // 是否为 *.domain 形式
|
||||
isAll bool
|
||||
domain string // 域名部分,不含 "*."
|
||||
}
|
||||
|
||||
type RuleSet struct {
|
||||
mu sync.RWMutex
|
||||
rules []Rule
|
||||
}
|
||||
|
||||
func initRule() *RuleSet {
|
||||
if ruleOnce == nil {
|
||||
ruleOnce = &RuleSet{}
|
||||
err := ruleOnce.Load(globalConfig.Rule)
|
||||
if err != nil {
|
||||
globalLogger.Esg(err, "init rule failed")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ruleOnce
|
||||
}
|
||||
|
||||
func (r *RuleSet) Load(rs string) error {
|
||||
reader := strings.NewReader(rs)
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
var rules []Rule
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
isNeg := false
|
||||
if strings.HasPrefix(line, "!") {
|
||||
isNeg = true
|
||||
line = strings.TrimSpace(line[1:])
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if line == "*" {
|
||||
rules = append(rules, Rule{
|
||||
raw: "*",
|
||||
isAll: true,
|
||||
isNeg: isNeg,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
isWildcard := false
|
||||
domain := line
|
||||
if strings.HasPrefix(line, "*.") {
|
||||
isWildcard = true
|
||||
domain = line[2:]
|
||||
}
|
||||
|
||||
rules = append(rules, Rule{
|
||||
raw: line,
|
||||
isNeg: isNeg,
|
||||
isWildcard: isWildcard,
|
||||
domain: strings.ToLower(domain),
|
||||
})
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.rules = rules
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldMitm: 根据当前规则集判断是否对 host 做 MITM
|
||||
// host 可能带端口(example.com:443),函数会只匹配 hostname 部分
|
||||
// 返回 true => MITM(解密),false => 透传
|
||||
func (r *RuleSet) shouldMitm(host string) bool {
|
||||
h := host
|
||||
if strings.HasPrefix(h, "[") {
|
||||
if hostSplitIdx := strings.LastIndex(h, "]"); hostSplitIdx != -1 {
|
||||
h = h[:hostSplitIdx+1]
|
||||
}
|
||||
}
|
||||
if hp, _, err := net.SplitHostPort(host); err == nil {
|
||||
h = hp
|
||||
}
|
||||
h = strings.ToLower(strings.Trim(h, "[]"))
|
||||
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
action := false
|
||||
for _, rule := range r.rules {
|
||||
if rule.isAll {
|
||||
action = !rule.isNeg
|
||||
continue
|
||||
}
|
||||
|
||||
if rule.isWildcard {
|
||||
if h == rule.domain || strings.HasSuffix(h, "."+rule.domain) {
|
||||
action = !rule.isNeg
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if h == rule.domain {
|
||||
action = !rule.isNeg
|
||||
}
|
||||
}
|
||||
return action
|
||||
}
|
||||
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@@ -49,6 +49,7 @@ declare module 'vue' {
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Screen: typeof import('./src/components/Screen.vue')['default']
|
||||
ShowLoading: typeof import('./src/components/ShowLoading.vue')['default']
|
||||
ShowOrEdit: typeof import('./src/components/ShowOrEdit.vue')['default']
|
||||
Sider: typeof import('./src/components/layout/Sider.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,12 @@
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.ellipsis-2 {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
</NIcon>
|
||||
</template>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center justify-start p-1.5 cursor-pointer" v-if="row.Status === 'running'" @click="action('cancel')">
|
||||
<div class="flex items-center justify-start p-1.5 cursor-pointer" v-if="row.Status === 'running' || row.Status === 'pending'" @click="action('cancel')">
|
||||
<n-icon
|
||||
size="28"
|
||||
class="text-red-500 dark:text-red-300 bg-red-500/20 dark:bg-red-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-red-500/40 transition-colors"
|
||||
|
||||
59
frontend/src/components/ShowOrEdit.vue
Normal file
59
frontend/src/components/ShowOrEdit.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div
|
||||
class="min-h-6"
|
||||
@click="handleOnClick"
|
||||
>
|
||||
<n-input
|
||||
v-if="isEdit"
|
||||
ref="inputRef"
|
||||
:value="inputValue"
|
||||
@update:value="v => inputValue = v"
|
||||
@change="handleChange"
|
||||
@blur="handleChange"
|
||||
/>
|
||||
|
||||
<n-tooltip
|
||||
v-else
|
||||
trigger="hover"
|
||||
placement="top"
|
||||
>
|
||||
<template #trigger>
|
||||
<div class="ellipsis-2">{{ inputValue }}</div>
|
||||
</template>
|
||||
<div class="ellipsis-2">{{ inputValue }}</div>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, watch } from 'vue'
|
||||
import type { InputInst } from 'naive-ui'
|
||||
|
||||
interface OnUpdateValue {
|
||||
(value: string): void
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
value: string | number
|
||||
onUpdateValue?: OnUpdateValue
|
||||
}>()
|
||||
|
||||
const isEdit = ref(false)
|
||||
const inputRef = ref<InputInst | null>(null)
|
||||
const inputValue = ref(String(props.value))
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
v => inputValue.value = String(v)
|
||||
)
|
||||
|
||||
function handleOnClick() {
|
||||
isEdit.value = true
|
||||
nextTick(() => inputRef.value?.focus())
|
||||
}
|
||||
|
||||
function handleChange() {
|
||||
props.onUpdateValue?.(String(inputValue.value))
|
||||
isEdit.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -47,6 +47,7 @@
|
||||
"video": "Video",
|
||||
"m3u8": "M3U8",
|
||||
"live": "Live Stream",
|
||||
"stream": "Data Stream",
|
||||
"xls": "Spreadsheet",
|
||||
"doc": "Document",
|
||||
"pdf": "PDF",
|
||||
@@ -55,7 +56,7 @@
|
||||
"choice": "choice",
|
||||
"type": "Type",
|
||||
"preview": "Preview",
|
||||
"preview_tip": "Preview not supported",
|
||||
"preview_tip": "Cannot preview",
|
||||
"status": "Status",
|
||||
"description": "Description",
|
||||
"resource_size": "Resource Size",
|
||||
@@ -76,6 +77,7 @@
|
||||
"open_link": "Open Link",
|
||||
"open_file": "Open File",
|
||||
"delete_row": "Delete Row",
|
||||
"delete_tip": "Running tasks cannot be deleted",
|
||||
"cancel_down": "Cancel Download",
|
||||
"more_operation": "More Operations",
|
||||
"video_decode": "WxDecrypt",
|
||||
@@ -118,9 +120,11 @@
|
||||
"connections_tip": "Keep default if unsure, usually CPU cores * 2, for faster downloads",
|
||||
"down_number": "Download Number",
|
||||
"down_number_tip": "Number of downloads executed simultaneously",
|
||||
"use_headers_tip": "Define headers for downloads, comma separated",
|
||||
"use_headers_tip": "Default system filtering, Define headers for downloads, comma separated",
|
||||
"mime_map": "Intercept Rules",
|
||||
"mime_map_tip": "JSON format, keep default if unsure, please restart software after modification",
|
||||
"domain_rule": "Domain Rule",
|
||||
"domain_rule_tip": "Default * matches all domains, One line for each rule,supports the following: \n*.qq.com\nvideo.qq.com\nexample.com\n\n# Exclude\n!static.qq.com",
|
||||
"port_format_error": "port format error",
|
||||
"host_format_error": "host format error",
|
||||
"basic_setting": "Basic Setting",
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"video": "视频",
|
||||
"m3u8": "m3u8",
|
||||
"live": "直播流",
|
||||
"stream": "流数据",
|
||||
"xls": "表格",
|
||||
"doc": "文档",
|
||||
"pdf": "pdf",
|
||||
@@ -55,7 +56,7 @@
|
||||
"choice": "已选",
|
||||
"type": "类型",
|
||||
"preview": "预览",
|
||||
"preview_tip": "暂不支持预览",
|
||||
"preview_tip": "无法预览",
|
||||
"status": "状态",
|
||||
"description": "描述",
|
||||
"resource_size": "资源大小",
|
||||
@@ -76,6 +77,7 @@
|
||||
"open_link": "打开链接",
|
||||
"open_file": "打开文件",
|
||||
"delete_row": "删除记录",
|
||||
"delete_tip": "运行中任务无法删除",
|
||||
"cancel_down": "取消下载",
|
||||
"more_operation": "更多操作",
|
||||
"video_decode": "视频解密",
|
||||
@@ -118,9 +120,11 @@
|
||||
"connections_tip": "如不清楚请保持默认,通常CPU核心数*2,用于加速下载",
|
||||
"down_number": "下载数",
|
||||
"down_number_tip": "同时进行的下载数量",
|
||||
"use_headers_tip": "定义下载时可使用的header参数,逗号分割",
|
||||
"use_headers_tip": "默认系统过滤,定义下载时可使用的header参数,逗号分割",
|
||||
"mime_map": "拦截规则",
|
||||
"mime_map_tip": "json格式,如果不清楚保持默认就行,修改后请重启软件",
|
||||
"domain_rule": "域名规则",
|
||||
"domain_rule_tip": "默认*匹配所有域,每个规则一行,支持如下: \n*.qq.com\nvideo.qq.com\nexample.com\n\n# 排除\n!static.qq.com",
|
||||
"port_format_error": "port 格式错误",
|
||||
"host_format_error": "host 格式错误",
|
||||
"basic_setting": "基础设置",
|
||||
|
||||
@@ -33,7 +33,8 @@ export const useIndexStore = defineStore("index-store", () => {
|
||||
UserAgent: "",
|
||||
UseHeaders: "",
|
||||
InsertTail: true,
|
||||
MimeMap: {}
|
||||
MimeMap: {},
|
||||
Rule: "*"
|
||||
})
|
||||
|
||||
const envInfo = ref({
|
||||
|
||||
1
frontend/src/types/app.d.ts
vendored
1
frontend/src/types/app.d.ts
vendored
@@ -31,6 +31,7 @@ export namespace appType {
|
||||
UseHeaders: string
|
||||
InsertTail: boolean
|
||||
MimeMap: { [key: string]: MimeMap }
|
||||
Rule: string
|
||||
}
|
||||
|
||||
interface MediaInfo {
|
||||
|
||||
@@ -151,6 +151,7 @@ import ImportJson from "@/components/ImportJson.vue"
|
||||
import {useEventStore} from "@/stores/event"
|
||||
import {BrowserOpenURL, ClipboardSetText} from "../../wailsjs/runtime"
|
||||
import Password from "@/components/Password.vue"
|
||||
import ShowOrEdit from "@/components/ShowOrEdit.vue"
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {
|
||||
DownloadOutline,
|
||||
@@ -188,6 +189,10 @@ const filteredData = computed(() => {
|
||||
result = result.filter(item => item.Description?.toLowerCase().includes(descriptionSearchValue.value.toLowerCase()))
|
||||
}
|
||||
|
||||
if (urlSearchValue.value) {
|
||||
result = result.filter(item => item.Url?.toLowerCase().includes(urlSearchValue.value.toLowerCase()))
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
@@ -204,6 +209,7 @@ const classifyAlias: { [key: string]: any } = {
|
||||
xls: computed(() => t("index.xls")),
|
||||
doc: computed(() => t("index.doc")),
|
||||
pdf: computed(() => t("index.pdf")),
|
||||
stream: computed(() => t("index.stream")),
|
||||
font: computed(() => t("index.font"))
|
||||
}
|
||||
|
||||
@@ -230,6 +236,7 @@ const classify = ref([
|
||||
])
|
||||
|
||||
const descriptionSearchValue = ref("")
|
||||
const urlSearchValue = ref("")
|
||||
const rememberChoice = ref(false)
|
||||
const rememberChoiceTmp = ref(false)
|
||||
|
||||
@@ -238,11 +245,49 @@ const columns = ref<any[]>([
|
||||
type: "selection",
|
||||
},
|
||||
{
|
||||
title: computed(() => {
|
||||
return checkedRowKeysValue.value.length > 0 ? h(NGradientText, {type: "success"}, t("index.choice") + `(${checkedRowKeysValue.value.length})`) : t("index.domain")
|
||||
}),
|
||||
title: () => {
|
||||
if (checkedRowKeysValue.value.length > 0) {
|
||||
return h(NGradientText, {type: "success"}, t("index.choice") + `(${checkedRowKeysValue.value.length})`)
|
||||
}
|
||||
return h('div', {class: 'flex items-center'}, [
|
||||
t('index.domain'),
|
||||
h(NPopover, {
|
||||
style: "--wails-draggable:no-drag",
|
||||
trigger: 'click',
|
||||
placement: 'bottom',
|
||||
showArrow: true,
|
||||
}, {
|
||||
trigger: () => h(NIcon, {
|
||||
size: "18",
|
||||
class: `ml-1 cursor-pointer ${urlSearchValue.value ? "text-green-600": "text-gray-500"}`,
|
||||
onClick: (e: MouseEvent) => e.stopPropagation()
|
||||
}, h(SearchOutline)),
|
||||
default: () => h('div', {class: 'p-2 w-64'}, [
|
||||
h(NInput, {
|
||||
value: urlSearchValue.value,
|
||||
'onUpdate:value': (val: string) => urlSearchValue.value = val,
|
||||
placeholder: t('index.search_description'),
|
||||
clearable: true
|
||||
}, {
|
||||
prefix: () => h(NIcon, {component: SearchOutline})
|
||||
})
|
||||
])
|
||||
})
|
||||
])
|
||||
},
|
||||
key: "Domain",
|
||||
width: 90,
|
||||
render: (row: appType.MediaInfo) => {
|
||||
return h(NTooltip, {
|
||||
trigger: 'hover',
|
||||
placement: 'top'
|
||||
}, {
|
||||
trigger: () => h('span', {
|
||||
class: 'cursor-default'
|
||||
}, row.Domain),
|
||||
default: () => row.Url
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: computed(() => t("index.type")),
|
||||
@@ -351,7 +396,7 @@ const columns = ref<any[]>([
|
||||
}, {
|
||||
trigger: () => h(NIcon, {
|
||||
size: "18",
|
||||
class: "ml-1 text-gray-500 cursor-pointer",
|
||||
class: `ml-1 cursor-pointer ${descriptionSearchValue.value ? "text-green-600": "text-gray-500"}`,
|
||||
onClick: (e: MouseEvent) => e.stopPropagation()
|
||||
}, h(SearchOutline)),
|
||||
default: () => h('div', {class: 'p-2 w-64'}, [
|
||||
@@ -369,10 +414,12 @@ const columns = ref<any[]>([
|
||||
key: "Description",
|
||||
width: 150,
|
||||
render: (row: appType.MediaInfo, index: number) => {
|
||||
const d = h("div", {class: "ellipsis-2",}, row.Description)
|
||||
return h(NTooltip, {trigger: 'hover', placement: 'top'}, {
|
||||
trigger: () => d,
|
||||
default: () => d
|
||||
return h(ShowOrEdit, {
|
||||
value: row.Description,
|
||||
onUpdateValue(v: string) {
|
||||
data.value[index].Description = v
|
||||
cacheData()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -586,7 +633,17 @@ const dataAction = (row: appType.MediaInfo, index: number, type: string) => {
|
||||
download(row, index)
|
||||
break
|
||||
case "cancel":
|
||||
if (row.Status === "running") {
|
||||
if (row.Status === "pending") {
|
||||
const queueIndex = downloadQueue.value.findIndex(item => item.Id === row.Id)
|
||||
if (queueIndex !== -1) {
|
||||
downloadQueue.value.splice(queueIndex, 1)
|
||||
}
|
||||
updateItem(row.Id, item => {
|
||||
item.Status = 'ready'
|
||||
item.SavePath = ''
|
||||
})
|
||||
cacheData()
|
||||
} else if (row.Status === "running") {
|
||||
appApi.cancel({id: row.Id}).then((res) => {
|
||||
updateItem(row.Id, item => {
|
||||
item.Status = 'ready'
|
||||
@@ -629,7 +686,11 @@ const dataAction = (row: appType.MediaInfo, index: number, type: string) => {
|
||||
decodeWxFile(row, index)
|
||||
break
|
||||
case "delete":
|
||||
appApi.delete({sign: row.UrlSign}).then(() => {
|
||||
if (row.Status === "pending" || row.Status === "running") {
|
||||
window?.$message?.error(t("index.delete_tip"))
|
||||
return
|
||||
}
|
||||
appApi.delete({sign: [row.UrlSign]}).then(() => {
|
||||
data.value.splice(index, 1)
|
||||
cacheData()
|
||||
})
|
||||
@@ -688,7 +749,21 @@ const batchCancel = async () => {
|
||||
loading.value = true
|
||||
const cancelTasks: Promise<any>[] = []
|
||||
data.value.forEach((item, index) => {
|
||||
if (checkedRowKeysValue.value.includes(item.Id) && item.Status === "running") {
|
||||
if (!checkedRowKeysValue.value.includes(item.Id)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (item.Status === "pending") {
|
||||
const queueIndex = downloadQueue.value.findIndex(qItem => qItem.Id === item.Id)
|
||||
if (queueIndex !== -1) {
|
||||
downloadQueue.value.splice(queueIndex, 1)
|
||||
}
|
||||
item.Status = 'ready'
|
||||
item.SavePath = ''
|
||||
return
|
||||
}
|
||||
|
||||
if (item.Status === "running") {
|
||||
if (activeDownloads > 0) {
|
||||
activeDownloads--
|
||||
}
|
||||
@@ -813,25 +888,30 @@ const close = () => {
|
||||
store.unsetProxy()
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
const clear = async () => {
|
||||
const newData = [] as any[]
|
||||
const signs: string[] = []
|
||||
if (checkedRowKeysValue.value.length > 0) {
|
||||
let newData = [] as any[]
|
||||
data.value.forEach((item, index) => {
|
||||
if (checkedRowKeysValue.value.includes(item.Id)) {
|
||||
appApi.delete({sign: item.UrlSign})
|
||||
if (checkedRowKeysValue.value.includes(item.Id) && item.Status !== "pending" && item.Status !== "running") {
|
||||
signs.push(item.UrlSign)
|
||||
} else {
|
||||
newData.push(item)
|
||||
}
|
||||
})
|
||||
data.value = newData
|
||||
checkedRowKeysValue.value = []
|
||||
cacheData()
|
||||
return
|
||||
} else {
|
||||
data.value.forEach((item, index) => {
|
||||
if (item.Status === "pending" || item.Status === "running") {
|
||||
newData.push(item)
|
||||
} else {
|
||||
signs.push(item.UrlSign)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
data.value = []
|
||||
localStorage.setItem("resources-data", "")
|
||||
appApi.clear()
|
||||
await appApi.delete({sign: signs})
|
||||
data.value = newData
|
||||
cacheData()
|
||||
}
|
||||
|
||||
const decodeWxFile = (row: appType.MediaInfo, index: number) => {
|
||||
@@ -952,13 +1032,4 @@ const checkLoading = () => {
|
||||
}
|
||||
}, 6000)
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.ellipsis-2 {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
</style>
|
||||
</script>
|
||||
@@ -197,6 +197,22 @@
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="t('setting.domain_rule')" path="DomainRule">
|
||||
<NInput
|
||||
v-model:value="formValue.Rule"
|
||||
type="textarea"
|
||||
rows="5"
|
||||
:placeholder="t('setting.domain_rule_tip')"
|
||||
/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.domain_rule_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
<NFormItem :label="t('setting.mime_map')" path="MimeMap">
|
||||
<NInput
|
||||
v-model:value="MimeMap"
|
||||
|
||||
2
go.mod
2
go.mod
@@ -5,7 +5,7 @@ go 1.22.0
|
||||
toolchain go1.23.2
|
||||
|
||||
require (
|
||||
github.com/elazarl/goproxy v0.0.0-20241223171911-d5978cb8c956
|
||||
github.com/elazarl/goproxy v1.7.2
|
||||
github.com/matoous/go-nanoid/v2 v2.1.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68
|
||||
|
||||
5
go.sum
5
go.sum
@@ -3,8 +3,8 @@ github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3IS
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elazarl/goproxy v0.0.0-20241223171911-d5978cb8c956 h1:HyPt0ZkHkpke+HFl/4dDMz55A/AjFn7ZnLSm8GfdnwU=
|
||||
github.com/elazarl/goproxy v0.0.0-20241223171911-d5978cb8c956/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
@@ -90,4 +90,3 @@ golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"info": {
|
||||
"companyName": "res-downloader",
|
||||
"productName": "res-downloader",
|
||||
"productVersion": "3.1.2",
|
||||
"productVersion": "3.1.3",
|
||||
"copyright": "Copyright © 2023",
|
||||
"comments": "This is a high-value high-performance and diverse resource downloader called res-downloader."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user