mirror of
https://github.com/putyy/res-downloader.git
synced 2026-01-12 14:14:55 +08:00
Compare commits
16 Commits
04e4f0e9cc
...
3.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da9039ea9c | ||
|
|
dc46668f0d | ||
|
|
142a2a84b4 | ||
|
|
84b6a142fc | ||
|
|
a853f1d991 | ||
|
|
dc261bb6ce | ||
|
|
d0ea8e4fab | ||
|
|
c1cce920a4 | ||
|
|
f0495c6858 | ||
|
|
a37bde428d | ||
|
|
7793f83ea3 | ||
|
|
29cc879b85 | ||
|
|
abfdb76589 | ||
|
|
3f07bae796 | ||
|
|
62bf0e2308 | ||
|
|
575e2d8904 |
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -8,7 +8,7 @@ body:
|
||||
attributes:
|
||||
label: "Title \\ 标题"
|
||||
description: "A brief summary of your feature request. \\ 对您功能建议的简要总结。"
|
||||
placeholder: "Enter the feature title here. \\ 在此输入功能标题。"
|
||||
placeholder: "Enter the feature title here. \\ 在此输入功能标题。 "
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
.idea
|
||||
test
|
||||
build/bin
|
||||
node_modules
|
||||
frontend/dist
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -20,7 +20,7 @@
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
|
||||
BIN
build/.DS_Store
vendored
BIN
build/.DS_Store
vendored
Binary file not shown.
@@ -32,11 +32,20 @@ cp build/bin/res-downloader build/linux/AppImage/usr/bin/
|
||||
|
||||
# 复制WebKit相关文件
|
||||
pushd build/linux/AppImage
|
||||
find /usr/lib* -name WebKitNetworkProcess -exec mkdir -p $(dirname '{}') \; -exec cp --parents '{}' "." \; || true
|
||||
find /usr/lib* -name WebKitWebProcess -exec mkdir -p $(dirname '{}') \; -exec cp --parents '{}' "." \; || true
|
||||
find /usr/lib* -name libwebkit2gtkinjectedbundle.so -exec mkdir -p $(dirname '{}') \; -exec cp --parents '{}' "." \; || true
|
||||
|
||||
for f in WebKitNetworkProcess WebKitWebProcess libwebkit2gtkinjectedbundle.so; do
|
||||
path=$(find /usr/lib* -name "$f" 2>/dev/null | head -n 1)
|
||||
if [ -n "$path" ]; then
|
||||
mkdir -p ./$(dirname "$path")
|
||||
cp --parents "$path" .
|
||||
else
|
||||
echo "⚠️ $f not found, you may need to install libwebkit2gtk"
|
||||
fi
|
||||
done
|
||||
|
||||
popd
|
||||
|
||||
# 下载appimagetool
|
||||
wget -O ./build/bin/appimagetool-x86_64.AppImage https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage
|
||||
chmod +x ./build/bin/appimagetool-x86_64.AppImage
|
||||
./build/bin/appimagetool-x86_64.AppImage build/linux/AppImage build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64.AppImage
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
!define INFO_PRODUCTNAME "res-downloader"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "3.0.5"
|
||||
!define INFO_PRODUCTVERSION "3.0.6"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "Copyright © 2023"
|
||||
|
||||
72
core/aes.go
Normal file
72
core/aes.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
type AESCipher struct {
|
||||
key []byte
|
||||
}
|
||||
|
||||
func NewAESCipher(key string) *AESCipher {
|
||||
return &AESCipher{key: []byte(key)}
|
||||
}
|
||||
|
||||
func (a *AESCipher) Encrypt(plainText string) (string, error) {
|
||||
block, err := aes.NewCipher(a.key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
padding := block.BlockSize() - len(plainText)%block.BlockSize()
|
||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
plainText = plainText + string(padText)
|
||||
|
||||
cipherText := make([]byte, aes.BlockSize+len(plainText))
|
||||
iv := cipherText[:aes.BlockSize]
|
||||
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode.CryptBlocks(cipherText[aes.BlockSize:], []byte(plainText))
|
||||
|
||||
return base64.StdEncoding.EncodeToString(cipherText), nil
|
||||
}
|
||||
|
||||
func (a *AESCipher) Decrypt(cipherText string) (string, error) {
|
||||
cipherTextBytes, err := base64.StdEncoding.DecodeString(cipherText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(a.key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(cipherTextBytes) < aes.BlockSize {
|
||||
return "", errors.New("ciphertext too short")
|
||||
}
|
||||
|
||||
iv := cipherTextBytes[:aes.BlockSize]
|
||||
cipherTextBytes = cipherTextBytes[aes.BlockSize:]
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(cipherTextBytes, cipherTextBytes)
|
||||
|
||||
padding := int(cipherTextBytes[len(cipherTextBytes)-1])
|
||||
if padding > len(cipherTextBytes) || padding > aes.BlockSize {
|
||||
return "", errors.New("padding size error")
|
||||
}
|
||||
plainText := cipherTextBytes[:len(cipherTextBytes)-padding]
|
||||
|
||||
return string(plainText), nil
|
||||
}
|
||||
56
core/app.go
56
core/app.go
@@ -3,14 +3,13 @@ package core
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/vrischmann/userdir"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
sysRuntime "runtime"
|
||||
"res-downloader/core/shared"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -25,7 +24,7 @@ type App struct {
|
||||
LockFile string `json:"-"`
|
||||
PublicCrt []byte `json:"-"`
|
||||
PrivateKey []byte `json:"-"`
|
||||
IsProxy bool `json:"-"`
|
||||
IsProxy bool `json:"IsProxy"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -52,8 +51,7 @@ func GetApp(assets embed.FS, wjs string) *App {
|
||||
Version: version,
|
||||
Description: "res-downloader是一款集网络资源嗅探 + 高速下载功能于一体的软件,高颜值、高性能和多样化,提供个人用户下载自己上传到各大平台的网络资源功能!",
|
||||
Copyright: "Copyright © 2023~" + strconv.Itoa(time.Now().Year()),
|
||||
PublicCrt: []byte(`
|
||||
-----BEGIN CERTIFICATE-----
|
||||
PublicCrt: []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDwzCCAqugAwIBAgIUFAnC6268dp/z1DR9E1UepiWgWzkwDQYJKoZIhvcNAQEL
|
||||
BQAwcDELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUNob25ncWluZzESMBAGA1UEBwwJ
|
||||
Q2hvbmdxaW5nMQ4wDAYDVQQKDAVnb3dhczEWMBQGA1UECwwNSVQgRGVwYXJ0bWVu
|
||||
@@ -77,8 +75,7 @@ e3oowvgwikqm6XR6BEcRpPkztqcKST7jPFGHiXWsAqiibc+/plMW9qebhfMXEGhQ
|
||||
D8HixYbEDg==
|
||||
-----END CERTIFICATE-----
|
||||
`),
|
||||
PrivateKey: []byte(`
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
PrivateKey: []byte(`-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDcDt23t6ioBoHG
|
||||
/Y2mOjxntWQa9dP3eNl+mAC6425DlEtyc6czNAIKuuM9wt+wAwDQAgrd5RaxdcpJ
|
||||
H1JlMkEtBFkIkdn0Ag98D7nwlVA9ON3xQi5Bkl+sN/oWOE8lOwvNyNNT6ZPu3qUS
|
||||
@@ -109,6 +106,10 @@ ILKEQKmPPzKs7kp/7Nz+2cT3
|
||||
`),
|
||||
}
|
||||
appOnce.UserDir = filepath.Join(userdir.GetConfigHome(), appOnce.AppName)
|
||||
err := os.MkdirAll(appOnce.UserDir, 0750)
|
||||
if err != nil {
|
||||
fmt.Println("Mkdir UserDir err: ", err.Error())
|
||||
}
|
||||
appOnce.LockFile = filepath.Join(appOnce.UserDir, "install.lock")
|
||||
initLogger()
|
||||
initConfig()
|
||||
@@ -123,22 +124,6 @@ ILKEQKmPPzKs7kp/7Nz+2cT3
|
||||
func (a *App) Startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
go httpServerOnce.run()
|
||||
time.AfterFunc(200*time.Millisecond, func() {
|
||||
if globalConfig.AutoProxy {
|
||||
appOnce.OpenSystemProxy()
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
if a.isInstall() {
|
||||
return
|
||||
}
|
||||
err := os.MkdirAll(a.UserDir, 0750)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
a.installCert()
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *App) OnExit() {
|
||||
@@ -146,24 +131,17 @@ func (a *App) OnExit() {
|
||||
globalLogger.Close()
|
||||
}
|
||||
|
||||
func (a *App) installCert() {
|
||||
if res, err := systemOnce.installCert(); err != nil {
|
||||
if sysRuntime.GOOS == "darwin" {
|
||||
_ = runtime.ClipboardSetText(appOnce.ctx, `echo "输入本地登录密码" && sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "`+systemOnce.CertFile+`" && touch `+a.LockFile+` && echo "安装完成"`)
|
||||
DialogErr("证书安装失败,请打开终端执行安装(命令已复制到剪切板),err:" + err.Error() + ", " + res)
|
||||
} else if sysRuntime.GOOS == "windows" && strings.Contains(err.Error(), "Access is denied.") {
|
||||
DialogErr("首次启用本软件,请使用鼠标右键选择以管理员身份运行")
|
||||
} else if sysRuntime.GOOS == "linux" && strings.Contains(err.Error(), "Access is denied.") {
|
||||
DialogErr("证书路径: " + systemOnce.CertFile + ", 请手动安装,安装完成后请执行: touch" + a.LockFile + " err:" + err.Error() + ", " + res)
|
||||
} else {
|
||||
globalLogger.Esg(err, res)
|
||||
DialogErr("err:" + err.Error() + ", " + res)
|
||||
}
|
||||
func (a *App) installCert() (string, error) {
|
||||
out, err := systemOnce.installCert()
|
||||
if err != nil {
|
||||
globalLogger.Esg(err, out)
|
||||
return out, err
|
||||
} else {
|
||||
if err := a.lock(); err != nil {
|
||||
globalLogger.err(err)
|
||||
globalLogger.Err(err)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (a *App) OpenSystemProxy() error {
|
||||
@@ -191,7 +169,7 @@ func (a *App) UnsetSystemProxy() error {
|
||||
}
|
||||
|
||||
func (a *App) isInstall() bool {
|
||||
return FileExist(a.LockFile)
|
||||
return shared.FileExist(a.LockFile)
|
||||
}
|
||||
|
||||
func (a *App) lock() error {
|
||||
|
||||
16
core/bind.go
Normal file
16
core/bind.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package core
|
||||
|
||||
type Bind struct {
|
||||
}
|
||||
|
||||
func NewBind() *Bind {
|
||||
return &Bind{}
|
||||
}
|
||||
|
||||
func (b *Bind) Config() *ResponseData {
|
||||
return httpServerOnce.buildResp(1, "ok", globalConfig)
|
||||
}
|
||||
|
||||
func (b *Bind) AppInfo() *ResponseData {
|
||||
return httpServerOnce.buildResp(1, "ok", appOnce)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ type MimeInfo struct {
|
||||
type Config struct {
|
||||
storage *Storage
|
||||
Theme string `json:"Theme"`
|
||||
Locale string `json:"Locale"`
|
||||
Host string `json:"Host"`
|
||||
Port string `json:"Port"`
|
||||
Quality int `json:"Quality"`
|
||||
@@ -45,6 +46,7 @@ func initConfig() *Config {
|
||||
"Host": "127.0.0.1",
|
||||
"Port": "8899",
|
||||
"Theme": "lightTheme",
|
||||
"Locale": "zh",
|
||||
"Quality": 0,
|
||||
"SaveDirectory": "",
|
||||
"FilenameLen": 0,
|
||||
@@ -52,7 +54,7 @@ func initConfig() *Config {
|
||||
"UpstreamProxy": "",
|
||||
"OpenProxy": false,
|
||||
"DownloadProxy": false,
|
||||
"AutoProxy": true,
|
||||
"AutoProxy": false,
|
||||
"WxAction": true,
|
||||
"TaskNumber": __TaskNumber__,
|
||||
"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",
|
||||
@@ -152,6 +154,7 @@ func (c *Config) setConfig(config Config) {
|
||||
c.Host = config.Host
|
||||
c.Port = config.Port
|
||||
c.Theme = config.Theme
|
||||
c.Locale = config.Locale
|
||||
c.Quality = config.Quality
|
||||
c.SaveDirectory = config.SaveDirectory
|
||||
c.FilenameLen = config.FilenameLen
|
||||
@@ -177,3 +180,56 @@ func (c *Config) setConfig(config Config) {
|
||||
_ = globalConfig.storage.Store(jsonData)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) getConfig(key string) interface{} {
|
||||
switch key {
|
||||
case "Host":
|
||||
return c.Host
|
||||
case "Port":
|
||||
return c.Port
|
||||
case "Theme":
|
||||
return c.Theme
|
||||
case "Locale":
|
||||
return c.Locale
|
||||
case "Quality":
|
||||
return c.Quality
|
||||
case "SaveDirectory":
|
||||
return c.SaveDirectory
|
||||
case "FilenameLen":
|
||||
return c.FilenameLen
|
||||
case "FilenameTime":
|
||||
return c.FilenameTime
|
||||
case "UpstreamProxy":
|
||||
return c.UpstreamProxy
|
||||
case "UserAgent":
|
||||
return c.UserAgent
|
||||
case "OpenProxy":
|
||||
return c.OpenProxy
|
||||
case "DownloadProxy":
|
||||
return c.DownloadProxy
|
||||
case "AutoProxy":
|
||||
return c.AutoProxy
|
||||
case "TaskNumber":
|
||||
return c.TaskNumber
|
||||
case "WxAction":
|
||||
return c.WxAction
|
||||
case "UseHeaders":
|
||||
return c.UseHeaders
|
||||
case "MimeMap":
|
||||
mimeMux.RLock()
|
||||
defer mimeMux.RUnlock()
|
||||
return c.MimeMap
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) typeSuffix(mime string) (string, string) {
|
||||
mimeMux.RLock()
|
||||
defer mimeMux.RUnlock()
|
||||
mime = strings.ToLower(strings.Split(mime, ";")[0])
|
||||
if v, ok := c.MimeMap[mime]; ok {
|
||||
return v.Type, v.Suffix
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProgressCallback func(totalDownloaded float64, totalSize float64)
|
||||
const (
|
||||
MaxRetries = 3 // 最大重试次数
|
||||
RetryDelay = 3 * time.Second // 重试延迟
|
||||
MinPartSize = 1 * 1024 * 1024 // 最小分片大小(1MB)
|
||||
)
|
||||
|
||||
type ProgressCallback func(totalDownloaded float64, totalSize float64, taskID int, taskProgress float64)
|
||||
|
||||
type ProgressChan struct {
|
||||
taskID int
|
||||
bytes int64
|
||||
}
|
||||
|
||||
type DownloadTask struct {
|
||||
taskID int
|
||||
@@ -20,6 +32,7 @@ type DownloadTask struct {
|
||||
rangeEnd int64
|
||||
downloadedSize int64
|
||||
isCompleted bool
|
||||
err error
|
||||
}
|
||||
|
||||
type FileDownloader struct {
|
||||
@@ -49,12 +62,16 @@ func NewFileDownloader(url, filename string, totalTasks int, headers map[string]
|
||||
}
|
||||
|
||||
func (fd *FileDownloader) buildClient() *http.Client {
|
||||
transport := &http.Transport{}
|
||||
transport := &http.Transport{
|
||||
MaxIdleConnsPerHost: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
}
|
||||
if fd.ProxyUrl != nil {
|
||||
transport.Proxy = http.ProxyURL(fd.ProxyUrl)
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +86,7 @@ func (fd *FileDownloader) setHeaders(request *http.Request) {
|
||||
func (fd *FileDownloader) init() error {
|
||||
parsedURL, err := url.Parse(fd.Url)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("parse URL failed: %w", err)
|
||||
}
|
||||
if parsedURL.Scheme != "" && parsedURL.Host != "" {
|
||||
fd.Referer = parsedURL.Scheme + "://" + parsedURL.Host + "/"
|
||||
@@ -95,23 +112,36 @@ func (fd *FileDownloader) init() error {
|
||||
}
|
||||
|
||||
fd.setHeaders(request)
|
||||
resp, err := fd.buildClient().Do(request)
|
||||
|
||||
var resp *http.Response
|
||||
for retries := 0; retries < MaxRetries; retries++ {
|
||||
resp, err = fd.buildClient().Do(request)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if retries < MaxRetries-1 {
|
||||
time.Sleep(RetryDelay)
|
||||
globalLogger.Warn().Msgf("HEAD request failed, retrying (%d/%d): %v", retries+1, MaxRetries, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("HEAD request failed: %w", err)
|
||||
return fmt.Errorf("HEAD request failed after %d retries: %w", MaxRetries, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
fd.TotalSize = resp.ContentLength
|
||||
if fd.TotalSize <= 0 {
|
||||
return fmt.Errorf("invalid file size")
|
||||
return errors.New("invalid file size")
|
||||
}
|
||||
if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > 10*1024*1024 {
|
||||
|
||||
if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > MinPartSize {
|
||||
fd.IsMultiPart = true
|
||||
}
|
||||
|
||||
dir := filepath.Dir(fd.FileName)
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("create directory failed: %w", err)
|
||||
}
|
||||
fd.File, err = os.OpenFile(fd.FileName, os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
@@ -126,10 +156,18 @@ func (fd *FileDownloader) init() error {
|
||||
|
||||
func (fd *FileDownloader) createDownloadTasks() {
|
||||
if fd.IsMultiPart {
|
||||
if int64(fd.totalTasks) > fd.TotalSize {
|
||||
fd.totalTasks = int(fd.TotalSize)
|
||||
if fd.totalTasks <= 0 {
|
||||
fd.totalTasks = 4
|
||||
}
|
||||
eachSize := fd.TotalSize / int64(fd.totalTasks)
|
||||
if eachSize < MinPartSize {
|
||||
fd.totalTasks = int(fd.TotalSize / MinPartSize)
|
||||
if fd.totalTasks < 1 {
|
||||
fd.totalTasks = 1
|
||||
}
|
||||
eachSize = fd.TotalSize / int64(fd.totalTasks)
|
||||
}
|
||||
|
||||
for i := 0; i < fd.totalTasks; i++ {
|
||||
start := eachSize * int64(i)
|
||||
end := eachSize*int64(i+1) - 1
|
||||
@@ -143,91 +181,186 @@ func (fd *FileDownloader) createDownloadTasks() {
|
||||
})
|
||||
}
|
||||
} else {
|
||||
fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{taskID: 0})
|
||||
fd.totalTasks = 1
|
||||
fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{
|
||||
taskID: 0,
|
||||
rangeStart: 0,
|
||||
rangeEnd: fd.TotalSize - 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (fd *FileDownloader) startDownload() {
|
||||
func (fd *FileDownloader) startDownload() error {
|
||||
wg := &sync.WaitGroup{}
|
||||
progressChan := make(chan int64)
|
||||
progressChan := make(chan ProgressChan, len(fd.DownloadTaskList))
|
||||
errorChan := make(chan error, len(fd.DownloadTaskList))
|
||||
|
||||
for _, task := range fd.DownloadTaskList {
|
||||
wg.Add(1)
|
||||
go fd.startDownloadTask(wg, progressChan, task)
|
||||
go fd.startDownloadTask(wg, progressChan, errorChan, task)
|
||||
}
|
||||
|
||||
go func() {
|
||||
taskProgress := make([]int64, len(fd.DownloadTaskList))
|
||||
totalDownloaded := int64(0)
|
||||
|
||||
for progress := range progressChan {
|
||||
taskProgress[progress.taskID] += progress.bytes
|
||||
totalDownloaded += progress.bytes
|
||||
|
||||
if fd.progressCallback != nil {
|
||||
taskPercentage := float64(0)
|
||||
if task := fd.DownloadTaskList[progress.taskID]; task != nil {
|
||||
taskSize := task.rangeEnd - task.rangeStart + 1
|
||||
if taskSize > 0 {
|
||||
taskPercentage = float64(taskProgress[progress.taskID]) / float64(taskSize) * 100
|
||||
}
|
||||
}
|
||||
fd.progressCallback(float64(totalDownloaded), float64(fd.TotalSize), progress.taskID, taskPercentage)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(progressChan)
|
||||
close(errorChan)
|
||||
}()
|
||||
|
||||
if fd.progressCallback != nil {
|
||||
totalDownloaded := int64(0)
|
||||
for p := range progressChan {
|
||||
totalDownloaded += p
|
||||
fd.progressCallback(float64(totalDownloaded), float64(fd.TotalSize))
|
||||
}
|
||||
var errArr []error
|
||||
for err := range errorChan {
|
||||
errArr = append(errArr, err)
|
||||
}
|
||||
|
||||
if len(errArr) > 0 {
|
||||
return fmt.Errorf("download failed with %d errors: %v", len(errArr), errArr[0])
|
||||
}
|
||||
|
||||
if err := fd.verifyDownload(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan chan int64, task *DownloadTask) {
|
||||
func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan chan ProgressChan, errorChan chan error, task *DownloadTask) {
|
||||
defer wg.Done()
|
||||
|
||||
for retries := 0; retries < MaxRetries; retries++ {
|
||||
err := fd.doDownloadTask(progressChan, task)
|
||||
if err == nil {
|
||||
task.isCompleted = true
|
||||
return
|
||||
}
|
||||
|
||||
task.err = err
|
||||
globalLogger.Warn().Msgf("Task %d failed (attempt %d/%d): %v", task.taskID, retries+1, MaxRetries, err)
|
||||
|
||||
if retries < MaxRetries-1 {
|
||||
time.Sleep(RetryDelay)
|
||||
}
|
||||
}
|
||||
|
||||
errorChan <- fmt.Errorf("task %d failed after %d attempts: %v", task.taskID, MaxRetries, task.err)
|
||||
}
|
||||
|
||||
func (fd *FileDownloader) doDownloadTask(progressChan chan ProgressChan, task *DownloadTask) error {
|
||||
request, err := http.NewRequest("GET", fd.Url, nil)
|
||||
if err != nil {
|
||||
globalLogger.Error().Stack().Err(err).Msgf("任务%d创建请求出错", task.taskID)
|
||||
return
|
||||
return fmt.Errorf("create request failed: %w", err)
|
||||
}
|
||||
fd.setHeaders(request)
|
||||
|
||||
if fd.IsMultiPart {
|
||||
rangeHeader := fmt.Sprintf("bytes=%d-%d", task.rangeStart, task.rangeEnd)
|
||||
rangeStart := task.rangeStart + task.downloadedSize
|
||||
rangeHeader := fmt.Sprintf("bytes=%d-%d", rangeStart, task.rangeEnd)
|
||||
request.Header.Set("Range", rangeHeader)
|
||||
}
|
||||
|
||||
client := fd.buildClient()
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Printf("任务%d发送下载请求出错!%s", task.taskID, err)
|
||||
return
|
||||
return fmt.Errorf("send request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
buf := make([]byte, 8192)
|
||||
if fd.IsMultiPart && resp.StatusCode != http.StatusPartialContent {
|
||||
return fmt.Errorf("server does not support range requests, status: %d", resp.StatusCode)
|
||||
} else if !fd.IsMultiPart && resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
remain := task.rangeEnd - (task.rangeStart + task.downloadedSize) + 1
|
||||
n64 := int64(n)
|
||||
if n64 > remain {
|
||||
n = int(remain)
|
||||
writeSize := int64(n)
|
||||
if writeSize > remain {
|
||||
writeSize = remain
|
||||
}
|
||||
_, writeErr := fd.File.WriteAt(buf[:n], task.rangeStart+task.downloadedSize)
|
||||
|
||||
_, writeErr := fd.File.WriteAt(buf[:writeSize], task.rangeStart+task.downloadedSize)
|
||||
if writeErr != nil {
|
||||
log.Printf("任务%d写入文件时出现错误!位置:%d, err: %s\n", task.taskID, task.rangeStart+task.downloadedSize, writeErr)
|
||||
return
|
||||
return fmt.Errorf("write file failed at offset %d: %w", task.rangeStart+task.downloadedSize, writeErr)
|
||||
}
|
||||
task.downloadedSize += n64
|
||||
progressChan <- n64
|
||||
|
||||
task.downloadedSize += writeSize
|
||||
progressChan <- ProgressChan{taskID: task.taskID, bytes: writeSize}
|
||||
|
||||
if task.rangeStart+task.downloadedSize-1 >= task.rangeEnd {
|
||||
task.isCompleted = true
|
||||
break
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
task.isCompleted = true
|
||||
expectedSize := task.rangeEnd - task.rangeStart + 1
|
||||
if task.downloadedSize < expectedSize {
|
||||
return fmt.Errorf("incomplete download: got %d bytes, expected %d", task.downloadedSize, expectedSize)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
break
|
||||
return fmt.Errorf("read response failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (fd *FileDownloader) verifyDownload() error {
|
||||
for _, task := range fd.DownloadTaskList {
|
||||
if !task.isCompleted {
|
||||
return fmt.Errorf("task %d not completed", task.taskID)
|
||||
}
|
||||
|
||||
expectedSize := task.rangeEnd - task.rangeStart + 1
|
||||
if task.downloadedSize != expectedSize {
|
||||
return fmt.Errorf("task %d size mismatch: got %d, expected %d", task.taskID, task.downloadedSize, expectedSize)
|
||||
}
|
||||
}
|
||||
|
||||
info, err := fd.File.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get file info failed: %w", err)
|
||||
}
|
||||
|
||||
if info.Size() != fd.TotalSize {
|
||||
return fmt.Errorf("file size mismatch: got %d, expected %d", info.Size(), fd.TotalSize)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fd *FileDownloader) Start() error {
|
||||
if err := fd.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
fd.createDownloadTasks()
|
||||
fd.startDownload()
|
||||
defer fd.File.Close()
|
||||
return nil
|
||||
|
||||
err := fd.startDownload()
|
||||
|
||||
if fd.File != nil {
|
||||
fd.File.Close()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
95
core/http.go
95
core/http.go
@@ -13,6 +13,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"res-downloader/core/shared"
|
||||
sysRuntime "runtime"
|
||||
"strings"
|
||||
)
|
||||
@@ -37,25 +38,31 @@ func initHttpServer() *HttpServer {
|
||||
func (h *HttpServer) run() {
|
||||
listener, err := net.Listen("tcp", globalConfig.Host+":"+globalConfig.Port)
|
||||
if err != nil {
|
||||
log.Fatalf("无法启动监听: %v", err)
|
||||
globalLogger.Err(err)
|
||||
log.Fatalf("Service cannot start: %v", err)
|
||||
}
|
||||
fmt.Println("服务已启动,监听 http://" + globalConfig.Host + ":" + globalConfig.Port)
|
||||
if err := http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Host == "127.0.0.1:"+globalConfig.Port && strings.Contains(r.URL.Path, "/cert") {
|
||||
w.Header().Set("Content-Type", "application/x-x509-ca-data")
|
||||
w.Header().Set("Content-Disposition", "attachment;filename=res-downloader-public.crt")
|
||||
w.Header().Set("Content-Transfer-Encoding", "binary")
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(appOnce.PublicCrt)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err = io.Copy(w, io.NopCloser(bytes.NewReader(appOnce.PublicCrt)))
|
||||
fmt.Println("Service started, listening http://" + globalConfig.Host + ":" + globalConfig.Port)
|
||||
if err1 := http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Host == "127.0.0.1:"+globalConfig.Port && HandleApi(w, r) {
|
||||
|
||||
} else {
|
||||
proxyOnce.Proxy.ServeHTTP(w, r) // 代理
|
||||
}
|
||||
})); err != nil {
|
||||
fmt.Printf("服务器异常: %v", err)
|
||||
})); err1 != nil {
|
||||
globalLogger.Err(err1)
|
||||
fmt.Printf("Service startup exception: %v", err1)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HttpServer) downCert(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/x-x509-ca-data")
|
||||
w.Header().Set("Content-Disposition", "attachment;filename=res-downloader-public.crt")
|
||||
w.Header().Set("Content-Transfer-Encoding", "binary")
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(appOnce.PublicCrt)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.Copy(w, io.NopCloser(bytes.NewReader(appOnce.PublicCrt)))
|
||||
}
|
||||
|
||||
func (h *HttpServer) preview(w http.ResponseWriter, r *http.Request) {
|
||||
realURL := r.URL.Query().Get("url")
|
||||
if realURL == "" {
|
||||
@@ -113,12 +120,12 @@ func (h *HttpServer) send(t string, data interface{}) {
|
||||
runtime.EventsEmit(appOnce.ctx, "event", string(jsonData))
|
||||
}
|
||||
|
||||
func (h *HttpServer) writeJson(w http.ResponseWriter, data ResponseData) {
|
||||
func (h *HttpServer) writeJson(w http.ResponseWriter, data *ResponseData) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
err := json.NewEncoder(w).Encode(data)
|
||||
if err != nil {
|
||||
globalLogger.err(err)
|
||||
globalLogger.Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,12 +139,7 @@ func (h *HttpServer) error(w http.ResponseWriter, args ...interface{}) {
|
||||
if len(args) > 1 {
|
||||
data = args[1]
|
||||
}
|
||||
|
||||
h.writeJson(w, ResponseData{
|
||||
Code: 0,
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
h.writeJson(w, h.buildResp(0, message, data))
|
||||
}
|
||||
|
||||
func (h *HttpServer) success(w http.ResponseWriter, args ...interface{}) {
|
||||
@@ -151,12 +153,15 @@ func (h *HttpServer) success(w http.ResponseWriter, args ...interface{}) {
|
||||
if len(args) > 1 {
|
||||
message = args[1].(string)
|
||||
}
|
||||
h.writeJson(w, h.buildResp(1, message, data))
|
||||
}
|
||||
|
||||
h.writeJson(w, ResponseData{
|
||||
Code: 1,
|
||||
func (h *HttpServer) buildResp(code int, message string, data interface{}) *ResponseData {
|
||||
return &ResponseData{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HttpServer) openDirectoryDialog(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -206,14 +211,10 @@ func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch sysRuntime.GOOS {
|
||||
case "darwin":
|
||||
// macOS
|
||||
cmd = exec.Command("open", "-R", filePath)
|
||||
case "windows":
|
||||
// Windows
|
||||
cmd = exec.Command("explorer", "/select,", filePath)
|
||||
case "linux":
|
||||
// linux
|
||||
// 尝试使用不同的文件管理器
|
||||
cmd = exec.Command("nautilus", filePath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
cmd = exec.Command("thunar", filePath)
|
||||
@@ -222,7 +223,7 @@ func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
|
||||
if err := cmd.Start(); err != nil {
|
||||
cmd = exec.Command("pcmanfm", filePath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
globalLogger.err(err)
|
||||
globalLogger.Err(err)
|
||||
h.error(w, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -236,23 +237,45 @@ func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
globalLogger.err(err)
|
||||
globalLogger.Err(err)
|
||||
h.error(w, err.Error())
|
||||
return
|
||||
}
|
||||
h.success(w)
|
||||
}
|
||||
|
||||
func (h *HttpServer) install(w http.ResponseWriter, r *http.Request) {
|
||||
if appOnce.isInstall() {
|
||||
h.success(w, respData{
|
||||
"isPass": systemOnce.Password == "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
out, err := appOnce.installCert()
|
||||
if err != nil {
|
||||
h.error(w, err.Error()+"\n"+out, respData{
|
||||
"isPass": systemOnce.Password == "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.success(w, respData{
|
||||
"isPass": systemOnce.Password == "",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *HttpServer) setSystemPassword(w http.ResponseWriter, r *http.Request) {
|
||||
var data struct {
|
||||
Password string `json:"password"`
|
||||
IsCache bool `json:"isCache"`
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(&data)
|
||||
if err != nil {
|
||||
h.error(w, err.Error())
|
||||
return
|
||||
}
|
||||
systemOnce.SetPassword(data.Password)
|
||||
systemOnce.SetPassword(data.Password, data.IsCache)
|
||||
h.success(w)
|
||||
}
|
||||
|
||||
@@ -260,12 +283,12 @@ func (h *HttpServer) openSystemProxy(w http.ResponseWriter, r *http.Request) {
|
||||
err := appOnce.OpenSystemProxy()
|
||||
if err != nil {
|
||||
h.error(w, err.Error(), respData{
|
||||
"isProxy": appOnce.IsProxy,
|
||||
"value": appOnce.IsProxy,
|
||||
})
|
||||
return
|
||||
}
|
||||
h.success(w, respData{
|
||||
"isProxy": appOnce.IsProxy,
|
||||
"value": appOnce.IsProxy,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -273,18 +296,18 @@ func (h *HttpServer) unsetSystemProxy(w http.ResponseWriter, r *http.Request) {
|
||||
err := appOnce.UnsetSystemProxy()
|
||||
if err != nil {
|
||||
h.error(w, err.Error(), respData{
|
||||
"isProxy": appOnce.IsProxy,
|
||||
"value": appOnce.IsProxy,
|
||||
})
|
||||
return
|
||||
}
|
||||
h.success(w, respData{
|
||||
"isProxy": appOnce.IsProxy,
|
||||
"value": appOnce.IsProxy,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *HttpServer) isProxy(w http.ResponseWriter, r *http.Request) {
|
||||
h.success(w, respData{
|
||||
"isProxy": appOnce.IsProxy,
|
||||
"value": appOnce.IsProxy,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -379,7 +402,7 @@ func (h *HttpServer) batchImport(w http.ResponseWriter, r *http.Request) {
|
||||
h.error(w, err.Error())
|
||||
return
|
||||
}
|
||||
fileName := filepath.Join(globalConfig.SaveDirectory, "res-downloader-"+GetCurrentDateTimeFormatted()+".txt")
|
||||
fileName := filepath.Join(globalConfig.SaveDirectory, "res-downloader-"+shared.GetCurrentDateTimeFormatted()+".txt")
|
||||
err := os.WriteFile(fileName, []byte(data.Content), 0644)
|
||||
if err != nil {
|
||||
h.error(w, err.Error())
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"res-downloader/core/shared"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
@@ -15,7 +16,7 @@ type Logger struct {
|
||||
|
||||
func initLogger() *Logger {
|
||||
if globalLogger == nil {
|
||||
globalLogger = NewLogger(!IsDevelopment(), filepath.Join(appOnce.UserDir, "logs", "app.log"))
|
||||
globalLogger = NewLogger(!shared.IsDevelopment(), filepath.Join(appOnce.UserDir, "logs", "app.log"))
|
||||
}
|
||||
return globalLogger
|
||||
}
|
||||
@@ -24,7 +25,7 @@ func (l *Logger) Close() {
|
||||
_ = l.logFile.Close()
|
||||
}
|
||||
|
||||
func (l *Logger) err(err error) {
|
||||
func (l *Logger) Err(err error) {
|
||||
l.Error().Stack().Err(err)
|
||||
}
|
||||
|
||||
@@ -38,7 +39,7 @@ func NewLogger(logFile bool, logPath string) *Logger {
|
||||
if logFile {
|
||||
// log to file
|
||||
logDir := filepath.Dir(logPath)
|
||||
if err := CreateDirIfNotExist(logDir); err != nil {
|
||||
if err := shared.CreateDirIfNotExist(logDir); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var (
|
||||
|
||||
@@ -24,10 +24,12 @@ func HandleApi(w http.ResponseWriter, r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/api/preview":
|
||||
httpServerOnce.preview(w, r)
|
||||
case "/api/install":
|
||||
httpServerOnce.install(w, r)
|
||||
case "/api/set-system-password":
|
||||
httpServerOnce.setSystemPassword(w, r)
|
||||
case "/api/preview":
|
||||
httpServerOnce.preview(w, r)
|
||||
case "/api/proxy-open":
|
||||
httpServerOnce.openSystemProxy(w, r)
|
||||
case "/api/proxy-unset":
|
||||
@@ -58,6 +60,8 @@ func HandleApi(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpServerOnce.wxFileDecode(w, r)
|
||||
case "/api/batch-import":
|
||||
httpServerOnce.batchImport(w, r)
|
||||
case "/api/cert":
|
||||
httpServerOnce.downCert(w, r)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
78
core/plugins/plugin.default.go
Normal file
78
core/plugins/plugin.default.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/elazarl/goproxy"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"net/http"
|
||||
"res-downloader/core/shared"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type DefaultPlugin struct {
|
||||
bridge *shared.Bridge
|
||||
}
|
||||
|
||||
func (p *DefaultPlugin) SetBridge(bridge *shared.Bridge) {
|
||||
p.bridge = bridge
|
||||
}
|
||||
|
||||
func (p *DefaultPlugin) Domains() []string {
|
||||
return []string{"default"}
|
||||
}
|
||||
|
||||
func (p *DefaultPlugin) OnRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (p *DefaultPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
|
||||
if resp == nil || resp.Request == nil || (resp.StatusCode != 200 && resp.StatusCode != 206) {
|
||||
return resp
|
||||
}
|
||||
|
||||
classify, suffix := p.bridge.TypeSuffix(resp.Header.Get("Content-Type"))
|
||||
if classify == "" {
|
||||
return resp
|
||||
}
|
||||
|
||||
rawUrl := resp.Request.URL.String()
|
||||
isAll, _ := p.bridge.GetResType("all")
|
||||
isClassify, _ := p.bridge.GetResType(classify)
|
||||
|
||||
urlSign := shared.Md5(rawUrl)
|
||||
if ok := p.bridge.MediaIsMarked(urlSign); !ok && (isAll || isClassify) {
|
||||
value, _ := strconv.ParseFloat(resp.Header.Get("content-length"), 64)
|
||||
id, err := gonanoid.New()
|
||||
if err != nil {
|
||||
id = urlSign
|
||||
}
|
||||
res := shared.MediaInfo{
|
||||
Id: id,
|
||||
Url: rawUrl,
|
||||
UrlSign: urlSign,
|
||||
CoverUrl: "",
|
||||
Size: shared.FormatSize(value),
|
||||
Domain: shared.GetTopLevelDomain(rawUrl),
|
||||
Classify: classify,
|
||||
Suffix: suffix,
|
||||
Status: shared.DownloadStatusReady,
|
||||
SavePath: "",
|
||||
DecodeKey: "",
|
||||
OtherData: map[string]string{},
|
||||
Description: "",
|
||||
ContentType: resp.Header.Get("Content-Type"),
|
||||
}
|
||||
|
||||
// Store entire request headers as JSON
|
||||
if headers, err := json.Marshal(resp.Request.Header); err == nil {
|
||||
res.OtherData["headers"] = string(headers)
|
||||
}
|
||||
|
||||
p.bridge.MarkMedia(urlSign)
|
||||
go func(res shared.MediaInfo) {
|
||||
p.bridge.Send("newResources", res)
|
||||
}(res)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
262
core/plugins/plugin.qq.com.go
Normal file
262
core/plugins/plugin.qq.com.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/elazarl/goproxy"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"res-downloader/core/shared"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type QqPlugin struct {
|
||||
bridge *shared.Bridge
|
||||
}
|
||||
|
||||
func (p *QqPlugin) SetBridge(bridge *shared.Bridge) {
|
||||
p.bridge = bridge
|
||||
}
|
||||
|
||||
func (p *QqPlugin) Domains() []string {
|
||||
return []string{"qq.com"}
|
||||
}
|
||||
|
||||
func (p *QqPlugin) OnRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||
if strings.Contains(r.Host, "qq.com") && strings.Contains(r.URL.Path, "/res-downloader/wechat") {
|
||||
if p.bridge.GetConfig("WxAction").(bool) && r.URL.Query().Get("type") == "1" {
|
||||
return p.handleWechatRequest(r, ctx)
|
||||
} else if !p.bridge.GetConfig("WxAction").(bool) && r.URL.Query().Get("type") == "2" {
|
||||
return p.handleWechatRequest(r, ctx)
|
||||
} else {
|
||||
return r, p.buildEmptyResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *QqPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
|
||||
if resp.StatusCode != 200 && resp.StatusCode != 206 {
|
||||
return nil
|
||||
}
|
||||
|
||||
host := resp.Request.Host
|
||||
Path := resp.Request.URL.Path
|
||||
|
||||
classify, _ := p.bridge.TypeSuffix(resp.Header.Get("Content-Type"))
|
||||
if classify == "video" && strings.HasSuffix(host, "finder.video.qq.com") {
|
||||
return resp
|
||||
}
|
||||
|
||||
if strings.HasSuffix(host, "channels.weixin.qq.com") &&
|
||||
(strings.Contains(Path, "/web/pages/feed") || strings.Contains(Path, "/web/pages/home")) {
|
||||
return p.replaceWxJsContent(resp, ".js\"", ".js?v="+p.v()+"\"")
|
||||
}
|
||||
|
||||
if strings.HasSuffix(host, "res.wx.qq.com") {
|
||||
respTemp := resp
|
||||
is := false
|
||||
if strings.HasSuffix(respTemp.Request.URL.RequestURI(), ".js?v="+p.v()) {
|
||||
respTemp = p.replaceWxJsContent(respTemp, ".js\"", ".js?v="+p.v()+"\"")
|
||||
is = true
|
||||
}
|
||||
|
||||
if strings.Contains(Path, "web/web-finder/res/js/virtual_svg-icons-register.publish") {
|
||||
body, err := io.ReadAll(respTemp.Body)
|
||||
if err != nil {
|
||||
return respTemp
|
||||
}
|
||||
bodyStr := string(body)
|
||||
newBody := regexp.MustCompile(`get\s*media\(\)\{`).
|
||||
ReplaceAllString(bodyStr, `
|
||||
get media(){
|
||||
if(this.objectDesc){
|
||||
fetch("https://wxapp.tc.qq.com/res-downloader/wechat?type=1", {
|
||||
method: "POST",
|
||||
mode: "no-cors",
|
||||
body: JSON.stringify(this.objectDesc),
|
||||
});
|
||||
};
|
||||
|
||||
`)
|
||||
|
||||
newBody = regexp.MustCompile(`async\s*finderGetCommentDetail\((\w+)\)\s*\{return(.*?)\s*}\s*async`).
|
||||
ReplaceAllString(newBody, `
|
||||
async finderGetCommentDetail($1) {
|
||||
var res = await$2;
|
||||
if (res?.data?.object?.objectDesc) {
|
||||
fetch("https://wxapp.tc.qq.com/res-downloader/wechat?type=2", {
|
||||
method: "POST",
|
||||
mode: "no-cors",
|
||||
body: JSON.stringify(res.data.object.objectDesc),
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}async
|
||||
`)
|
||||
newBodyBytes := []byte(newBody)
|
||||
respTemp.Body = io.NopCloser(bytes.NewBuffer(newBodyBytes))
|
||||
respTemp.ContentLength = int64(len(newBodyBytes))
|
||||
respTemp.Header.Set("Content-Length", fmt.Sprintf("%d", len(newBodyBytes)))
|
||||
return respTemp
|
||||
}
|
||||
if is {
|
||||
return respTemp
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *QqPlugin) handleWechatRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return r, p.buildEmptyResponse(r)
|
||||
}
|
||||
|
||||
isAll, _ := p.bridge.GetResType("all")
|
||||
isClassify, _ := p.bridge.GetResType("video")
|
||||
|
||||
if !isAll && !isClassify {
|
||||
return r, p.buildEmptyResponse(r)
|
||||
}
|
||||
|
||||
go p.handleMedia(body)
|
||||
|
||||
return r, p.buildEmptyResponse(r)
|
||||
}
|
||||
|
||||
func (p *QqPlugin) handleMedia(body []byte) {
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mediaArr, ok := result["media"].([]interface{})
|
||||
if !ok || len(mediaArr) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
firstMedia, ok := mediaArr[0].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
rawUrl, ok := firstMedia["url"].(string)
|
||||
if !ok || rawUrl == "" {
|
||||
return
|
||||
}
|
||||
|
||||
urlSign := shared.Md5(rawUrl)
|
||||
if p.bridge.MediaIsMarked(urlSign) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := gonanoid.New()
|
||||
if err != nil {
|
||||
id = urlSign
|
||||
}
|
||||
|
||||
res := shared.MediaInfo{
|
||||
Id: id,
|
||||
Url: rawUrl,
|
||||
UrlSign: urlSign,
|
||||
CoverUrl: "",
|
||||
Size: "0",
|
||||
Domain: shared.GetTopLevelDomain(rawUrl),
|
||||
Classify: "video",
|
||||
Suffix: ".mp4",
|
||||
Status: shared.DownloadStatusReady,
|
||||
SavePath: "",
|
||||
DecodeKey: "",
|
||||
OtherData: map[string]string{},
|
||||
Description: "",
|
||||
ContentType: "video/mp4",
|
||||
}
|
||||
|
||||
if mediaType, ok := firstMedia["mediaType"].(float64); ok && mediaType == 9 {
|
||||
res.Classify = "image"
|
||||
res.Suffix = ".png"
|
||||
res.ContentType = "image/png"
|
||||
}
|
||||
|
||||
if urlToken, ok := firstMedia["urlToken"].(string); ok {
|
||||
res.Url += urlToken
|
||||
}
|
||||
|
||||
switch size := firstMedia["fileSize"].(type) {
|
||||
case float64:
|
||||
res.Size = shared.FormatSize(size)
|
||||
case string:
|
||||
if value, err := strconv.ParseFloat(size, 64); err == nil {
|
||||
res.Size = shared.FormatSize(value)
|
||||
}
|
||||
}
|
||||
|
||||
if coverUrl, ok := firstMedia["coverUrl"].(string); ok {
|
||||
res.CoverUrl = coverUrl
|
||||
}
|
||||
|
||||
if decodeKey, ok := firstMedia["decodeKey"].(string); ok {
|
||||
res.DecodeKey = decodeKey
|
||||
}
|
||||
|
||||
if desc, ok := result["description"].(string); ok {
|
||||
res.Description = desc
|
||||
}
|
||||
|
||||
if spec, ok := firstMedia["spec"].([]interface{}); ok {
|
||||
var fileFormats []string
|
||||
for _, item := range spec {
|
||||
if m, ok := item.(map[string]interface{}); ok {
|
||||
if format, ok := m["fileFormat"].(string); ok {
|
||||
fileFormats = append(fileFormats, format)
|
||||
}
|
||||
}
|
||||
}
|
||||
res.OtherData["wx_file_formats"] = strings.Join(fileFormats, "#")
|
||||
}
|
||||
|
||||
p.bridge.MarkMedia(urlSign)
|
||||
|
||||
go func(res shared.MediaInfo) {
|
||||
p.bridge.Send("newResources", res)
|
||||
}(res)
|
||||
}
|
||||
|
||||
func (p *QqPlugin) buildEmptyResponse(r *http.Request) *http.Response {
|
||||
body := "The content does not exist"
|
||||
resp := &http.Response{
|
||||
Status: http.StatusText(http.StatusOK),
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
ContentLength: int64(len(body)),
|
||||
Request: r,
|
||||
}
|
||||
resp.Header.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *QqPlugin) replaceWxJsContent(resp *http.Response, old, new string) *http.Response {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return resp
|
||||
}
|
||||
bodyString := string(body)
|
||||
newBodyString := strings.ReplaceAll(bodyString, old, new)
|
||||
newBodyBytes := []byte(newBodyString)
|
||||
resp.Body = io.NopCloser(bytes.NewBuffer(newBodyBytes))
|
||||
resp.ContentLength = int64(len(newBodyBytes))
|
||||
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(newBodyBytes)))
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *QqPlugin) v() string {
|
||||
return p.bridge.GetVersion()
|
||||
}
|
||||
329
core/proxy.go
329
core/proxy.go
@@ -1,22 +1,18 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/elazarl/goproxy"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"res-downloader/core/plugins"
|
||||
"res-downloader/core/shared"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/elazarl/goproxy"
|
||||
)
|
||||
|
||||
type Proxy struct {
|
||||
@@ -42,6 +38,46 @@ type MediaInfo struct {
|
||||
OtherData map[string]string
|
||||
}
|
||||
|
||||
var pluginRegistry = make(map[string]shared.Plugin)
|
||||
|
||||
func init() {
|
||||
ps := []shared.Plugin{
|
||||
&plugins.QqPlugin{},
|
||||
&plugins.DefaultPlugin{},
|
||||
}
|
||||
|
||||
bridge := &shared.Bridge{
|
||||
GetVersion: func() string {
|
||||
return appOnce.Version
|
||||
},
|
||||
GetResType: func(key string) (bool, bool) {
|
||||
return resourceOnce.getResType(key)
|
||||
},
|
||||
TypeSuffix: func(mine string) (string, string) {
|
||||
return globalConfig.typeSuffix(mine)
|
||||
},
|
||||
MediaIsMarked: func(key string) bool {
|
||||
return resourceOnce.mediaIsMarked(key)
|
||||
},
|
||||
MarkMedia: func(key string) {
|
||||
resourceOnce.markMedia(key)
|
||||
},
|
||||
GetConfig: func(key string) interface{} {
|
||||
return globalConfig.getConfig(key)
|
||||
},
|
||||
Send: func(t string, data interface{}) {
|
||||
httpServerOnce.send(t, data)
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range ps {
|
||||
p.SetBridge(bridge)
|
||||
for _, domain := range p.Domains() {
|
||||
pluginRegistry[domain] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initProxy() *Proxy {
|
||||
if proxyOnce == nil {
|
||||
proxyOnce = &Proxy{}
|
||||
@@ -53,7 +89,7 @@ func initProxy() *Proxy {
|
||||
func (p *Proxy) Startup() {
|
||||
err := p.setCa()
|
||||
if err != nil {
|
||||
DialogErr("启动代理服务失败:" + err.Error())
|
||||
DialogErr("Failed to start proxy service:" + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -69,7 +105,7 @@ func (p *Proxy) Startup() {
|
||||
func (p *Proxy) setCa() error {
|
||||
ca, err := tls.X509KeyPair(appOnce.PublicCrt, appOnce.PrivateKey)
|
||||
if err != nil {
|
||||
DialogErr("启动代理服务失败1")
|
||||
DialogErr("Failed to start proxy service 1")
|
||||
return err
|
||||
}
|
||||
if ca.Leaf, err = x509.ParseCertificate(ca.Certificate[0]); err != nil {
|
||||
@@ -104,264 +140,41 @@ func (p *Proxy) setTransport() {
|
||||
p.Proxy.Tr = transport
|
||||
}
|
||||
|
||||
func (p *Proxy) matchPlugin(host string) shared.Plugin {
|
||||
domain := shared.GetTopLevelDomain(host)
|
||||
if plugin, ok := pluginRegistry[domain]; ok {
|
||||
return plugin
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Proxy) httpRequestEvent(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||
if strings.Contains(r.Host, "res-downloader.666666.com") && strings.Contains(r.URL.Path, "/wechat") {
|
||||
if globalConfig.WxAction && r.URL.Query().Get("type") == "1" {
|
||||
return p.handleWechatRequest(r, ctx)
|
||||
} else if !globalConfig.WxAction && r.URL.Query().Get("type") == "2" {
|
||||
return p.handleWechatRequest(r, ctx)
|
||||
} else {
|
||||
return r, p.buildEmptyResponse(r)
|
||||
plugin := p.matchPlugin(r.Host)
|
||||
if plugin != nil {
|
||||
newReq, newResp := plugin.OnRequest(r, ctx)
|
||||
if newResp != nil {
|
||||
return newReq, newResp
|
||||
}
|
||||
|
||||
if newReq != nil {
|
||||
return newReq, nil
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (p *Proxy) handleWechatRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return r, p.buildEmptyResponse(r)
|
||||
}
|
||||
|
||||
isAll, _ := resourceOnce.getResType("all")
|
||||
isClassify, _ := resourceOnce.getResType("video")
|
||||
|
||||
if !isAll && !isClassify {
|
||||
return r, p.buildEmptyResponse(r)
|
||||
}
|
||||
go func(body []byte) {
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
media, ok := result["media"].([]interface{})
|
||||
if !ok || len(media) <= 0 {
|
||||
return
|
||||
}
|
||||
firstMedia, ok := media[0].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rowUrl, ok := firstMedia["url"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
urlSign := Md5(rowUrl.(string))
|
||||
if resourceOnce.mediaIsMarked(urlSign) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := gonanoid.New()
|
||||
if err != nil {
|
||||
id = urlSign
|
||||
}
|
||||
res := MediaInfo{
|
||||
Id: id,
|
||||
Url: rowUrl.(string),
|
||||
UrlSign: urlSign,
|
||||
CoverUrl: "",
|
||||
Size: "0",
|
||||
Domain: GetTopLevelDomain(rowUrl.(string)),
|
||||
Classify: "video",
|
||||
Suffix: ".mp4",
|
||||
Status: DownloadStatusReady,
|
||||
SavePath: "",
|
||||
DecodeKey: "",
|
||||
OtherData: map[string]string{},
|
||||
Description: "",
|
||||
ContentType: "video/mp4",
|
||||
}
|
||||
|
||||
if mediaType, ok := firstMedia["mediaType"].(float64); ok && mediaType == 9 {
|
||||
res.Classify = "image"
|
||||
res.Suffix = ".png"
|
||||
res.ContentType = "image/png"
|
||||
}
|
||||
|
||||
if urlToken, ok := firstMedia["urlToken"].(string); ok {
|
||||
res.Url = res.Url + urlToken
|
||||
}
|
||||
if fileSize, ok := firstMedia["fileSize"].(float64); ok {
|
||||
res.Size = FormatSize(fileSize)
|
||||
}
|
||||
if coverUrl, ok := firstMedia["coverUrl"].(string); ok {
|
||||
res.CoverUrl = coverUrl
|
||||
}
|
||||
if fileSize, ok := firstMedia["fileSize"].(string); ok {
|
||||
value, err := strconv.ParseFloat(fileSize, 64)
|
||||
if err == nil {
|
||||
res.Size = FormatSize(value)
|
||||
}
|
||||
}
|
||||
if decodeKey, ok := firstMedia["decodeKey"].(string); ok {
|
||||
res.DecodeKey = decodeKey
|
||||
}
|
||||
if desc, ok := result["description"].(string); ok {
|
||||
res.Description = desc
|
||||
}
|
||||
if spec, ok := firstMedia["spec"].([]interface{}); ok {
|
||||
var fileFormats []string
|
||||
for _, item := range spec {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
if format, exists := itemMap["fileFormat"].(string); exists {
|
||||
fileFormats = append(fileFormats, format)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.OtherData["wx_file_formats"] = strings.Join(fileFormats, "#")
|
||||
}
|
||||
resourceOnce.markMedia(urlSign)
|
||||
httpServerOnce.send("newResources", res)
|
||||
}(body)
|
||||
return r, p.buildEmptyResponse(r)
|
||||
}
|
||||
|
||||
func (p *Proxy) buildEmptyResponse(r *http.Request) *http.Response {
|
||||
body := "内容不存在"
|
||||
resp := &http.Response{
|
||||
Status: "200 OK",
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
ContentLength: int64(len(body)),
|
||||
Request: r,
|
||||
}
|
||||
resp.Header.Set("Content-Type", "text/plain")
|
||||
return resp
|
||||
return pluginRegistry["default"].OnRequest(r, ctx)
|
||||
}
|
||||
|
||||
func (p *Proxy) httpResponseEvent(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 {
|
||||
return resp
|
||||
}
|
||||
|
||||
host := resp.Request.Host
|
||||
Path := resp.Request.URL.Path
|
||||
|
||||
if strings.HasSuffix(host, "channels.weixin.qq.com") &&
|
||||
(strings.Contains(Path, "/web/pages/feed") || strings.Contains(Path, "/web/pages/home")) {
|
||||
return p.replaceWxJsContent(resp, ".js\"", ".js?v="+p.v()+"\"")
|
||||
}
|
||||
|
||||
if strings.HasSuffix(host, "res.wx.qq.com") {
|
||||
respTemp := resp
|
||||
is := false
|
||||
if strings.HasSuffix(respTemp.Request.URL.RequestURI(), ".js?v="+p.v()) {
|
||||
respTemp = p.replaceWxJsContent(respTemp, ".js\"", ".js?v="+p.v()+"\"")
|
||||
is = true
|
||||
}
|
||||
|
||||
if strings.Contains(Path, "web/web-finder/res/js/virtual_svg-icons-register.publish") {
|
||||
body, err := io.ReadAll(respTemp.Body)
|
||||
if err != nil {
|
||||
return respTemp
|
||||
}
|
||||
bodyStr := string(body)
|
||||
newBody := regexp.MustCompile(`get\s*media\(\)\{`).
|
||||
ReplaceAllString(bodyStr, `
|
||||
get media(){
|
||||
if(this.objectDesc){
|
||||
fetch("https://res-downloader.666666.com/wechat?type=1", {
|
||||
method: "POST",
|
||||
mode: "no-cors",
|
||||
body: JSON.stringify(this.objectDesc),
|
||||
});
|
||||
};
|
||||
|
||||
`)
|
||||
|
||||
newBody = regexp.MustCompile(`async\s*finderGetCommentDetail\((\w+)\)\s*\{return(.*?)\s*}\s*async`).
|
||||
ReplaceAllString(newBody, `
|
||||
async finderGetCommentDetail($1) {
|
||||
var res = await$2;
|
||||
if (res?.data?.object?.objectDesc) {
|
||||
fetch("https://res-downloader.666666.com/wechat?type=2", {
|
||||
method: "POST",
|
||||
mode: "no-cors",
|
||||
body: JSON.stringify(res.data.object.objectDesc),
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}async
|
||||
`)
|
||||
newBodyBytes := []byte(newBody)
|
||||
respTemp.Body = io.NopCloser(bytes.NewBuffer(newBodyBytes))
|
||||
respTemp.ContentLength = int64(len(newBodyBytes))
|
||||
respTemp.Header.Set("Content-Length", fmt.Sprintf("%d", len(newBodyBytes)))
|
||||
return respTemp
|
||||
}
|
||||
if is {
|
||||
return respTemp
|
||||
plugin := p.matchPlugin(resp.Request.Host)
|
||||
if plugin != nil {
|
||||
newResp := plugin.OnResponse(resp, ctx)
|
||||
if newResp != nil {
|
||||
return newResp
|
||||
}
|
||||
}
|
||||
|
||||
classify, suffix := TypeSuffix(resp.Header.Get("Content-Type"))
|
||||
if classify == "" {
|
||||
return resp
|
||||
}
|
||||
|
||||
if classify == "video" && strings.HasSuffix(host, "finder.video.qq.com") {
|
||||
//if !globalConfig.WxAction && classify == "video" && strings.HasSuffix(host, "finder.video.qq.com") {
|
||||
return resp
|
||||
}
|
||||
|
||||
rawUrl := resp.Request.URL.String()
|
||||
isAll, _ := resourceOnce.getResType("all")
|
||||
isClassify, _ := resourceOnce.getResType(classify)
|
||||
|
||||
urlSign := Md5(rawUrl)
|
||||
if ok := resourceOnce.mediaIsMarked(urlSign); !ok && (isAll || isClassify) {
|
||||
value, _ := strconv.ParseFloat(resp.Header.Get("content-length"), 64)
|
||||
id, err := gonanoid.New()
|
||||
if err != nil {
|
||||
id = urlSign
|
||||
}
|
||||
res := MediaInfo{
|
||||
Id: id,
|
||||
Url: rawUrl,
|
||||
UrlSign: urlSign,
|
||||
CoverUrl: "",
|
||||
Size: FormatSize(value),
|
||||
Domain: GetTopLevelDomain(rawUrl),
|
||||
Classify: classify,
|
||||
Suffix: suffix,
|
||||
Status: DownloadStatusReady,
|
||||
SavePath: "",
|
||||
DecodeKey: "",
|
||||
OtherData: map[string]string{},
|
||||
Description: "",
|
||||
ContentType: resp.Header.Get("Content-Type"),
|
||||
}
|
||||
|
||||
// Store entire request headers as JSON
|
||||
if headers, err := json.Marshal(resp.Request.Header); err == nil {
|
||||
res.OtherData["headers"] = string(headers)
|
||||
}
|
||||
|
||||
resourceOnce.markMedia(urlSign)
|
||||
httpServerOnce.send("newResources", res)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *Proxy) replaceWxJsContent(resp *http.Response, old, new string) *http.Response {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return resp
|
||||
}
|
||||
bodyString := string(body)
|
||||
newBodyString := strings.ReplaceAll(bodyString, old, new)
|
||||
newBodyBytes := []byte(newBodyString)
|
||||
resp.Body = io.NopCloser(bytes.NewBuffer(newBodyBytes))
|
||||
resp.ContentLength = int64(len(newBodyBytes))
|
||||
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(newBodyBytes)))
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *Proxy) v() string {
|
||||
return appOnce.Version
|
||||
return pluginRegistry["default"].OnResponse(resp, ctx)
|
||||
}
|
||||
|
||||
@@ -9,19 +9,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"res-downloader/core/shared"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
DownloadStatusReady string = "ready" // task create but not start
|
||||
DownloadStatusRunning string = "running"
|
||||
DownloadStatusError string = "error"
|
||||
DownloadStatusDone string = "done"
|
||||
DownloadStatusHandle string = "handle"
|
||||
)
|
||||
|
||||
type WxFileDecodeResult struct {
|
||||
SavePath string
|
||||
Message string
|
||||
@@ -102,7 +95,7 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
|
||||
}
|
||||
go func(mediaInfo MediaInfo) {
|
||||
rawUrl := mediaInfo.Url
|
||||
fileName := Md5(rawUrl)
|
||||
fileName := shared.Md5(rawUrl)
|
||||
if mediaInfo.Description != "" {
|
||||
fileName = regexp.MustCompile(`[^\w\p{Han}]`).ReplaceAllString(mediaInfo.Description, "")
|
||||
fileLen := globalConfig.FilenameLen
|
||||
@@ -117,7 +110,7 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
|
||||
}
|
||||
|
||||
if globalConfig.FilenameTime {
|
||||
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+"_"+GetCurrentDateTimeFormatted()+mediaInfo.Suffix)
|
||||
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+"_"+shared.GetCurrentDateTimeFormatted()+mediaInfo.Suffix)
|
||||
} else {
|
||||
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+mediaInfo.Suffix)
|
||||
}
|
||||
@@ -147,8 +140,8 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
|
||||
headers, _ := r.parseHeaders(mediaInfo)
|
||||
|
||||
downloader := NewFileDownloader(rawUrl, mediaInfo.SavePath, globalConfig.TaskNumber, headers)
|
||||
downloader.progressCallback = func(totalDownloaded, totalSize float64) {
|
||||
r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded*100/totalSize))+"%", DownloadStatusRunning)
|
||||
downloader.progressCallback = func(totalDownloaded, totalSize float64, taskID int, taskProgress float64) {
|
||||
r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded*100/totalSize))+"%", shared.DownloadStatusRunning)
|
||||
}
|
||||
err := downloader.Start()
|
||||
if err != nil {
|
||||
@@ -156,23 +149,21 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
|
||||
return
|
||||
}
|
||||
if decodeStr != "" {
|
||||
r.progressEventsEmit(mediaInfo, "解密中", DownloadStatusRunning)
|
||||
r.progressEventsEmit(mediaInfo, "decrypting in progress", shared.DownloadStatusRunning)
|
||||
if err := r.decodeWxFile(mediaInfo.SavePath, decodeStr); err != nil {
|
||||
r.progressEventsEmit(mediaInfo, "解密出错"+err.Error())
|
||||
r.progressEventsEmit(mediaInfo, "decryption error: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
r.progressEventsEmit(mediaInfo, "完成", DownloadStatusDone)
|
||||
r.progressEventsEmit(mediaInfo, "complete", shared.DownloadStatusDone)
|
||||
}(mediaInfo)
|
||||
}
|
||||
|
||||
// 解析并组装 headers
|
||||
func (r *Resource) parseHeaders(mediaInfo MediaInfo) (map[string]string, error) {
|
||||
headers := make(map[string]string)
|
||||
|
||||
if hh, ok := mediaInfo.OtherData["headers"]; ok {
|
||||
var tempHeaders map[string][]string
|
||||
// 解析 JSON 字符串为 map[string][]string
|
||||
if err := json.Unmarshal([]byte(hh), &tempHeaders); err != nil {
|
||||
return headers, fmt.Errorf("parse headers JSON err: %v", err)
|
||||
}
|
||||
@@ -193,7 +184,7 @@ func (r *Resource) wxFileDecode(mediaInfo MediaInfo, fileName, decodeStr string)
|
||||
return "", err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
mediaInfo.SavePath = strings.ReplaceAll(fileName, ".mp4", "_解密.mp4")
|
||||
mediaInfo.SavePath = strings.ReplaceAll(fileName, ".mp4", "_decrypt.mp4")
|
||||
|
||||
destinationFile, err := os.Create(mediaInfo.SavePath)
|
||||
if err != nil {
|
||||
@@ -213,7 +204,7 @@ func (r *Resource) wxFileDecode(mediaInfo MediaInfo, fileName, decodeStr string)
|
||||
}
|
||||
|
||||
func (r *Resource) progressEventsEmit(mediaInfo MediaInfo, args ...string) {
|
||||
Status := DownloadStatusError
|
||||
Status := shared.DownloadStatusError
|
||||
Message := "ok"
|
||||
|
||||
if len(args) > 0 {
|
||||
|
||||
18
core/shared/base.go
Normal file
18
core/shared/base.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package shared
|
||||
|
||||
type MediaInfo struct {
|
||||
Id string
|
||||
Url string
|
||||
UrlSign string
|
||||
CoverUrl string
|
||||
Size string
|
||||
Domain string
|
||||
Classify string
|
||||
Suffix string
|
||||
SavePath string
|
||||
Status string
|
||||
DecodeKey string
|
||||
Description string
|
||||
ContentType string
|
||||
OtherData map[string]string
|
||||
}
|
||||
9
core/shared/const.go
Normal file
9
core/shared/const.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package shared
|
||||
|
||||
const (
|
||||
DownloadStatusReady string = "ready" // task create but not start
|
||||
DownloadStatusRunning string = "running"
|
||||
DownloadStatusError string = "error"
|
||||
DownloadStatusDone string = "done"
|
||||
DownloadStatusHandle string = "handle"
|
||||
)
|
||||
23
core/shared/plugin.go
Normal file
23
core/shared/plugin.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"github.com/elazarl/goproxy"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Bridge struct {
|
||||
GetVersion func() string
|
||||
GetResType func(key string) (bool, bool)
|
||||
TypeSuffix func(mime string) (string, string)
|
||||
MediaIsMarked func(key string) bool
|
||||
MarkMedia func(key string)
|
||||
GetConfig func(key string) interface{}
|
||||
Send func(t string, data interface{})
|
||||
}
|
||||
|
||||
type Plugin interface {
|
||||
SetBridge(*Bridge)
|
||||
Domains() []string
|
||||
OnRequest(*http.Request, *goproxy.ProxyCtx) (*http.Request, *http.Response)
|
||||
OnResponse(*http.Response, *goproxy.ProxyCtx) *http.Response
|
||||
}
|
||||
70
core/shared/utils.go
Normal file
70
core/shared/utils.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Md5(data string) string {
|
||||
hashNew := md5.New()
|
||||
hashNew.Write([]byte(data))
|
||||
hash := hashNew.Sum(nil)
|
||||
return hex.EncodeToString(hash)
|
||||
}
|
||||
|
||||
func FormatSize(size float64) string {
|
||||
if size > 1048576 {
|
||||
return fmt.Sprintf("%.2fMB", float64(size)/1048576)
|
||||
}
|
||||
if size > 1024 {
|
||||
return fmt.Sprintf("%.2fKB", float64(size)/1024)
|
||||
}
|
||||
return fmt.Sprintf("%.0fb", size)
|
||||
}
|
||||
|
||||
func GetTopLevelDomain(rawURL string) string {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err == nil && u.Host != "" {
|
||||
rawURL = u.Host
|
||||
}
|
||||
domain, err := publicsuffix.EffectiveTLDPlusOne(rawURL)
|
||||
if err != nil {
|
||||
return rawURL
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
func FileExist(file string) bool {
|
||||
info, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
func CreateDirIfNotExist(dir string) error {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
return os.MkdirAll(dir, 0750)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsDevelopment() bool {
|
||||
return os.Getenv("APP_ENV") == "development"
|
||||
}
|
||||
|
||||
func GetCurrentDateTimeFormatted() string {
|
||||
now := time.Now()
|
||||
return fmt.Sprintf("%04d%02d%02d%02d%02d%02d",
|
||||
now.Year(),
|
||||
now.Month(),
|
||||
now.Day(),
|
||||
now.Hour(),
|
||||
now.Minute(),
|
||||
now.Second())
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"res-downloader/core/shared"
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
@@ -18,7 +19,7 @@ func NewStorage(filename string, def []byte) *Storage {
|
||||
}
|
||||
|
||||
func (l *Storage) Load() ([]byte, error) {
|
||||
if !FileExist(l.fileName) {
|
||||
if !shared.FileExist(l.fileName) {
|
||||
err := os.WriteFile(l.fileName, l.def, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SystemSetup struct {
|
||||
CertFile string
|
||||
Password string
|
||||
CertFile string
|
||||
CacheFile string
|
||||
Password string
|
||||
aesCipher *AESCipher
|
||||
}
|
||||
|
||||
func initSystem() *SystemSetup {
|
||||
if systemOnce == nil {
|
||||
systemOnce = &SystemSetup{
|
||||
CertFile: filepath.Join(appOnce.UserDir, "cert.crt"),
|
||||
aesCipher: NewAESCipher("resd48w2d7er95627d447c490a8f02ff"),
|
||||
CertFile: filepath.Join(appOnce.UserDir, "cert.crt"),
|
||||
CacheFile: filepath.Join(appOnce.UserDir, "pass.cache"),
|
||||
}
|
||||
systemOnce.checkPasswordFile()
|
||||
}
|
||||
return systemOnce
|
||||
}
|
||||
@@ -35,6 +42,42 @@ func (s *SystemSetup) initCert() ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SystemSetup) SetPassword(password string) {
|
||||
func (s *SystemSetup) SetPassword(password string, isCache bool) {
|
||||
s.Password = password
|
||||
if isCache {
|
||||
encrypted, err := s.aesCipher.Encrypt(password)
|
||||
if err == nil {
|
||||
err1 := os.WriteFile(s.CacheFile, []byte(encrypted), 0750)
|
||||
if err1 != nil {
|
||||
fmt.Println("Failed to write password: ", err1.Error())
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Failed to Encrypt password: ", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SystemSetup) checkPasswordFile() {
|
||||
fileInfo, err := os.Stat(s.CacheFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lastModified := fileInfo.ModTime()
|
||||
oneMonthAgo := time.Now().AddDate(0, -1, 0)
|
||||
if lastModified.Before(oneMonthAgo) {
|
||||
os.Remove(s.CacheFile)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(s.CacheFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
password, err := s.aesCipher.Decrypt(string(content))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.Password = password
|
||||
}
|
||||
|
||||
@@ -64,28 +64,27 @@ func (s *SystemSetup) setProxy() error {
|
||||
return err
|
||||
}
|
||||
|
||||
is := false
|
||||
errs := ""
|
||||
isSuccess := false
|
||||
var errs strings.Builder
|
||||
for _, serviceName := range services {
|
||||
cmds := [][]string{
|
||||
commands := [][]string{
|
||||
{"networksetup", "-setwebproxy", serviceName, "127.0.0.1", globalConfig.Port},
|
||||
{"networksetup", "-setsecurewebproxy", serviceName, "127.0.0.1", globalConfig.Port},
|
||||
}
|
||||
for _, args := range cmds {
|
||||
if output, err := s.runCommand(args); err != nil {
|
||||
errs = errs + "\n output:" + string(output) + "err:" + err.Error()
|
||||
fmt.Println("setProxy:", output, " err:", err.Error())
|
||||
for _, cmd := range commands {
|
||||
if output, err := s.runCommand(cmd); err != nil {
|
||||
errs.WriteString(fmt.Sprintf("cmd: %v\noutput: %s\nerr: %s\n", cmd, output, err))
|
||||
} else {
|
||||
is = true
|
||||
isSuccess = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if is {
|
||||
if isSuccess {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to set proxy for any active network service, errs: %s", errs)
|
||||
return fmt.Errorf("failed to set proxy for any active network service, errs:%s", errs)
|
||||
}
|
||||
|
||||
func (s *SystemSetup) unsetProxy() error {
|
||||
@@ -94,28 +93,27 @@ func (s *SystemSetup) unsetProxy() error {
|
||||
return err
|
||||
}
|
||||
|
||||
is := false
|
||||
errs := ""
|
||||
isSuccess := false
|
||||
var errs strings.Builder
|
||||
for _, serviceName := range services {
|
||||
cmds := [][]string{
|
||||
commands := [][]string{
|
||||
{"networksetup", "-setwebproxystate", serviceName, "off"},
|
||||
{"networksetup", "-setsecurewebproxystate", serviceName, "off"},
|
||||
}
|
||||
for _, args := range cmds {
|
||||
if output, err := s.runCommand(args); err != nil {
|
||||
errs = errs + "\n output:" + string(output) + "err:" + err.Error()
|
||||
fmt.Println("unsetProxy:", output, " err:", err.Error())
|
||||
for _, cmd := range commands {
|
||||
if output, err := s.runCommand(cmd); err != nil {
|
||||
errs.WriteString(fmt.Sprintf("cmd: %v\noutput: %s\nerr: %s\n", cmd, output, err))
|
||||
} else {
|
||||
is = true
|
||||
isSuccess = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if is {
|
||||
if isSuccess {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to unset proxy for any active network service, errs: %s", errs)
|
||||
return fmt.Errorf("failed to unset proxy for any active network service, errs:%s", errs)
|
||||
}
|
||||
|
||||
func (s *SystemSetup) installCert() (string, error) {
|
||||
@@ -123,19 +121,7 @@ func (s *SystemSetup) installCert() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
getPasswordCmd := exec.Command("osascript", "-e", `tell app "System Events" to display dialog "请输入你的电脑密码,用于安装证书文件:" default answer "" with hidden answer`, "-e", `text returned of result`)
|
||||
passwordOutput, err := getPasswordCmd.Output()
|
||||
if err != nil {
|
||||
return string(passwordOutput), err
|
||||
}
|
||||
|
||||
password := strings.TrimSpace(string(passwordOutput))
|
||||
s.SetPassword(password)
|
||||
|
||||
cmd := exec.Command("sudo", "-S", "security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", s.CertFile)
|
||||
cmd.Stdin = bytes.NewReader([]byte(password + "\n"))
|
||||
output, err := cmd.CombinedOutput()
|
||||
output, err := s.runCommand([]string{"security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", s.CertFile})
|
||||
if err != nil {
|
||||
return string(output), err
|
||||
}
|
||||
|
||||
@@ -3,10 +3,43 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *SystemSetup) getLinuxDistro() (string, error) {
|
||||
data, err := os.ReadFile("/etc/os-release")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if strings.HasPrefix(line, "ID=") {
|
||||
return strings.Trim(strings.TrimPrefix(line, "ID="), "\""), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("could not determine linux distribution")
|
||||
}
|
||||
|
||||
func (s *SystemSetup) runCommand(args []string, sudo bool) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, fmt.Errorf("no command provided")
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if s.Password != "" && sudo {
|
||||
cmd = exec.Command("sudo", append([]string{"-S"}, args...)...)
|
||||
cmd.Stdin = bytes.NewReader([]byte(s.Password + "\n"))
|
||||
} else {
|
||||
cmd = exec.Command(args[0], args[1:]...)
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (s *SystemSetup) setProxy() error {
|
||||
commands := [][]string{
|
||||
{"gsettings", "set", "org.gnome.system.proxy", "mode", "manual"},
|
||||
@@ -15,23 +48,32 @@ func (s *SystemSetup) setProxy() error {
|
||||
{"gsettings", "set", "org.gnome.system.proxy.https", "host", "127.0.0.1"},
|
||||
{"gsettings", "set", "org.gnome.system.proxy.https", "port", globalConfig.Port},
|
||||
}
|
||||
is := false
|
||||
|
||||
isSuccess := false
|
||||
var errs strings.Builder
|
||||
|
||||
for _, cmd := range commands {
|
||||
if err := exec.Command(cmd[0], cmd[1:]...).Run(); err != nil {
|
||||
fmt.Println(err)
|
||||
if output, err := s.runCommand(cmd, false); err != nil {
|
||||
errs.WriteString(fmt.Sprintf("cmd: %v\noutput: %s\nerr: %s\n", cmd, output, err))
|
||||
} else {
|
||||
is = true
|
||||
isSuccess = true
|
||||
}
|
||||
}
|
||||
if is {
|
||||
|
||||
if isSuccess {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Failed to activate proxy")
|
||||
|
||||
return fmt.Errorf("failed to set proxy:\n%s", errs.String())
|
||||
}
|
||||
|
||||
func (s *SystemSetup) unsetProxy() error {
|
||||
cmd := []string{"gsettings", "set", "org.gnome.system.proxy", "mode", "none"}
|
||||
return exec.Command(cmd[0], cmd[1:]...).Run()
|
||||
output, err := s.runCommand(cmd, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unset proxy: %s\noutput: %s", err.Error(), string(output))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SystemSetup) installCert() (string, error) {
|
||||
@@ -40,35 +82,56 @@ func (s *SystemSetup) installCert() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
actions := [][]string{
|
||||
{"/usr/local/share/ca-certificates/", "update-ca-certificates"},
|
||||
{"/usr/share/ca-certificates/trust-source/anchors/", "update-ca-trust"},
|
||||
{"/usr/share/ca-certificates/trust-source/anchors/", "trust extract-compat"},
|
||||
{"/etc/pki/ca-trust/source/anchors/", "update-ca-trust"},
|
||||
{"/etc/ssl/ca-certificates/", "update-ca-certificates"},
|
||||
distro, err := s.getLinuxDistro()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("detect distro failed: %w", err)
|
||||
}
|
||||
|
||||
is := false
|
||||
certName := appOnce.AppName + ".crt"
|
||||
|
||||
for _, action := range actions {
|
||||
dir := action[0]
|
||||
if err := exec.Command("sudo", "cp", "-f", s.CertFile, dir+appOnce.AppName+".crt").Run(); err != nil {
|
||||
fmt.Printf("Failed to copy to %s: %v\n", dir, err)
|
||||
continue
|
||||
var certPath string
|
||||
if distro == "deepin" {
|
||||
certDir := "/usr/share/ca-certificates/" + appOnce.AppName
|
||||
certPath = certDir + "/" + certName
|
||||
s.runCommand([]string{"mkdir", "-p", certDir}, true)
|
||||
} else {
|
||||
certPath = "/usr/local/share/ca-certificates/" + certName
|
||||
}
|
||||
|
||||
var outs, errs strings.Builder
|
||||
isSuccess := false
|
||||
|
||||
if output, err := s.runCommand([]string{"cp", "-f", s.CertFile, certPath}, true); err != nil {
|
||||
errs.WriteString(fmt.Sprintf("copy cert failed: %s\n%s\n", err.Error(), output))
|
||||
} else {
|
||||
isSuccess = true
|
||||
outs.Write(output)
|
||||
}
|
||||
|
||||
if distro == "deepin" {
|
||||
confPath := "/etc/ca-certificates.conf"
|
||||
checkCmd := []string{"grep", "-qxF", certName, confPath}
|
||||
if _, err := s.runCommand(checkCmd, true); err != nil {
|
||||
echoCmd := []string{"bash", "-c", fmt.Sprintf("echo '%s' >> %s", certName, confPath)}
|
||||
if output, err := s.runCommand(echoCmd, true); err != nil {
|
||||
errs.WriteString(fmt.Sprintf("append conf failed: %s\n%s\n", err.Error(), output))
|
||||
} else {
|
||||
isSuccess = true
|
||||
outs.Write(output)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := action[1]
|
||||
if err := exec.Command("sudo", cmd).Run(); err != nil {
|
||||
fmt.Printf("Failed to refresh certificates using %s: %v\n", cmd, err)
|
||||
continue
|
||||
}
|
||||
|
||||
is = true
|
||||
}
|
||||
|
||||
if !is {
|
||||
return "", fmt.Errorf("Certificate installation failed")
|
||||
if output, err := s.runCommand([]string{"update-ca-certificates"}, true); err != nil {
|
||||
errs.WriteString(fmt.Sprintf("update failed: %s\n%s\n", err.Error(), output))
|
||||
} else {
|
||||
isSuccess = true
|
||||
outs.Write(output)
|
||||
}
|
||||
|
||||
return "", nil
|
||||
if isSuccess {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return outs.String(), fmt.Errorf("certificate installation failed:\n%s", errs.String())
|
||||
}
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Empty(data interface{}) {
|
||||
}
|
||||
|
||||
func DialogErr(message string) {
|
||||
_, _ = runtime.MessageDialog(appOnce.ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.ErrorDialog,
|
||||
@@ -22,84 +12,3 @@ func DialogErr(message string) {
|
||||
DefaultButton: "Cancel",
|
||||
})
|
||||
}
|
||||
|
||||
func IsDevelopment() bool {
|
||||
return os.Getenv("APP_ENV") == "development"
|
||||
}
|
||||
|
||||
func FileExist(file string) bool {
|
||||
_, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func CreateDirIfNotExist(dir string) error {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
return os.MkdirAll(dir, 0750)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TypeSuffix(mime string) (string, string) {
|
||||
mimeMux.RLock()
|
||||
defer mimeMux.RUnlock()
|
||||
mime = strings.ToLower(strings.Split(mime, ";")[0])
|
||||
if v, ok := globalConfig.MimeMap[mime]; ok {
|
||||
return v.Type, v.Suffix
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func BuildReferer(rawURL string) string {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return u.Scheme + "://" + u.Host + "/"
|
||||
}
|
||||
|
||||
func Md5(data string) string {
|
||||
hashNew := md5.New()
|
||||
hashNew.Write([]byte(data))
|
||||
hash := hashNew.Sum(nil)
|
||||
return hex.EncodeToString(hash)
|
||||
}
|
||||
|
||||
func FormatSize(size float64) string {
|
||||
if size > 1048576 {
|
||||
return fmt.Sprintf("%.2fMB", float64(size)/1048576)
|
||||
}
|
||||
if size > 1024 {
|
||||
return fmt.Sprintf("%.2fKB", float64(size)/1024)
|
||||
}
|
||||
return fmt.Sprintf("%.0fb", size)
|
||||
}
|
||||
|
||||
func GetTopLevelDomain(rawURL string) string {
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
host := parsedURL.Hostname()
|
||||
parts := strings.Split(host, ".")
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(parts[len(parts)-2:], ".")
|
||||
}
|
||||
|
||||
func GetCurrentDateTimeFormatted() string {
|
||||
now := time.Now()
|
||||
return fmt.Sprintf("%04d%02d%02d%02d%02d%02d",
|
||||
now.Year(),
|
||||
now.Month(),
|
||||
now.Day(),
|
||||
now.Hour(),
|
||||
now.Minute(),
|
||||
now.Second())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# res-downloader V3
|
||||
<div align="center">
|
||||
<a href="https://github.com/putyy/res-downloader"><img src="images/logo.png" width="120"/></a>
|
||||
<h1><strong>res-downloader</strong></h1>
|
||||
</div>
|
||||
|
||||
> 全新技术栈,更新、更小、更快、更稳
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
* [论坛](https://s.gowas.cn/d/4089)
|
||||
* [反馈](https://github.com/putyy/res-downloader/issues)
|
||||
* [日志](https://github.com/putyy/res-downloader/releases)
|
||||
* [QQ群](https://qm.qq.com/q/ImE37ayJmc)
|
||||
* [WX群](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)
|
||||
@@ -3,7 +3,7 @@
|
||||

|
||||
|
||||
## 拦截资源
|
||||
### 视频号
|
||||
### 视频号
|
||||
- 打开视频号即可看到本软件中拦截到的资源
|
||||

|
||||
|
||||
|
||||
BIN
docs/images/logo.png
Executable file
BIN
docs/images/logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
@@ -27,15 +27,13 @@
|
||||
subMaxLevel: 4,
|
||||
// 小屏设备下合并导航栏到侧边栏
|
||||
mergeNavbar: true,
|
||||
basePath: '/',
|
||||
homepage: 'readme.md',
|
||||
search: {
|
||||
maxAge: 86400000,// 过期时间,单位毫秒,默认一天
|
||||
paths: 'auto',// 注意:仅适用于 paths: 'auto' 模式
|
||||
placeholder: '搜索',
|
||||
// 支持本地化
|
||||
placeholder: {
|
||||
'/zh-cn/': '搜索',
|
||||
'/': 'Type to search'
|
||||
},
|
||||
placeholder: '搜索',
|
||||
noData: '找不到结果',
|
||||
depth: 4,
|
||||
hideOtherSidebarContent: false,
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
- 双击下载好的dmg文件,将res-downloader拖入应用即可,如图:
|
||||

|
||||
|
||||
## Linux安装过程
|
||||
- 执行文件运行方式举例
|
||||
## Linux安装过程(自行替换掉对应的安装文件目录)
|
||||
- ubuntu安装deb文件
|
||||
> sudo apt install res-downloader_3.0.2_linux_x64.deb
|
||||
|
||||
- 执行文件运行方式
|
||||
> chmod +x ./res-downloader_3.0.2_linux_x64
|
||||
> sudo ./res-downloader_3.0.2_linux_x64
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
## 隐藏功能
|
||||
- 导出\导入 数据,1秒内连续点击空白处5次即可开启(单次有效)
|
||||

|
||||
|
||||
## 清空列表、类型筛选
|
||||
- 当资源列表过大时,无法快速找到需要的资源,这时可以先清空列表再去刷新需要的资源页面
|
||||
- 资源列表过多,可以快速根据需要的资源类型进行筛选
|
||||
|
||||
@@ -1,15 +1,63 @@
|
||||
# res-downloader
|
||||
## 爱享素材下载器
|
||||
<div align="center">
|
||||
|
||||
🎯 基于Go + [wails](https://github.com/wailsapp/wails)
|
||||
📦 操作简单、可获取不同类型的网络资源
|
||||
🖥️ 支持Windows、Mac、Linux
|
||||
🌐 支持视频、音频、图片、m3u8、直播流等常见网络资源
|
||||
💪 支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、qq音乐等网络资源下载
|
||||
👼 支持设置代理以获取特殊网络下的资源
|
||||
<a href="https://github.com/putyy/res-downloader"><img src="images/logo.png" width="120"/></a>
|
||||
<h1>res-downloader</h1>
|
||||
<h4>📖 中文 | <a href="https://github.com/putyy/res-downloader/blob/master/README-EN.md">English</a></h4>
|
||||
|
||||
## 实现 & 初衷
|
||||
?> 通过设置系统网络代理拦截响应,筛选出需要的资源, 同fiddler、charles等抓包软件、浏览器F12打开控制也能达到目的,只不过这些软件需要手动进行筛选,对于小白用户上手还是有点难度,本软件对部分资源做了特殊处理,更适合大众用户,所以就有了本项目。
|
||||
[](https://github.com/putyy/res-downloader/stargazers)
|
||||
[](https://github.com/putyy/res-downloader/fork)
|
||||
[](https://github.com/putyy/res-downloader/releases)
|
||||

|
||||
[](https://github.com/putyy/res-downloader/blob/master/LICENSE)
|
||||
|
||||
## 免责声明
|
||||
?> 本软件用于学习研究使用,若因使用本软件造成的一切法律责任均与本人无关!
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
### 🎉 爱享素材下载器
|
||||
|
||||
> 一款基于 Go + [Wails](https://github.com/wailsapp/wails) 的跨平台资源下载工具,简洁易用,支持多种资源嗅探与下载。
|
||||
|
||||
## ✨ 功能特色
|
||||
|
||||
- 🚀 **简单易用**:操作简单,界面清晰美观
|
||||
- 🖥️ **多平台支持**:Windows / macOS / Linux
|
||||
- 🌐 **多资源类型支持**:视频 / 音频 / 图片 / m3u8 / 直播流等
|
||||
- 📱 **平台兼容广泛**:支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、QQ音乐等
|
||||
- 🌍 **代理抓包**:支持设置代理获取受限网络下的资源
|
||||
|
||||
## 📚 文档 & 版本
|
||||
|
||||
- 📘 [在线文档](https://res.putyy.com/)
|
||||
- 💬 [加入交流群](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)
|
||||
- 🧩 [最新版](https://github.com/putyy/res-downloader/releases) | [Mini版 使用默认浏览器展示UI](https://github.com/putyy/resd-mini) | [Electron旧版 支持Win7](https://github.com/putyy/res-downloader/tree/old)
|
||||
> *群满时可加微信 `AmorousWorld`,请备注“来源”*
|
||||
|
||||
## 🧩 下载地址
|
||||
|
||||
- 🆕 [GitHub 下载](https://github.com/putyy/res-downloader/releases)
|
||||
- 🆕 [蓝奏云下载(密码:9vs5)](https://wwjv.lanzoum.com/b04wgtfyb)
|
||||
- ⚠️ *Win7 用户请下载 `2.3.0` 版本*
|
||||
|
||||
|
||||
## 🖼️ 预览
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 🧠 更多问题
|
||||
|
||||
- [GitHub Issues](https://github.com/putyy/res-downloader/issues)
|
||||
- [爱享论坛讨论帖](https://s.gowas.cn/d/4089)
|
||||
|
||||
## 💡 实现原理 & 初衷
|
||||
|
||||
本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 免责声明
|
||||
|
||||
> 本软件仅供学习与研究用途,禁止用于任何商业或违法用途。
|
||||
如因此产生的任何法律责任,概与作者无关!
|
||||
|
||||
@@ -1,3 +1,34 @@
|
||||
## 视频号拦截了一大堆 找不到想要的
|
||||
> 设置里面关闭全量拦截,将视频转发好友后打开
|
||||
|
||||
## 某某网址拦截不了?
|
||||
> 本软件实现原理 & 初衷如下,并非万能的,所以有一些应用拦截不了很正常
|
||||
```
|
||||
本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。
|
||||
```
|
||||
|
||||
## 软件打不开了?之前可以打开
|
||||
> 删除对应目录, 然后重启
|
||||
```
|
||||
## Mac执行
|
||||
rm -rf /Users/$(whoami)/Library/Preferences/res-downloader
|
||||
|
||||
## Windows手动删除以下目录,Administrator为用户名 通常如下:
|
||||
C:\Users\Administrator\AppData\Roaming\res-downloader
|
||||
|
||||
## Linux手动删除以下目录
|
||||
/home/user/.config/res-downloader/home/user/.config/res-downloader
|
||||
```
|
||||
|
||||
## 某应用只支持手机打开 如何拦截?
|
||||
> 这里需要注意的是 应用使用http协议通讯才能拦截,且安卓7.0以上系统不再信任用户CA证书 所以没法拦截,解决方案自行查找,
|
||||
```
|
||||
1. 将手机和电脑处于同一个网络
|
||||
2. 在手机端安装res-downloader的证书
|
||||
3. 将手机网络代理设置为res-downloader的代理
|
||||
4. 正常使用
|
||||
```
|
||||
|
||||
## Mac 提示“已损坏,无法打开”, 打开命令行执行如下命令:
|
||||
> sudo xattr -d com.apple.quarantine /Applications/res-downloader.app
|
||||
|
||||
@@ -10,7 +41,7 @@
|
||||
> 手动关闭系统代理设置
|
||||
|
||||
## 链接不是私密链接
|
||||
> 通常是证书未正确安装,最新版证书下载:软件左下角?点击后有下载地址
|
||||
> 通常是证书未正确安装,最新版证书下载:软件左下角 ?点击后有下载地址
|
||||
> 根据自己系统进行安装证书操作(不懂的自行百度),手动安装需安装到受信任的根证书
|
||||
|
||||
- Mac手动安装证书(V3+版本支持),打开终端复制以下命令 粘贴到终端回车 按照提示输入密码,完成后再打开软件:
|
||||
|
||||
2
frontend/components.d.ts
vendored
2
frontend/components.d.ts
vendored
@@ -31,8 +31,6 @@ declare module 'vue' {
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
|
||||
576
frontend/package-lock.json
generated
576
frontend/package-lock.json
generated
@@ -13,7 +13,9 @@
|
||||
"flv.js": "^1.6.2",
|
||||
"naive-ui": "^2.38.2",
|
||||
"pinia": "^2.1.7",
|
||||
"video.js": "^8.22.0",
|
||||
"vue": "^3.2.37",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-router": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -23,14 +25,12 @@
|
||||
"@vitejs/plugin-vue": "^3.0.3",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"pug": "^3.0.3",
|
||||
"sass": "^1.77.6",
|
||||
"sass-loader": "^14.2.1",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^4.6.4",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"video.js": "^8.19.1",
|
||||
"vite": "^3.0.7",
|
||||
"vue-tsc": "^1.8.27"
|
||||
}
|
||||
@@ -95,7 +95,6 @@
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.26.0.tgz",
|
||||
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
@@ -198,6 +197,50 @@
|
||||
"vue": ">=3"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.3.tgz",
|
||||
"integrity": "sha512-cMuHunYO7LE80azTitcvEbs1KJmtd6g7I5pxlApV3Jo547zdO3h31/0uXpqHc+Y3RKt1wo2y68RGSx77Z1klyA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "11.1.3",
|
||||
"@intlify/shared": "11.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.3.tgz",
|
||||
"integrity": "sha512-7rbqqpo2f5+tIcwZTAG/Ooy9C8NDVwfDkvSeDPWUPQW+Dyzfw2o9H103N5lKBxO7wxX9dgCDjQ8Umz73uYw3hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.1.3",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.3.tgz",
|
||||
"integrity": "sha512-pTFBgqa/99JRA2H1qfyqv97MKWJrYngXBA/I0elZcYxvJgcCw3mApAoPW3mJ7vx3j+Ti0FyKUFZ4hWxdjKaxvA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -714,10 +757,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@videojs/http-streaming": {
|
||||
"version": "3.16.2",
|
||||
"resolved": "https://registry.npmmirror.com/@videojs/http-streaming/-/http-streaming-3.16.2.tgz",
|
||||
"integrity": "sha512-fvt4ko7FknxiT9FnjyNQt6q2px+awrkM+Orv7IB/4gldvj94u4fowGfmNHynnvNTPgPkdxHklGmFLGfclYw8HA==",
|
||||
"dev": true,
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.17.0.tgz",
|
||||
"integrity": "sha512-Ch1P3tvvIEezeZXyK11UfWgp4cWKX4vIhZ30baN/lRinqdbakZ5hiAI3pGjRy3d+q/Epyc8Csz5xMdKNNGYpcw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -739,9 +781,8 @@
|
||||
},
|
||||
"node_modules/@videojs/vhs-utils": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz",
|
||||
"integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -756,7 +797,6 @@
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/@videojs/xhr/-/xhr-2.7.0.tgz",
|
||||
"integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
@@ -943,32 +983,17 @@
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-7.4.1.tgz",
|
||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aes-decrypter": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/aes-decrypter/-/aes-decrypter-4.0.2.tgz",
|
||||
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz",
|
||||
"integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -1031,20 +1056,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/assert-never": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/assert-never/-/assert-never-1.4.0.tgz",
|
||||
"integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
|
||||
@@ -1106,19 +1117,6 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-walk": {
|
||||
"version": "3.0.0-canary-5",
|
||||
"resolved": "https://registry.npmmirror.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
|
||||
"integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.9.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -1195,37 +1193,6 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
|
||||
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.3.tgz",
|
||||
"integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"get-intrinsic": "^1.2.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase-css": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||
@@ -1257,16 +1224,6 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/character-parser": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/character-parser/-/character-parser-2.2.0.tgz",
|
||||
"integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-regex": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
|
||||
@@ -1339,17 +1296,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/constantinople": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/constantinople/-/constantinople-4.0.1.tgz",
|
||||
"integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.6.0",
|
||||
"@babel/types": "^7.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -1481,33 +1427,10 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/doctypes": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/doctypes/-/doctypes-1.1.0.tgz",
|
||||
"integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-walk": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
@@ -1542,39 +1465,6 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
|
||||
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmmirror.com/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
@@ -2147,31 +2037,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
|
||||
"integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"dunder-proto": "^1.0.0",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.0.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz",
|
||||
@@ -2210,55 +2075,12 @@
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/global/-/global-4.4.0.tgz",
|
||||
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"min-document": "^2.19.0",
|
||||
"process": "^0.11.10"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -2327,17 +2149,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-expression": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-expression/-/is-expression-4.0.0.tgz",
|
||||
"integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^7.1.1",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -2362,7 +2173,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/is-function/-/is-function-1.0.2.tgz",
|
||||
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
@@ -2388,32 +2198,6 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-2.2.2.tgz",
|
||||
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz",
|
||||
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"gopd": "^1.2.0",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -2447,13 +2231,6 @@
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/js-stringify": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/js-stringify/-/js-stringify-1.0.2.tgz",
|
||||
"integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||
@@ -2461,17 +2238,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jstransformer": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/jstransformer/-/jstransformer-1.0.0.tgz",
|
||||
"integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-promise": "^2.0.0",
|
||||
"promise": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
@@ -2530,9 +2296,8 @@
|
||||
},
|
||||
"node_modules/m3u8-parser": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/m3u8-parser/-/m3u8-parser-7.2.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz",
|
||||
"integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -2549,16 +2314,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -2608,7 +2363,6 @@
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmmirror.com/min-document/-/min-document-2.19.0.tgz",
|
||||
"integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dom-walk": "^0.1.0"
|
||||
}
|
||||
@@ -2667,9 +2421,8 @@
|
||||
},
|
||||
"node_modules/mpd-parser": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/mpd-parser/-/mpd-parser-1.3.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz",
|
||||
"integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -2697,9 +2450,8 @@
|
||||
},
|
||||
"node_modules/mux.js": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/mux.js/-/mux.js-7.1.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz",
|
||||
"integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
@@ -2953,9 +2705,8 @@
|
||||
},
|
||||
"node_modules/pkcs7": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/pkcs7/-/pkcs7-1.0.4.tgz",
|
||||
"resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
|
||||
"integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5"
|
||||
@@ -3129,164 +2880,17 @@
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/promise": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/promise/-/promise-7.3.1.tgz",
|
||||
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asap": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pug": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/pug/-/pug-3.0.3.tgz",
|
||||
"integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pug-code-gen": "^3.0.3",
|
||||
"pug-filters": "^4.0.0",
|
||||
"pug-lexer": "^5.0.1",
|
||||
"pug-linker": "^4.0.0",
|
||||
"pug-load": "^3.0.0",
|
||||
"pug-parser": "^6.0.0",
|
||||
"pug-runtime": "^3.0.1",
|
||||
"pug-strip-comments": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pug-attrs": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pug-attrs/-/pug-attrs-3.0.0.tgz",
|
||||
"integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"constantinople": "^4.0.1",
|
||||
"js-stringify": "^1.0.2",
|
||||
"pug-runtime": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pug-code-gen": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/pug-code-gen/-/pug-code-gen-3.0.3.tgz",
|
||||
"integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"constantinople": "^4.0.1",
|
||||
"doctypes": "^1.1.0",
|
||||
"js-stringify": "^1.0.2",
|
||||
"pug-attrs": "^3.0.0",
|
||||
"pug-error": "^2.1.0",
|
||||
"pug-runtime": "^3.0.1",
|
||||
"void-elements": "^3.1.0",
|
||||
"with": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pug-error": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/pug-error/-/pug-error-2.1.0.tgz",
|
||||
"integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pug-filters": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pug-filters/-/pug-filters-4.0.0.tgz",
|
||||
"integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"constantinople": "^4.0.1",
|
||||
"jstransformer": "1.0.0",
|
||||
"pug-error": "^2.0.0",
|
||||
"pug-walk": "^2.0.0",
|
||||
"resolve": "^1.15.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pug-lexer": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/pug-lexer/-/pug-lexer-5.0.1.tgz",
|
||||
"integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"character-parser": "^2.2.0",
|
||||
"is-expression": "^4.0.0",
|
||||
"pug-error": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pug-linker": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pug-linker/-/pug-linker-4.0.0.tgz",
|
||||
"integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pug-error": "^2.0.0",
|
||||
"pug-walk": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pug-load": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pug-load/-/pug-load-3.0.0.tgz",
|
||||
"integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4.1.1",
|
||||
"pug-walk": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pug-parser": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pug-parser/-/pug-parser-6.0.0.tgz",
|
||||
"integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pug-error": "^2.0.0",
|
||||
"token-stream": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pug-runtime": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/pug-runtime/-/pug-runtime-3.0.1.tgz",
|
||||
"integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pug-strip-comments": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz",
|
||||
"integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pug-error": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pug-walk": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pug-walk/-/pug-walk-2.0.0.tgz",
|
||||
"integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -3336,7 +2940,6 @@
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
@@ -3822,13 +3425,6 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/token-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/token-stream/-/token-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/treemate": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz",
|
||||
@@ -4131,14 +3727,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/video.js": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmmirror.com/video.js/-/video.js-8.21.0.tgz",
|
||||
"integrity": "sha512-zcwerRb257QAuWfi8NH9yEX7vrGKFthjfcONmOQ4lxFRpDAbAi+u5LAjCjMWqhJda6zEmxkgdDpOMW3Y21QpXA==",
|
||||
"dev": true,
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.22.0.tgz",
|
||||
"integrity": "sha512-xge2kpjsvC0zgFJ1cqt+wTqsi21+huFswlonPFh7qiplypsb4FN/D2Rz6bWdG/S9eQaPHfWHsarmJL/7D3DHoA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/http-streaming": "^3.16.2",
|
||||
"@videojs/http-streaming": "^3.17.0",
|
||||
"@videojs/vhs-utils": "^4.1.1",
|
||||
"@videojs/xhr": "2.7.0",
|
||||
"aes-decrypter": "^4.0.2",
|
||||
@@ -4155,7 +3750,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz",
|
||||
"integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"global": "^4.4.0"
|
||||
@@ -4172,14 +3766,12 @@
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/videojs-font/-/videojs-font-4.2.0.tgz",
|
||||
"integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/videojs-vtt.js": {
|
||||
"version": "0.15.5",
|
||||
"resolved": "https://registry.npmmirror.com/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
|
||||
"integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"global": "^4.3.1"
|
||||
@@ -4235,16 +3827,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vooks": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz",
|
||||
@@ -4304,6 +3886,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.3.tgz",
|
||||
"integrity": "sha512-Pcylh9z9S5+CJAqgbRZ3EKxFIBIrtY5YUppU722GIT65+Nukm0TCqiQegZnNLCZkXGthxe0cpqj0AoM51H+6Gw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.1.3",
|
||||
"@intlify/shared": "11.1.3",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz",
|
||||
@@ -4395,22 +3997,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/with": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/with/-/with-7.0.2.tgz",
|
||||
"integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.9.6",
|
||||
"@babel/types": "^7.9.6",
|
||||
"assert-never": "^1.2.1",
|
||||
"babel-walk": "3.0.0-canary-5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
"flv.js": "^1.6.2",
|
||||
"naive-ui": "^2.38.2",
|
||||
"pinia": "^2.1.7",
|
||||
"video.js": "^8.22.0",
|
||||
"vue": "^3.2.37",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-router": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -24,14 +26,12 @@
|
||||
"@vitejs/plugin-vue": "^3.0.3",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"pug": "^3.0.3",
|
||||
"sass": "^1.77.6",
|
||||
"sass-loader": "^14.2.1",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^4.6.4",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"video.js": "^8.19.1",
|
||||
"vite": "^3.0.7",
|
||||
"vue-tsc": "^1.8.27"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
4b201370b63f9befd97a0e875507a444
|
||||
853e4a476a4f41b58875469cfb541133
|
||||
@@ -1,23 +1,32 @@
|
||||
<template>
|
||||
<NConfigProvider class="h-full" :theme="theme" :locale="zhCN">
|
||||
<NConfigProvider class="h-full" :theme="theme" :locale="uiLocale">
|
||||
<NaiveProvider>
|
||||
<RouterView />
|
||||
<RouterView/>
|
||||
<ShowLoading :isLoading="loading"/>
|
||||
<Password v-model:showModal="showPassword" @submit="handlePassword"/>
|
||||
</NaiveProvider>
|
||||
<NGlobalStyle />
|
||||
<NModalProvider />
|
||||
<NGlobalStyle/>
|
||||
<NModalProvider/>
|
||||
</NConfigProvider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import NaiveProvider from '@/components/NaiveProvider.vue'
|
||||
import {darkTheme, lightTheme, zhCN} from 'naive-ui'
|
||||
import {darkTheme, lightTheme, zhCN, enUS} from 'naive-ui'
|
||||
import {useIndexStore} from "@/stores"
|
||||
import {computed, onMounted} from "vue"
|
||||
import {computed, onMounted, ref} from "vue"
|
||||
import {useEventStore} from "@/stores/event"
|
||||
import {appType} from "@/types/app";
|
||||
import type {appType} from "@/types/app"
|
||||
import appApi from "@/api/app"
|
||||
import ShowLoading from "@/components/ShowLoading.vue"
|
||||
import Password from "@/components/Password.vue"
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const store = useIndexStore()
|
||||
const eventStore = useEventStore()
|
||||
const loading = ref(false)
|
||||
const showPassword = ref(false)
|
||||
const {t, locale} = useI18n()
|
||||
|
||||
const theme = computed(() => {
|
||||
if (store.globalConfig.Theme === "darkTheme") {
|
||||
@@ -28,31 +37,65 @@ const theme = computed(() => {
|
||||
return lightTheme
|
||||
})
|
||||
|
||||
const uiLocale = computed(() => {
|
||||
locale.value = store.globalConfig.Locale
|
||||
if (store.globalConfig.Locale === "zh") {
|
||||
return zhCN
|
||||
}
|
||||
return enUS
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await store.init()
|
||||
loading.value = true
|
||||
handleInstall().then((is: boolean)=>{
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
|
||||
eventStore.init()
|
||||
eventStore.addHandle({
|
||||
type: "message",
|
||||
event: (res: appType.Message)=>{
|
||||
event: (res: appType.Message) => {
|
||||
switch (res?.code) {
|
||||
case 0:
|
||||
window?.$message?.error(res.message)
|
||||
window.$message?.error(res.message)
|
||||
break
|
||||
case 1:
|
||||
window?.$message?.success(res.message)
|
||||
window.$message?.success(res.message)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
eventStore.addHandle({
|
||||
type: "updateProxyStatus",
|
||||
event: (res: any)=>{
|
||||
store.updateProxyStatus(res)
|
||||
const handleInstall = async () => {
|
||||
const res = await appApi.install()
|
||||
if (res.code === 1) {
|
||||
store.globalConfig.AutoProxy && store.openProxy()
|
||||
return true
|
||||
}
|
||||
|
||||
window.$message?.error(res.message, {duration: 5000})
|
||||
|
||||
if (store.envInfo.platform === 'windows' && res.message.includes('Access is denied')) {
|
||||
window.$message?.error('首次启用本软件,请使用鼠标右键选择以管理员身份运行')
|
||||
} else if (['darwin', 'linux'].includes(store.envInfo.platform)) {
|
||||
showPassword.value = true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const handlePassword = async (password: string, isCache: boolean) => {
|
||||
const res = await appApi.setSystemPassword({password, isCache})
|
||||
if (res.code === 0) {
|
||||
window.$message?.error(res.message)
|
||||
return
|
||||
}
|
||||
handleInstall().then((is: boolean)=>{
|
||||
if (is) {
|
||||
showPassword.value = false
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
}
|
||||
</script>
|
||||
@@ -1,6 +1,12 @@
|
||||
import request from '@/api/request'
|
||||
|
||||
export default {
|
||||
install() {
|
||||
return request({
|
||||
url: '/api/install',
|
||||
method: 'post'
|
||||
})
|
||||
},
|
||||
setSystemPassword(data: object) {
|
||||
return request({
|
||||
url: 'api/set-system-password',
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
import axios from 'axios';
|
||||
import type {AxiosResponse, InternalAxiosRequestConfig} from 'axios';
|
||||
import type {AxiosResponse, InternalAxiosRequestConfig} from 'axios'
|
||||
import axios from 'axios'
|
||||
import {useIndexStore} from "@/stores";
|
||||
import {computed} from "vue";
|
||||
|
||||
interface RequestOptions {
|
||||
url: string;
|
||||
method: 'get' | 'post' | 'put' | 'delete'; // 根据需要扩展
|
||||
params?: Record<string, any>;
|
||||
data?: Record<string, any>;
|
||||
url: string
|
||||
method: 'get' | 'post' | 'put' | 'delete'
|
||||
params?: Record<string, any>
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: "/",
|
||||
});
|
||||
})
|
||||
|
||||
instance.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig<any>) => {
|
||||
return config;
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
return Promise.reject(error)
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
return response.data;
|
||||
return response.data
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const request = ({url, method, params, data}: RequestOptions): Promise<any> => {
|
||||
return instance({url, method, params, data});
|
||||
};
|
||||
return instance({url, method, params, data, baseURL: window.$baseUrl})
|
||||
}
|
||||
|
||||
export default request;
|
||||
export default request
|
||||
@@ -5,20 +5,20 @@
|
||||
style="--wails-draggable:no-drag"
|
||||
preset="card"
|
||||
class="w-[640px]"
|
||||
title="关于"
|
||||
:title="t('footer.title')"
|
||||
>
|
||||
<div class="rounded p-5">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row">
|
||||
<div>
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="flex flex-row items-end">
|
||||
<div class="text-4xl font-bold">{{ store.appInfo.AppName }}</div>
|
||||
<div class="text-xs pl-5 text-slate-400">
|
||||
Version {{ store.appInfo.Version }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-slate-400 w-80 pt-2 pb-4">
|
||||
{{ store.appInfo.Description }}
|
||||
{{ t('footer.description') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-8">
|
||||
@@ -28,29 +28,20 @@
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-2xl font-bold text-emerald-600">
|
||||
支持市面上几乎所有的网络应用
|
||||
{{ t('footer.support') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-5 gap-2 text-sm m-4 text-slate-400">
|
||||
<span>抖音</span>
|
||||
<span>快手</span>
|
||||
<span>小红书</span>
|
||||
<span>视频号</span>
|
||||
<span>小程序</span>
|
||||
<span>公众号</span>
|
||||
<span>酷狗音乐</span>
|
||||
<span>QQ音乐</span>
|
||||
<span>QQ微视</span>
|
||||
<span>......</span>
|
||||
<span v-for="item in t('footer.application').split(',')">{{ item }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full text-sm justify-between pt-8 text-slate-400">
|
||||
<div>{{ store.appInfo.Copyright }}</div>
|
||||
<div class="flex">
|
||||
<button class="pl-4" @click="toWebsite('https://s.gowas.cn/d/4089')">论坛</button>
|
||||
<button class="pl-4" @click="toWebsite('http://127.0.0.1:8899/cert')">证书</button>
|
||||
<button class="pl-4" @click="toWebsite('https://github.com/putyy/res-downloader')">软件源码</button>
|
||||
<button class="pl-4" @click="toWebsite('https://github.com/putyy/res-downloader/issues')">帮助支持</button>
|
||||
<button class="pl-4" @click="toWebsite('https://github.com/putyy/res-downloader/releases')">更新日志</button>
|
||||
<button class="pl-4" @click="toWebsite('https://s.gowas.cn/d/4089')">{{ t('footer.forum') }}</button>
|
||||
<button class="pl-4" @click="toWebsite(certUrl)">{{ t('footer.cert') }}</button>
|
||||
<button class="pl-4" @click="toWebsite('https://github.com/putyy/res-downloader')">{{ t('footer.source_code') }}</button>
|
||||
<button class="pl-4" @click="toWebsite('https://github.com/putyy/res-downloader/issues')">{{ t('footer.help') }}</button>
|
||||
<button class="pl-4" @click="toWebsite('https://github.com/putyy/res-downloader/releases')">{{ t('footer.update_log') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,10 +51,16 @@
|
||||
<script lang="ts" setup>
|
||||
import {useIndexStore} from "@/stores"
|
||||
import {BrowserOpenURL} from "../../wailsjs/runtime"
|
||||
import {computed} from "vue"
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const {t} = useI18n()
|
||||
const store = useIndexStore()
|
||||
const props = defineProps(["showModal"])
|
||||
const emits = defineEmits(["update:showModal"])
|
||||
|
||||
const certUrl = computed(()=>{
|
||||
return store.baseUrl + "/api/cert"
|
||||
})
|
||||
const changeShow = (value: boolean) => {
|
||||
emits('update:showModal', value)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
style="--wails-draggable:no-drag"
|
||||
preset="card"
|
||||
class="w-[640px]"
|
||||
title="导入数据"
|
||||
:title="t('index.batch_import')"
|
||||
>
|
||||
<NForm
|
||||
size="medium"
|
||||
@@ -15,17 +15,19 @@
|
||||
style="--wails-draggable:no-drag"
|
||||
>
|
||||
<NFormItem>
|
||||
<NInput type="textarea" v-model:value="content" rows="8" :autosize="false" placeholder="添加多个时,请确保每行只有一个(每个链接回车换行)"></NInput>
|
||||
<NInput type="textarea" v-model:value="content" rows="8" :autosize="false" :placeholder="t('index.import_placeholder')"></NInput>
|
||||
</NFormItem>
|
||||
<NFormItem>
|
||||
<NButton strong secondary type="success" @click="emits('submit', content)" class="w-20">提交</NButton>
|
||||
<NButton strong secondary type="success" @click="emits('submit', content)" class="w-20">{{ t('common.submit') }}</NButton>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NModal>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue"
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const {t} = useI18n()
|
||||
const content = ref("")
|
||||
const props = defineProps<{
|
||||
showModal: boolean
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
useNotification,
|
||||
} from "naive-ui"
|
||||
|
||||
|
||||
function registerNaiveTools() {
|
||||
window.$loadingBar = useLoadingBar()
|
||||
window.$dialog = useDialog()
|
||||
|
||||
@@ -4,43 +4,60 @@
|
||||
:on-update:show="changeShow"
|
||||
style="--wails-draggable:no-drag"
|
||||
preset="dialog"
|
||||
title="管理员授权"
|
||||
:title="t('components.password_title')"
|
||||
content=""
|
||||
:show-icon="false"
|
||||
:mask-closable="false"
|
||||
:close-on-esc="false"
|
||||
class="rounded-lg"
|
||||
>
|
||||
<n-form>
|
||||
<div>
|
||||
<div class="text-red-500 text-base">
|
||||
本操作需要管理员授权,仅对本次运行期间有效!
|
||||
{{ t("components.password_tip") }}
|
||||
</div>
|
||||
<n-form-item path="password" label="">
|
||||
<div class="mt-3">
|
||||
<n-input
|
||||
v-model:value="password"
|
||||
v-model:value="formValue.password"
|
||||
type="password"
|
||||
placeholder="请输入你的电脑密码"
|
||||
:placeholder="t('components.password_placeholder')"
|
||||
class="w-full"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #action>
|
||||
<div class="flex justify-end gap-4">
|
||||
<n-button @click="emits('update:showModal', false)">取消操作</n-button>
|
||||
<n-button type="primary" @click="emits('submit', password)">确认操作</n-button>
|
||||
</div>
|
||||
<div class="mt-3 text-base">
|
||||
<label>{{ t("components.password_cache") }}</label>
|
||||
<NSwitch class="pl-1" v-model:value="formValue.cache" :aria-placeholder="t('components.password_cache')"/>
|
||||
</div>
|
||||
</div>
|
||||
<template #action>
|
||||
<n-button type="primary" @click="submit">{{ t("common.submit") }}</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed} from 'vue'
|
||||
import {NModal, NForm, NFormItem, NInput, NButton} from 'naive-ui'
|
||||
import {reactive} from 'vue'
|
||||
import {NButton, NInput, NModal} from 'naive-ui'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const props = defineProps({
|
||||
const {t} = useI18n()
|
||||
|
||||
defineProps({
|
||||
showModal: Boolean,
|
||||
})
|
||||
const password = ref("")
|
||||
|
||||
const formValue = reactive({
|
||||
password: "",
|
||||
cache: false,
|
||||
})
|
||||
|
||||
const emits = defineEmits(["update:showModal", "submit"])
|
||||
const changeShow = (value: boolean) => emits("update:showModal", value)
|
||||
|
||||
const submit = () => {
|
||||
if (!formValue.password) {
|
||||
window.$message?.error(t("components.password_empty"))
|
||||
return
|
||||
}
|
||||
emits('submit', formValue.password, formValue.cache)
|
||||
}
|
||||
</script>
|
||||
@@ -5,7 +5,7 @@
|
||||
:on-update:show="changeShow"
|
||||
preset="card"
|
||||
class="w-[540px] h-auto"
|
||||
title="预览"
|
||||
:title="t('index.preview')"
|
||||
display-directive="show"
|
||||
:on-after-enter="onAfterEnter"
|
||||
:on-after-leave="onAfterLeave"
|
||||
@@ -30,7 +30,9 @@ import axios from "axios"
|
||||
// @ts-ignore
|
||||
import { getDecryptionArray } from '@/assets/js/decrypt.js'
|
||||
import type Player from "video.js/dist/types/player"
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const {t} = useI18n()
|
||||
const videoPlayer = ref<HTMLElement | any>(null)
|
||||
let player: Player | null = null
|
||||
let flvPlayer: flvjs.Player | null = null
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
<template>
|
||||
<NSpace style="--wails-draggable:no-drag">
|
||||
<NButton v-if="row.Classify != 'live' && row.Classify != 'm3u8'" type="success" :tertiary="true" size="small" @click="action('down')">
|
||||
直接下载
|
||||
{{ t("index.direct_download") }}
|
||||
</NButton>
|
||||
<NButton type="info" :tertiary="true" size="small" @click="action('copy')">
|
||||
复制链接
|
||||
{{ t("index.copy_link") }}
|
||||
</NButton>
|
||||
<NButton v-if="row.Classify != 'live' && row.Classify != 'm3u8'" type="info" :tertiary="true" size="small" @click="action('open')">
|
||||
打开浏览
|
||||
{{ t("index.open_link") }}
|
||||
</NButton>
|
||||
<NButton v-if="row.DecodeKey" type="warning" :tertiary="true" size="small" @click="action('decode')">
|
||||
视频解密
|
||||
{{ t("index.video_decode") }}
|
||||
</NButton>
|
||||
<NButton type="info" :tertiary="true" size="small" @click="action('json')">
|
||||
复制数据
|
||||
{{ t("index.copy_data") }}
|
||||
</NButton>
|
||||
<NButton type="error" :tertiary="true" size="small" @click="action('delete')">
|
||||
删除
|
||||
{{ t("common.delete") }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const {t} = useI18n()
|
||||
const props = defineProps<{
|
||||
row: any,
|
||||
index: number,
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
<template>
|
||||
<div class="flex justify-around px-2 pt-3">
|
||||
<button class="w-3 h-3 rounded-full flex items-center justify-center p-[2px] bg-[#fd5b5b]" @click="closeWindow">
|
||||
<svg t="1729232659670" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11784" width="200" height="200">
|
||||
<path d="M617.92 516.096l272 272-101.824 101.824-272-272-272 272-101.856-101.824 272-272-275.008-275.04L241.056 139.2l275.04 275.04 275.04-275.04 101.824 101.824-275.04 275.04z" fill="#2c2c2c" p-id="11785"/>
|
||||
<button
|
||||
class="w-4 h-4 rounded-full bg-[#ff5c57] flex items-center justify-center transition-all duration-200 hover:bg-[#e0443e] group"
|
||||
@click="closeWindow"
|
||||
:title="t('components.screen_close')"
|
||||
>
|
||||
<svg class="w-[0.7rem] h-[0.7rem] text-[#2c2c2c] opacity-0 group-hover:opacity-100 transition-opacity duration-150" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 6L18 18M6 18L18 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="w-3 h-3 rounded-full bg-yellow-500 flex items-center justify-center p-[2px]" @click="minimizeWindow">
|
||||
<svg t="1729223652686" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9954" width="200" height="200">
|
||||
<path d="M892 472c22.1 0 40 17.9 40 40 0 21.9-17.6 39.6-39.3 40H132c-22.1 0-40-17.9-40-40 0-21.9 17.6-39.6 39.3-40H892z" fill="#111111" p-id="9955"/>
|
||||
<path d="M892 472c22.1 0 40 17.9 40 40 0 21.9-17.6 39.6-39.3 40H132c-22.1 0-40-17.9-40-40 0-21.9 17.6-39.6 39.3-40H892z" fill="#111111" p-id="9956"/>
|
||||
<button
|
||||
class="w-4 h-4 rounded-full bg-[#ffbc38] flex items-center justify-center transition-all duration-200 hover:bg-[#e0a824] group"
|
||||
@click="minimizeWindow"
|
||||
:title="t('components.screen_minimize')"
|
||||
>
|
||||
<svg class="w-[0.7rem] h-[0.7rem] text-[#2c2c2c] opacity-0 group-hover:opacity-100 transition-opacity duration-150" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 12H20" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="w-3 h-3 rounded-full bg-green-500 flex items-center justify-center" :class="isMaximized ? 'p-[2px]' : 'p-[3px]'" @click="maximizeWindow">
|
||||
<svg v-if="isMaximized" t="1729223337944" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2168" width="200" height="200">
|
||||
<button
|
||||
class="w-4 h-4 rounded-full bg-[#28c840] flex items-center justify-center transition-all duration-200 hover:bg-[#1ea230] group"
|
||||
@click="maximizeWindow"
|
||||
:title="isMaximized ? t('components.screen_restore') : t('components.screen_maximize')"
|
||||
>
|
||||
<svg v-if="isMaximized" class="w-[0.5rem] h-[0.5rem] text-[#2c2c2c] opacity-0 group-hover:opacity-100 transition-opacity duration-150" t="1729223337944" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2168">
|
||||
<path d="M965.6 512.8L564 512c-28.8 0-52 23.2-52 51.2l1.6 401.6c0 28.8 16.8 35.2 36.8 15.2l430.4-431.2c20-20 12.8-36-15.2-36zM510.4 58.4c0-28.8-16.8-35.2-36.8-15.2L44 474.4c-20 20-12.8 36.8 15.2 36.8l401.6 0.8c28.8 0 51.2-23.2 51.2-51.2l-1.6-402.4z" fill="#2c2c2c" p-id="2169"/>
|
||||
</svg>
|
||||
<svg v-else t="1729223289826" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1881" width="200" height="200">
|
||||
<svg v-else class="w-[0.5rem] h-[0.5rem] text-[#2c2c2c] opacity-0 group-hover:opacity-100 transition-opacity duration-150" t="1729223289826" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1881">
|
||||
<path d="M966.2236448 410.53297813l0 496.32142187a56.61582187 56.61582187 0 0 1-56.59306667 56.61582187l-496.32142293-1e-8a56.59306667 56.59306667 0 0 1-39.9815104-96.68835519L869.53528854 370.57422187a56.61582187 56.61582187 0 0 1 96.71111146 39.95875626z m-905.67111147 200.15786666L60.55253333 114.39217812a56.61582187 56.61582187 0 0 1 56.59306667-56.61582292l496.29866667 0a56.59306667 56.59306667 0 0 1 39.98151146 96.68835626l-496.2076448 496.20764374a56.61582187 56.61582187 0 0 1-96.68835519-39.9815104z" fill="#373C43" p-id="1882"/>
|
||||
</svg>
|
||||
</button>
|
||||
@@ -25,8 +36,10 @@
|
||||
<script lang="ts" setup>
|
||||
import {ref} from "vue"
|
||||
import {Quit, WindowFullscreen, WindowMinimise, WindowUnfullscreen} from "../../wailsjs/runtime"
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const isMaximized = ref(false);
|
||||
const {t} = useI18n()
|
||||
const isMaximized = ref(false)
|
||||
|
||||
const closeWindow = () => {
|
||||
Quit()
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50" v-if="isLoading">
|
||||
<div class="flex flex-col items-center">
|
||||
<svg class="animate-spin h-10 w-10 text-white mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<svg class="animate-spin h-10 w-10 text-white mb-4" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
|
||||
</svg>
|
||||
<span class="text-white">{{ loadingText }}</span>
|
||||
<span class="text-white" v-if="loadingText">{{ loadingText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps(["isLoading", "loadingText"])
|
||||
defineProps<{
|
||||
isLoading: boolean
|
||||
loadingText?: string
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="flex pb-2 flex-col h-full min-w-[80px] border-r border-slate-100 dark:border-slate-900">
|
||||
<Screen v-if="envInfo.platform!=='darwin'"></Screen>
|
||||
<div class="w-full flex flex-row items-center justify-center pt-5 ml-[-5px]" :class="envInfo.platform==='darwin' ? 'pt-8' : 'pt-2'">
|
||||
<img class="w-12 h-12 cursor-pointer" src="@/assets/image/logo.png" alt="res-downloader logo" @click="handleFooterUpdate('github')"/>
|
||||
<div class="w-full flex flex-row items-center justify-center pt-5" :class="envInfo.platform==='darwin' ? 'pt-8' : 'pt-2'">
|
||||
<img class="w-12 h-12 cursor-pointer" src="@/assets/image/logo.png" alt="res-downloader logo" @click="handleFooterUpdate('github')"/>
|
||||
</div>
|
||||
<main class="flex-1 flex-grow-1 mb-5 overflow-auto flex flex-col pt-1 items-center h-full">
|
||||
<main class="flex-1 flex-grow-1 mb-5 overflow-auto flex flex-col pt-1 items-center h-full" v-if="is">
|
||||
<NScrollbar :size="1">
|
||||
<NLayout has-sider>
|
||||
<NLayoutSider
|
||||
@@ -14,10 +14,11 @@
|
||||
:on-after-enter="() => { showAppName = true }"
|
||||
:on-after-leave="() => { showAppName = false }"
|
||||
:collapsed-width="70"
|
||||
:default-collapsed="true"
|
||||
:width="120"
|
||||
:collapsed="collapsed"
|
||||
:width="140"
|
||||
:native-scrollbar="false"
|
||||
:inverted="inverted"
|
||||
:on-update:collapsed="collapsedChange"
|
||||
class="bg-inherit"
|
||||
>
|
||||
<NMenu
|
||||
@@ -43,36 +44,43 @@
|
||||
</NScrollbar>
|
||||
</main>
|
||||
</div>
|
||||
<Footer v-model:showModal="showAppInfo" />
|
||||
<Footer v-model:showModal="showAppInfo"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {MenuOption} from "naive-ui"
|
||||
import {NIcon} from "naive-ui"
|
||||
import {computed, h, ref, watch} from "vue"
|
||||
import {computed, h, onMounted, ref, watch} from "vue"
|
||||
import {useRoute, useRouter} from "vue-router"
|
||||
import {
|
||||
CloudOutline,
|
||||
SettingsOutline,
|
||||
HelpCircleOutline,
|
||||
MoonOutline, SunnyOutline, LogoGithub
|
||||
MoonOutline,
|
||||
SunnyOutline,
|
||||
LanguageSharp,
|
||||
LogoGithub
|
||||
} from "@vicons/ionicons5"
|
||||
import {useIndexStore} from "@/stores"
|
||||
import Footer from "@/components/Footer.vue"
|
||||
import Screen from "@/components/Screen.vue";
|
||||
import {BrowserOpenURL} from "../../../wailsjs/runtime";
|
||||
import Screen from "@/components/Screen.vue"
|
||||
import {BrowserOpenURL} from "../../../wailsjs/runtime"
|
||||
import {useI18n} from "vue-i18n"
|
||||
|
||||
const {t} = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const inverted = ref(false)
|
||||
const collapsed = ref(false)
|
||||
const showAppName = ref(false)
|
||||
const showAppInfo = ref(false)
|
||||
const menuValue = ref(route.fullPath.substring(1))
|
||||
const store = useIndexStore()
|
||||
const is = ref(false)
|
||||
|
||||
const envInfo = store.envInfo
|
||||
|
||||
const globalConfig = computed(()=>{
|
||||
const globalConfig = computed(() => {
|
||||
return store.globalConfig
|
||||
})
|
||||
|
||||
@@ -82,7 +90,15 @@ const theme = computed(() => {
|
||||
|
||||
watch(() => route.path, (newPath, oldPath) => {
|
||||
menuValue.value = route.fullPath.substring(1)
|
||||
});
|
||||
})
|
||||
|
||||
onMounted(()=>{
|
||||
const collapsedCache = localStorage.getItem("collapsed");
|
||||
if (collapsedCache) {
|
||||
collapsed.value = JSON.parse(collapsedCache).collapsed
|
||||
}
|
||||
is.value = true
|
||||
})
|
||||
|
||||
const renderIcon = (icon: any) => {
|
||||
return () => h(NIcon, null, {default: () => h(icon)})
|
||||
@@ -90,30 +106,35 @@ const renderIcon = (icon: any) => {
|
||||
|
||||
const menuOptions = ref([
|
||||
{
|
||||
label: "拦截",
|
||||
label: computed(() => t("menu.index")),
|
||||
key: 'index',
|
||||
icon: renderIcon(CloudOutline),
|
||||
},
|
||||
{
|
||||
label: "设置",
|
||||
label: computed(() => t("menu.setting")),
|
||||
key: 'setting',
|
||||
icon: renderIcon(SettingsOutline),
|
||||
},
|
||||
])
|
||||
|
||||
const footerOptions = ref([
|
||||
{
|
||||
label: "主题",
|
||||
key: 'theme',
|
||||
icon: theme,
|
||||
},
|
||||
{
|
||||
label: "github",
|
||||
key: 'github',
|
||||
icon: renderIcon(LogoGithub),
|
||||
},
|
||||
{
|
||||
label: "关于",
|
||||
label: computed(() => t("menu.locale")),
|
||||
key: 'locale',
|
||||
icon: renderIcon(LanguageSharp),
|
||||
},
|
||||
{
|
||||
label: computed(() => t("menu.theme")),
|
||||
key: 'theme',
|
||||
icon: theme,
|
||||
},
|
||||
{
|
||||
label: computed(() => t("menu.about")),
|
||||
key: 'about',
|
||||
icon: renderIcon(HelpCircleOutline),
|
||||
},
|
||||
@@ -133,6 +154,7 @@ const handleFooterUpdate = (key: string, item?: MenuOption) => {
|
||||
BrowserOpenURL("https://github.com/putyy/res-downloader")
|
||||
return
|
||||
}
|
||||
|
||||
if (key === "theme") {
|
||||
if (globalConfig.value.Theme === "darkTheme") {
|
||||
store.setConfig({Theme: "lightTheme"})
|
||||
@@ -142,8 +164,23 @@ const handleFooterUpdate = (key: string, item?: MenuOption) => {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (key === "locale") {
|
||||
if (globalConfig.value.Locale === "zh") {
|
||||
store.setConfig({Locale: "en"})
|
||||
return
|
||||
}
|
||||
store.setConfig({Locale: "zh"})
|
||||
return
|
||||
}
|
||||
|
||||
menuValue.value = key
|
||||
return router.push({path: "/" + key})
|
||||
}
|
||||
|
||||
const collapsedChange = (value: boolean)=>{
|
||||
console.log("collapsedChange",value)
|
||||
collapsed.value = value
|
||||
localStorage.setItem("collapsed", JSON.stringify({collapsed: value}))
|
||||
}
|
||||
</script>
|
||||
14
frontend/src/i18n.ts
Normal file
14
frontend/src/i18n.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {createI18n} from 'vue-i18n'
|
||||
import en from './locales/en.json'
|
||||
import zh from './locales/zh.json'
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'zh',
|
||||
fallbackLocale: 'en',
|
||||
messages: {
|
||||
en,
|
||||
zh
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n
|
||||
113
frontend/src/locales/en.json
Normal file
113
frontend/src/locales/en.json
Normal file
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"common": {
|
||||
"copy_success": "Copy Success",
|
||||
"copy_fail": "Copy Failed",
|
||||
"file_path": "File Path",
|
||||
"loading": "Loading",
|
||||
"select": "Select",
|
||||
"submit": "Submit",
|
||||
"delete": "Delete",
|
||||
"yes": "Yes",
|
||||
"no": "No"
|
||||
},
|
||||
"components": {
|
||||
"password_title": "Admin Authorization",
|
||||
"password_tip": "The password entered this time is only valid during this session, used for installing certificates or setting system proxy!",
|
||||
"password_placeholder": "Please enter your computer password",
|
||||
"password_cache": "Cache Password",
|
||||
"password_empty": "Password cannot be empty",
|
||||
"screen_minimize": "Minimize",
|
||||
"screen_restore": "Restore",
|
||||
"screen_maximize": "Maximize",
|
||||
"screen_close": "Close"
|
||||
},
|
||||
"menu": {
|
||||
"index": "Intercept",
|
||||
"setting": "Setting",
|
||||
"locale": "中文",
|
||||
"theme": "Theme",
|
||||
"about": "About"
|
||||
},
|
||||
"index": {
|
||||
"open_grab": "Start Grabbing",
|
||||
"close_grab": "Stop Grabbing",
|
||||
"grab_type": "Grab Type",
|
||||
"clear_list": "Clear List",
|
||||
"batch_download": "Batch Download",
|
||||
"batch_export": "Batch Export",
|
||||
"batch_import": "Batch Import",
|
||||
"import_success": "Export Success",
|
||||
"all": "All",
|
||||
"image": "Image",
|
||||
"audio": "Audio",
|
||||
"video": "Video",
|
||||
"m3u8": "M3U8",
|
||||
"live": "Live Stream",
|
||||
"xls": "Spreadsheet",
|
||||
"doc": "Document",
|
||||
"pdf": "PDF",
|
||||
"font": "Font",
|
||||
"domain": "Domain",
|
||||
"type": "Type",
|
||||
"preview": "Preview",
|
||||
"preview_tip": "Preview not supported",
|
||||
"status": "Status",
|
||||
"description": "Description",
|
||||
"resource_size": "Resource Size",
|
||||
"save_path": "Save Path",
|
||||
"save_path_empty": "Please set save location",
|
||||
"operation": "Operation",
|
||||
"ready": "Ready",
|
||||
"running": "Running",
|
||||
"error": "Error",
|
||||
"done": "Done",
|
||||
"handle": "Post Processing",
|
||||
"direct_download": "Download",
|
||||
"download_success": "Download Success",
|
||||
"copy_link": "Copy Link",
|
||||
"copy_data": "Copy Data",
|
||||
"open_link": "Open Link",
|
||||
"open_file": "Open File",
|
||||
"video_decode": "WxDecrypt",
|
||||
"video_decode_loading": "Decrypting",
|
||||
"video_decode_no": "Cannot Decrypt",
|
||||
"video_decode_success": "Decrypt Success",
|
||||
"use_data": "Please select required data",
|
||||
"import_placeholder": "When adding multiple items, ensure each line contains only one (each link on a new line)",
|
||||
"import_empty": "Please enter data to import"
|
||||
},
|
||||
"setting": {
|
||||
"restart_tip": "Keep default if unsure, please restart software after modification",
|
||||
"save_dir": "Save Directory",
|
||||
"filename_rules": "Filename Rules",
|
||||
"filename_rules_tip": "Input controls filename length (excluding timestamp, 0 means invalid, effective when description exists), switch controls whether to add timestamp at filename end",
|
||||
"auto_proxy": "Auto Intercept",
|
||||
"auto_proxy_tip": "Enable intercept when software starts",
|
||||
"quality": "Quality",
|
||||
"quality_value": "Default(Recommended),Ultra HD,High Quality,Medium Quality,Low Quality",
|
||||
"quality_tip": "Effective for video accounts",
|
||||
"full_intercept": "Full Intercept",
|
||||
"full_intercept_tip": "Whether to fully intercept WeChat video accounts, No: only intercept video details",
|
||||
"upstream_proxy": "Upstream Proxy",
|
||||
"upstream_proxy_tip": "For combining with other proxy tools, format: http://username:password@your.proxy.server:port",
|
||||
"download_proxy": "Download Proxy",
|
||||
"download_proxy_tip": "Whether to use upstream proxy for downloads",
|
||||
"user_agent_tip": "Keep default if unsure",
|
||||
"connections": "Connections",
|
||||
"connections_tip": "Keep default if unsure, usually CPU cores * 2, for faster downloads",
|
||||
"use_headers_tip": "Define headers for downloads, comma separated",
|
||||
"mime_map": "Intercept Rules",
|
||||
"mime_map_tip": "JSON format, keep default if unsure"
|
||||
},
|
||||
"footer": {
|
||||
"title": "About Us",
|
||||
"description": "A software combining network resource sniffing + high-speed download functions, with high aesthetics, performance and diversity, providing personal users with the ability to download their own resources uploaded to various platforms!",
|
||||
"support": "Supports almost all network applications on the market",
|
||||
"application": "Douyin,Kuaishou,Xiaohongshu,Wechat,Mini Programs,Youtube,Kugou Music,QQ Music,QQ Weishi,......",
|
||||
"forum": "Forum",
|
||||
"cert": "Certificate",
|
||||
"source_code": "Source Code",
|
||||
"help": "Issues",
|
||||
"update_log": "Update Log"
|
||||
}
|
||||
}
|
||||
113
frontend/src/locales/zh.json
Normal file
113
frontend/src/locales/zh.json
Normal file
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"common": {
|
||||
"copy_success": "复制成功",
|
||||
"copy_fail": "复制失败",
|
||||
"file_path": "文件路径",
|
||||
"loading": "loading",
|
||||
"select": "选择",
|
||||
"submit": "提交",
|
||||
"delete": "删除",
|
||||
"yes": "是",
|
||||
"no": "否"
|
||||
},
|
||||
"components": {
|
||||
"password_title": "管理员授权",
|
||||
"password_tip": "本次输入的密码仅在本次运行期间有效,用于安装证书或设置系统代理!",
|
||||
"password_placeholder": "请输入你的电脑密码",
|
||||
"password_cache": "缓存密码",
|
||||
"password_empty": "密码不能为空",
|
||||
"screen_minimize": "最小化",
|
||||
"screen_restore": "还原",
|
||||
"screen_maximize": "最大化",
|
||||
"screen_close": "关闭"
|
||||
},
|
||||
"menu": {
|
||||
"index": "获取资源",
|
||||
"setting": "系统设置",
|
||||
"locale": "English",
|
||||
"theme": "主题更换",
|
||||
"about": "关于我们"
|
||||
},
|
||||
"index": {
|
||||
"open_grab": "开启抓取",
|
||||
"close_grab": "关闭抓取",
|
||||
"grab_type": "抓取类型",
|
||||
"clear_list": "清空列表",
|
||||
"batch_download": "批量下载",
|
||||
"batch_export": "批量导出",
|
||||
"batch_import": "批量导入",
|
||||
"import_success": "导出成功",
|
||||
"all": "全部",
|
||||
"image": "图片",
|
||||
"audio": "音频",
|
||||
"video": "视频",
|
||||
"m3u8": "m3u8",
|
||||
"live": "直播流",
|
||||
"xls": "表格",
|
||||
"doc": "文档",
|
||||
"pdf": "pdf",
|
||||
"font": "字体",
|
||||
"domain": "域",
|
||||
"type": "类型",
|
||||
"preview": "预览",
|
||||
"preview_tip": "暂不支持预览",
|
||||
"status": "状态",
|
||||
"description": "描述",
|
||||
"resource_size": "资源大小",
|
||||
"save_path": "保存路径",
|
||||
"save_path_empty": "请设置保存位置",
|
||||
"operation": "操作",
|
||||
"ready": "就绪",
|
||||
"running": "运行中",
|
||||
"error": "错误",
|
||||
"done": "完成",
|
||||
"handle": "后续处理",
|
||||
"direct_download": "直接下载",
|
||||
"download_success": "下载成功",
|
||||
"copy_link": "复制链接",
|
||||
"copy_data": "复制数据",
|
||||
"open_link": "打开链接",
|
||||
"open_file": "打开文件",
|
||||
"video_decode": "视频解密",
|
||||
"video_decode_loading": "解密中",
|
||||
"video_decode_no": "无法解密",
|
||||
"video_decode_success": "解密成功",
|
||||
"use_data": "请选择需要的数据",
|
||||
"import_placeholder": "添加多个时,请确保每行只有一个(每个链接回车换行)",
|
||||
"import_empty": "请输入需要导入的数据"
|
||||
},
|
||||
"setting": {
|
||||
"restart_tip": "如果不清楚保持默认就行,修改后请重启软件",
|
||||
"save_dir": "保存目录",
|
||||
"filename_rules": "文件命名",
|
||||
"filename_rules_tip": "输入框控制文件命名的长度(不含时间、0为无效,此选项有描述信息时有效),开关控制文件末尾是否添加时间标识",
|
||||
"auto_proxy": "自动拦截",
|
||||
"auto_proxy_tip": "打开软件时动启用拦截",
|
||||
"quality": "清晰度",
|
||||
"quality_value": "默认(推荐),超清,高画质,中画质,低画质",
|
||||
"quality_tip": "视频号有效",
|
||||
"full_intercept": "全量拦截",
|
||||
"full_intercept_tip": "微信视频号是否全量拦截,否:只拦截视频详情",
|
||||
"upstream_proxy": "上游代理",
|
||||
"upstream_proxy_tip": "用于结合其他代理工具,格式: http://username:password@your.proxy.server:port",
|
||||
"download_proxy": "下载代理",
|
||||
"download_proxy_tip": "下载操作时是否使用上游代理",
|
||||
"user_agent_tip": "如不清楚请保持默认",
|
||||
"connections": "连接数",
|
||||
"connections_tip": "如不清楚请保持默认,通常CPU核心数*2,用于加速下载",
|
||||
"use_headers_tip": "定义下载时可使用的header参数,逗号分割",
|
||||
"mime_map": "拦截规则",
|
||||
"mime_map_tip": "json格式,如果不清楚保持默认就行"
|
||||
},
|
||||
"footer": {
|
||||
"title": "关于我们",
|
||||
"description": "一款集网络资源嗅探 + 高速下载功能于一体的软件,高颜值、高性能和多样化,提供个人用户下载自己上传到各大平台的网络资源功能!",
|
||||
"support": "支持市面上几乎所有的网络应用",
|
||||
"application": "抖音,快手,小红书,视频号,小程序,公众号,酷狗音乐,QQ音乐,QQ微视,......",
|
||||
"forum": "论坛",
|
||||
"cert": "证书",
|
||||
"source_code": "软件源码",
|
||||
"help": "帮助支持",
|
||||
"update_log": "更新日志"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import './assets/css/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import {createApp} from 'vue'
|
||||
import {createPinia} from 'pinia'
|
||||
import i18n from './i18n'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
createApp(App)
|
||||
.use(router)
|
||||
.use(i18n)
|
||||
.use(createPinia())
|
||||
.mount('#app')
|
||||
|
||||
@@ -16,7 +16,7 @@ const routes = [
|
||||
{
|
||||
path: "/setting",
|
||||
name: "setting",
|
||||
meta: {keepAlive: true},
|
||||
meta: {keepAlive: false},
|
||||
component: () => import("@/views/setting.vue"),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@ import {appType} from "@/types/app"
|
||||
export const useEventStore = defineStore('ws-store', () => {
|
||||
const handles = ref<any>({})
|
||||
|
||||
const init = ()=>{
|
||||
const init = () => {
|
||||
EventsOn("event", (res: any) => {
|
||||
const data = JSON.parse(res)
|
||||
if (handles.value.hasOwnProperty(data.type)) {
|
||||
|
||||
@@ -3,6 +3,8 @@ import {ref} from "vue"
|
||||
import type {appType} from "@/types/app"
|
||||
import appApi from "@/api/app"
|
||||
import {Environment} from "../../wailsjs/runtime"
|
||||
import * as bind from "../../wailsjs/go/core/Bind"
|
||||
import {core} from "../../wailsjs/go/models"
|
||||
|
||||
export const useIndexStore = defineStore("index-store", () => {
|
||||
const appInfo = ref<appType.App>({
|
||||
@@ -14,6 +16,7 @@ export const useIndexStore = defineStore("index-store", () => {
|
||||
|
||||
const globalConfig = ref<appType.Config>({
|
||||
Theme: "lightTheme",
|
||||
Locale: "zh",
|
||||
Host: "0.0.0.0",
|
||||
Port: "8899",
|
||||
Quality: 0,
|
||||
@@ -40,30 +43,28 @@ export const useIndexStore = defineStore("index-store", () => {
|
||||
const tableHeight = ref(800)
|
||||
|
||||
const isProxy = ref(false)
|
||||
const baseUrl = ref("")
|
||||
|
||||
const init = async () => {
|
||||
Environment().then((res) => {
|
||||
envInfo.value = res
|
||||
})
|
||||
await getAppInfo()
|
||||
await appApi.getConfig().then((res) => {
|
||||
|
||||
await bind.AppInfo().then((res: core.ResponseData)=>{
|
||||
appInfo.value = Object.assign({}, appInfo.value, res.data)
|
||||
isProxy.value = res.data.IsProxy
|
||||
})
|
||||
|
||||
await bind.Config().then((res: core.ResponseData)=>{
|
||||
globalConfig.value = Object.assign({}, globalConfig.value, res.data)
|
||||
})
|
||||
setTimeout(() => {
|
||||
appApi.isProxy().then((res: any) => {
|
||||
isProxy.value = res.data.isProxy
|
||||
})
|
||||
}, 150)
|
||||
|
||||
baseUrl.value = "http://"+globalConfig.value.Host + ":" +globalConfig.value.Port
|
||||
window.$baseUrl = baseUrl.value
|
||||
window.addEventListener("resize", handleResize);
|
||||
handleResize()
|
||||
}
|
||||
|
||||
const getAppInfo = async () => {
|
||||
await appApi.appInfo().then((res) => {
|
||||
appInfo.value = Object.assign({}, appInfo.value, res.data)
|
||||
})
|
||||
}
|
||||
|
||||
const setConfig = (formValue: Object) => {
|
||||
globalConfig.value = Object.assign({}, globalConfig.value, formValue)
|
||||
appApi.setConfig(globalConfig.value)
|
||||
@@ -73,9 +74,32 @@ export const useIndexStore = defineStore("index-store", () => {
|
||||
tableHeight.value = document.documentElement.clientHeight || window.innerHeight
|
||||
}
|
||||
|
||||
const updateProxyStatus = (res: any) => {
|
||||
isProxy.value = res.isProxy
|
||||
const openProxy = async () => {
|
||||
return appApi.openSystemProxy().then(handleProxy)
|
||||
}
|
||||
|
||||
return {appInfo, globalConfig, tableHeight, isProxy, envInfo, init, getAppInfo, setConfig, updateProxyStatus}
|
||||
const unsetProxy = async () => {
|
||||
return appApi.unsetSystemProxy().then(handleProxy)
|
||||
}
|
||||
|
||||
const handleProxy = (res: appType.Res) => {
|
||||
isProxy.value = res.data.value
|
||||
if (res.code === 0) {
|
||||
window?.$message?.error(res.message)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
return {
|
||||
appInfo,
|
||||
globalConfig,
|
||||
tableHeight,
|
||||
isProxy,
|
||||
envInfo,
|
||||
baseUrl,
|
||||
init,
|
||||
setConfig,
|
||||
openProxy,
|
||||
unsetProxy
|
||||
}
|
||||
})
|
||||
5
frontend/src/types/app.d.ts
vendored
5
frontend/src/types/app.d.ts
vendored
@@ -13,6 +13,7 @@ export namespace appType {
|
||||
|
||||
interface Config {
|
||||
Theme: string
|
||||
Locale: string
|
||||
Host: string
|
||||
Port: string
|
||||
Quality: number
|
||||
@@ -27,7 +28,7 @@ export namespace appType {
|
||||
TaskNumber: number
|
||||
UserAgent: string
|
||||
UseHeaders: string
|
||||
MimeMap: {[key: string]: MimeMap}
|
||||
MimeMap: { [key: string]: MimeMap }
|
||||
}
|
||||
|
||||
interface MediaInfo {
|
||||
@@ -44,7 +45,7 @@ export namespace appType {
|
||||
DecodeKey: string
|
||||
Description: string
|
||||
ContentType: string
|
||||
OtherData: {[key: string]: string}
|
||||
OtherData: { [key: string]: string }
|
||||
}
|
||||
|
||||
interface DownloadProgress {
|
||||
|
||||
10
frontend/src/types/global.d.ts
vendored
10
frontend/src/types/global.d.ts
vendored
@@ -1,9 +1,9 @@
|
||||
interface Window {
|
||||
$loadingBar?: import('naive-ui').LoadingBarProviderInst;
|
||||
$dialog?: import('naive-ui').DialogProviderInst;
|
||||
$message?: import('naive-ui').MessageProviderInst;
|
||||
$notification?: import('naive-ui').NotificationProviderInst;
|
||||
$ws?: WebSocket;
|
||||
$loadingBar?: import('naive-ui').LoadingBarProviderInst
|
||||
$dialog?: import('naive-ui').DialogProviderInst
|
||||
$message?: import('naive-ui').MessageProviderInst
|
||||
$notification?: import('naive-ui').NotificationProviderInst
|
||||
$baseUrl?: string
|
||||
}
|
||||
|
||||
declare module '*.vue' {
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
<template>
|
||||
<div class="flex flex-col p-5">
|
||||
<div class="h-full flex flex-col p-5 overflow-y-auto [&::-webkit-scrollbar]:hidden">
|
||||
<div class="pb-2 z-40">
|
||||
<NSpace>
|
||||
<NButton v-if="isProxy" secondary type="primary" @click.stop="close" style="--wails-draggable:no-drag">关闭代理</NButton>
|
||||
<NButton v-else tertiary type="tertiary" @click.stop="open" style="--wails-draggable:no-drag">开启代理</NButton>
|
||||
<NButton tertiary type="error" @click.stop="clear" style="--wails-draggable:no-drag">清空列表</NButton>
|
||||
<NSelect style="min-width: 100px;--wails-draggable:no-drag" placeholder="拦截类型" v-model:value="resourcesType" multiple clearable :max-tag-count="3" :options="classify"></NSelect>
|
||||
<NButton tertiary type="info" @click.stop="batchDown" style="--wails-draggable:no-drag">批量下载</NButton>
|
||||
<NButton tertiary type="info" @click.stop="batchImport" style="--wails-draggable:no-drag">批量导出</NButton>
|
||||
<NButton tertiary type="info" @click.stop="showImport=true" style="--wails-draggable:no-drag">批量导入</NButton>
|
||||
<NButton v-if="isProxy" secondary type="primary" @click.stop="close" style="--wails-draggable:no-drag">
|
||||
{{ t("index.close_grab") }}
|
||||
</NButton>
|
||||
<NButton v-else tertiary type="tertiary" @click.stop="open" style="--wails-draggable:no-drag">
|
||||
{{ t("index.open_grab") }}
|
||||
</NButton>
|
||||
<NButton tertiary type="error" @click.stop="clear" style="--wails-draggable:no-drag">
|
||||
{{ t("index.clear_list") }}
|
||||
</NButton>
|
||||
<NSelect style="min-width: 100px;--wails-draggable:no-drag" :placeholder="t('index.grab_type')"
|
||||
v-model:value="resourcesType" multiple clearable :max-tag-count="3" :options="classify"></NSelect>
|
||||
<NButton tertiary type="info" @click.stop="batchDown" style="--wails-draggable:no-drag">
|
||||
{{ t("index.batch_download") }}
|
||||
</NButton>
|
||||
<NButton tertiary type="info" @click.stop="batchImport" style="--wails-draggable:no-drag">
|
||||
{{ t("index.batch_export") }}
|
||||
</NButton>
|
||||
<NButton tertiary type="info" @click.stop="showImport=true" style="--wails-draggable:no-drag">
|
||||
{{ t("index.batch_import") }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
@@ -23,6 +36,7 @@
|
||||
:height-for-row="()=> 48"
|
||||
:checked-row-keys="checkedRowKeysValue"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
style="--wails-draggable:no-drag"
|
||||
/>
|
||||
</div>
|
||||
<Preview v-model:showModal="showPreviewRow" :previewRow="previewRow"/>
|
||||
@@ -34,7 +48,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {NButton, NImage, NTooltip} from "naive-ui"
|
||||
import {computed, h, onMounted, ref, watch} from "vue"
|
||||
import {computed, h, onMounted, ref, reactive, watch} from "vue"
|
||||
import type {appType} from "@/types/app"
|
||||
|
||||
import type {DataTableRowKey, ImageRenderToolbarProps} from "naive-ui"
|
||||
@@ -44,13 +58,14 @@ import ShowLoading from "@/components/ShowLoading.vue"
|
||||
import {getDecryptionArray} from '@/assets/js/decrypt.js'
|
||||
import {useIndexStore} from "@/stores"
|
||||
import appApi from "@/api/app"
|
||||
import {DwStatus} from "@/const"
|
||||
import ResAction from "@/components/ResAction.vue"
|
||||
import ImportJson from "@/components/ImportJson.vue"
|
||||
import {useEventStore} from "@/stores/event"
|
||||
import {BrowserOpenURL, ClipboardSetText} from "../../wailsjs/runtime"
|
||||
import Password from "@/components/Password.vue"
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const {t} = useI18n()
|
||||
const eventStore = useEventStore()
|
||||
const isProxy = computed(() => {
|
||||
return store.isProxy
|
||||
@@ -58,24 +73,36 @@ const isProxy = computed(() => {
|
||||
const data = ref<any[]>([])
|
||||
const store = useIndexStore()
|
||||
const tableHeight = computed(() => {
|
||||
return store.tableHeight - 132
|
||||
return store.globalConfig.Locale === "zh" ? store.tableHeight - 130 : store.tableHeight - 151
|
||||
})
|
||||
const resourcesType = ref<string[]>(["all"])
|
||||
const classifyAlias: {[key: string]: string} = {
|
||||
image: "图片",
|
||||
audio: "音频",
|
||||
video: "视频",
|
||||
m3u8: "m3u8",
|
||||
live: "直播流",
|
||||
xls: "表格",
|
||||
doc: "文档",
|
||||
pdf: "pdf",
|
||||
font: "字体"
|
||||
|
||||
const classifyAlias: { [key: string]: any } = {
|
||||
image: computed(() => t("index.image")),
|
||||
audio: computed(() => t("index.audio")),
|
||||
video: computed(() => t("index.video")),
|
||||
m3u8: computed(() => t("index.m3u8")),
|
||||
live: computed(() => t("index.live")),
|
||||
xls: computed(() => t("index.xls")),
|
||||
doc: computed(() => t("index.pdf")),
|
||||
pdf: computed(() => t("index.pdf")),
|
||||
font: computed(() => t("index.font"))
|
||||
}
|
||||
|
||||
const dwStatus = computed<any>(() => {
|
||||
return {
|
||||
ready: t("common.ready"),
|
||||
running: t("common.running"),
|
||||
error: t("common.error"),
|
||||
done: t("common.done"),
|
||||
handle: t("common.handle")
|
||||
}
|
||||
})
|
||||
|
||||
const classify = ref([
|
||||
{
|
||||
value: "all",
|
||||
label: "全部",
|
||||
label: computed(() => t("index.all")),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -84,13 +111,13 @@ const columns = ref<any[]>([
|
||||
type: "selection",
|
||||
},
|
||||
{
|
||||
title: "域",
|
||||
title: computed(() => t("index.domain")),
|
||||
key: "Domain",
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
title: computed(() => t("index.type")),
|
||||
key: "Classify",
|
||||
filterOptions: Array.from(classify.value).slice(1),
|
||||
filterOptions: computed(() => Array.from(classify.value).slice(1)),
|
||||
filterMultiple: true,
|
||||
filter: (value: string, row: appType.MediaInfo) => {
|
||||
return !!~row.Classify.indexOf(String(value))
|
||||
@@ -105,7 +132,7 @@ const columns = ref<any[]>([
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "预览",
|
||||
title: computed(() => t("index.preview")),
|
||||
key: "Url",
|
||||
width: 120,
|
||||
render: (row: appType.MediaInfo) => {
|
||||
@@ -138,9 +165,9 @@ const columns = ref<any[]>([
|
||||
{
|
||||
default: () => {
|
||||
if (row.Classify === "audio" || row.Classify === "video" || row.Classify === "m3u8" || row.Classify === "live") {
|
||||
return "预览"
|
||||
return t("index.preview")
|
||||
}
|
||||
return "暂不支持预览"
|
||||
return t("index.preview_tip")
|
||||
}
|
||||
}
|
||||
),
|
||||
@@ -148,14 +175,14 @@ const columns = ref<any[]>([
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
title: computed(() => t("index.status")),
|
||||
key: "Status",
|
||||
render: (row: appType.MediaInfo) => {
|
||||
return DwStatus[row.Status as keyof typeof DwStatus]
|
||||
return dwStatus[row.Status as keyof typeof dwStatus]
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
title: computed(() => t("index.description")),
|
||||
key: "Description",
|
||||
width: 150,
|
||||
render: (row: appType.MediaInfo, index: number) => {
|
||||
@@ -172,11 +199,11 @@ const columns = ref<any[]>([
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "资源大小",
|
||||
title: computed(() => t("index.resource_size")),
|
||||
key: "Size"
|
||||
},
|
||||
{
|
||||
title: "保存路径",
|
||||
title: computed(() => t("index.save_path")),
|
||||
key: "SavePath",
|
||||
render(row: appType.MediaInfo, index: number) {
|
||||
return h("a",
|
||||
@@ -196,7 +223,7 @@ const columns = ref<any[]>([
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
title: computed(() => t("index.operation")),
|
||||
key: "actions",
|
||||
render(row: appType.MediaInfo, index: number) {
|
||||
return h(ResAction, {key: index, row: row, index: index, onAction: dataAction})
|
||||
@@ -258,7 +285,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
localStorage.setItem("resources-data", JSON.stringify(data.value))
|
||||
window?.$message?.success("下载成功")
|
||||
window?.$message?.success(t("index.download_success"))
|
||||
break;
|
||||
case "error":
|
||||
loading.value = false
|
||||
@@ -269,9 +296,9 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
|
||||
watch(()=>{
|
||||
watch(() => {
|
||||
return store.globalConfig.MimeMap
|
||||
}, ()=>{
|
||||
}, () => {
|
||||
buildClassify()
|
||||
})
|
||||
|
||||
@@ -280,11 +307,11 @@ watch(resourcesType, (n, o) => {
|
||||
appApi.setType(resourcesType.value)
|
||||
})
|
||||
|
||||
const buildClassify = ()=>{
|
||||
const buildClassify = () => {
|
||||
const mimeMap = store.globalConfig.MimeMap ?? {}
|
||||
const seen = new Set()
|
||||
classify.value = [
|
||||
{value: "all", label: "全部"},
|
||||
{value: "all", label: computed(() => t("index.all"))},
|
||||
...Object.values(mimeMap)
|
||||
.filter(({Type}) => {
|
||||
if (seen.has(Type)) return false;
|
||||
@@ -306,18 +333,18 @@ const dataAction = (row: appType.MediaInfo, index: number, type: string) => {
|
||||
case "copy":
|
||||
ClipboardSetText(row.Url).then((is: boolean) => {
|
||||
if (is) {
|
||||
window?.$message?.success("复制成功")
|
||||
window?.$message?.success(t("common.copy_success"))
|
||||
} else {
|
||||
window?.$message?.error("复制失败")
|
||||
window?.$message?.error(t("common.copy_fail"))
|
||||
}
|
||||
})
|
||||
break
|
||||
case "json":
|
||||
ClipboardSetText(encodeURIComponent(JSON.stringify(row))).then((is: boolean) => {
|
||||
if (is) {
|
||||
window?.$message?.success("复制成功")
|
||||
window?.$message?.success(t("common.copy_success"))
|
||||
} else {
|
||||
window?.$message?.error("复制失败")
|
||||
window?.$message?.error(t("common.copy_fail"))
|
||||
}
|
||||
})
|
||||
break
|
||||
@@ -362,7 +389,7 @@ const batchDown = async () => {
|
||||
return
|
||||
}
|
||||
if (!store.globalConfig.SaveDirectory) {
|
||||
window?.$message?.error("请设置保存位置")
|
||||
window?.$message?.error(t("index.save_path_empty"))
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < data.value.length; i++) {
|
||||
@@ -373,16 +400,16 @@ const batchDown = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const batchImport = ()=>{
|
||||
const batchImport = () => {
|
||||
if (checkedRowKeysValue.value.length <= 0) {
|
||||
window?.$message?.error('请选择需要导出的数据')
|
||||
window?.$message?.error(t("index.use_data"))
|
||||
return
|
||||
}
|
||||
if (!store.globalConfig.SaveDirectory) {
|
||||
window?.$message?.error("请设置保存目录")
|
||||
window?.$message?.error(t("index.save_path_empty"))
|
||||
return
|
||||
}
|
||||
loadingText.value = "导出中"
|
||||
loadingText.value = t("common.loading")
|
||||
loading.value = true
|
||||
let jsonData = []
|
||||
for (let i = 0; i < data.value.length; i++) {
|
||||
@@ -394,8 +421,8 @@ const batchImport = ()=>{
|
||||
window?.$message?.error(res.message)
|
||||
return
|
||||
}
|
||||
window?.$message?.success("导出成功")
|
||||
window?.$message?.info("文件路径:" + res.data?.file_name, {
|
||||
window?.$message?.success(t("index.import_success"))
|
||||
window?.$message?.info(t("index.save_path") + ":" + res.data?.file_name, {
|
||||
duration: 5000
|
||||
})
|
||||
})
|
||||
@@ -424,14 +451,17 @@ async function checkVariable() {
|
||||
|
||||
const download = (row: appType.MediaInfo, index: number) => {
|
||||
if (!store.globalConfig.SaveDirectory) {
|
||||
window?.$message?.error("请设置保存位置")
|
||||
window?.$message?.error(t("index.save_path_empty"))
|
||||
return
|
||||
}
|
||||
loadingText.value = "ready"
|
||||
loading.value = true
|
||||
downIndex.value = index
|
||||
if (row.DecodeKey) {
|
||||
appApi.download({...row, decodeStr: uint8ArrayToBase64(getDecryptionArray(row.DecodeKey))}).then((res: appType.Res) => {
|
||||
appApi.download({
|
||||
...row,
|
||||
decodeStr: uint8ArrayToBase64(getDecryptionArray(row.DecodeKey))
|
||||
}).then((res: appType.Res) => {
|
||||
if (res.code === 0) {
|
||||
loading.value = false
|
||||
window?.$message?.error(res.message)
|
||||
@@ -448,27 +478,18 @@ const download = (row: appType.MediaInfo, index: number) => {
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
appApi.openSystemProxy().then((res: appType.Res) => {
|
||||
if (res.code === 0 ){
|
||||
if (store.envInfo.platform === "darwin") {
|
||||
showPassword.value = true
|
||||
return
|
||||
}
|
||||
window?.$message?.error(res.message)
|
||||
store.openProxy().then((res: appType.Res) => {
|
||||
if (res.code === 1) {
|
||||
return
|
||||
}
|
||||
store.updateProxyStatus(res.data)
|
||||
if (store.envInfo.platform === "darwin" || store.envInfo.platform === "linux") {
|
||||
showPassword.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
appApi.unsetSystemProxy().then((res: appType.Res) => {
|
||||
if (res.code === 0 ){
|
||||
window?.$message?.error(res.message)
|
||||
return
|
||||
}
|
||||
store.updateProxyStatus(res.data)
|
||||
})
|
||||
store.unsetProxy()
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
@@ -479,7 +500,7 @@ const clear = () => {
|
||||
|
||||
const decodeWxFile = (row: appType.MediaInfo, index: number) => {
|
||||
if (!row.DecodeKey) {
|
||||
window?.$message?.error("无法解密")
|
||||
window?.$message?.error(t("index.video_decode_no"))
|
||||
return
|
||||
}
|
||||
appApi.openFileDialog().then((res: appType.Res) => {
|
||||
@@ -488,7 +509,7 @@ const decodeWxFile = (row: appType.MediaInfo, index: number) => {
|
||||
return
|
||||
}
|
||||
if (res.data.file) {
|
||||
loadingText.value = "解密中"
|
||||
loadingText.value = t("index.video_decode_loading")
|
||||
loading.value = true
|
||||
appApi.wxFileDecode({
|
||||
...row,
|
||||
@@ -503,13 +524,17 @@ const decodeWxFile = (row: appType.MediaInfo, index: number) => {
|
||||
data.value[index].SavePath = res.data.save_path
|
||||
data.value[index].Status = "done"
|
||||
localStorage.setItem("resources-data", JSON.stringify(data.value))
|
||||
window?.$message?.success("解密成功")
|
||||
window?.$message?.success(t("index.video_decode_success"))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleImport = (content: string)=>{
|
||||
const handleImport = (content: string) => {
|
||||
if (!content) {
|
||||
// window?.$message?.error(t("view_index.import_empty"))
|
||||
return
|
||||
}
|
||||
content.split("\n").forEach((line, index) => {
|
||||
try {
|
||||
let res = JSON.parse(decodeURIComponent(line))
|
||||
@@ -519,7 +544,7 @@ const handleImport = (content: string)=>{
|
||||
res.Status = "ready"
|
||||
data.value.unshift(res)
|
||||
}
|
||||
}catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
});
|
||||
@@ -527,13 +552,14 @@ const handleImport = (content: string)=>{
|
||||
showImport.value = false
|
||||
}
|
||||
|
||||
const handlePassword = (password: string)=>{
|
||||
appApi.setSystemPassword({password: password}).then((res: appType.Res)=>{
|
||||
const handlePassword = (password: string, isCache: boolean) => {
|
||||
appApi.setSystemPassword({password: password, isCache: isCache}).then((res: appType.Res) => {
|
||||
if (res.code === 0) {
|
||||
window?.$message?.error(res.message)
|
||||
return
|
||||
}
|
||||
open()
|
||||
showPassword.value = false
|
||||
store.openProxy()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="h-full relative p-5 overflow-y-auto [&::-webkit-scrollbar]:hidden">
|
||||
<div class="h-full relative p-5 overflow-y-auto [&::-webkit-scrollbar]:hidden" :key="renderKey">
|
||||
<NForm
|
||||
:model="formValue"
|
||||
size="medium"
|
||||
@@ -17,7 +17,7 @@
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
如果不清楚请保持默认,修改后请重启软件
|
||||
{{ t("setting.restart_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
@@ -29,12 +29,12 @@
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
如果不清楚保持默认,修改后请重启软件
|
||||
{{ t("setting.restart_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="上游代理" path="UpstreamProxy">
|
||||
<NInput v-model:value="formValue.UpstreamProxy" placeholder="例如: http://127.0.0.1:7890"/>
|
||||
<NFormItem :label="t('setting.upstream_proxy')" path="UpstreamProxy">
|
||||
<NInput v-model:value="formValue.UpstreamProxy" placeholder="http://127.0.0.1:7890"/>
|
||||
<NSwitch v-model:value="formValue.OpenProxy" class="ml-1"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
@@ -42,54 +42,17 @@
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
可结合其他代理工具,用于访问国外网站、以及正常网络无法访问的资源
|
||||
{{ t("setting.upstream_proxy_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<NFormItem label="下载代理" path="DownloadProxy">
|
||||
<NSwitch v-model:value="formValue.DownloadProxy"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
进行下载时使用代理请求
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
<NFormItem :label="t('setting.save_dir')" path="SaveDirectory">
|
||||
<NInput :value="formValue.SaveDirectory" :placeholder="t('setting.save_dir')"/>
|
||||
<NButton strong secondary type="primary" @click="selectDir" class="ml-1">{{ t('common.select') }}</NButton>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="自动拦截" path="AutoProxy">
|
||||
<NSwitch v-model:value="formValue.AutoProxy"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
打开软件时自动启用拦截
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="全量拦截" path="WxAction">
|
||||
<NSwitch v-model:value="formValue.WxAction"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
微信视频号是否全量拦截,否:只拦截视频详情
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<NFormItem label="保存位置" path="SaveDirectory">
|
||||
<NInput :value="formValue.SaveDirectory" placeholder="保存位置"/>
|
||||
<NButton strong secondary type="primary" @click="selectDir" class="ml-1">选择</NButton>
|
||||
</NFormItem>
|
||||
<NFormItem label="文件命名" path="FilenameLen">
|
||||
<div class="grid grid-cols-2">
|
||||
<NFormItem :label="t('setting.filename_rules')" path="FilenameLen">
|
||||
<NInputNumber v-model:value="formValue.FilenameLen" :min="0" :max="9999" placeholder="0"/>
|
||||
<NSwitch v-model:value="formValue.FilenameTime" class="ml-1"></NSwitch>
|
||||
<NTooltip trigger="hover">
|
||||
@@ -98,20 +61,11 @@
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
输入框控制文件命名的长度(不含时间、0为无效),开关控制文件末尾是否添加时间标识
|
||||
{{ t("setting.filename_rules_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<NFormItem label="主题" path="theme">
|
||||
<NRadioGroup v-model:value="formValue.Theme" name="theme">
|
||||
<NRadio value="lightTheme">浅色</NRadio>
|
||||
<NRadio value="darkTheme">深色</NRadio>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="清晰度" path="Quality">
|
||||
<NFormItem :label="t('setting.quality')" path="Quality">
|
||||
<NSelect v-model:value="formValue.Quality" :options="options"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
@@ -119,11 +73,51 @@
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
视频号有效
|
||||
{{ t("setting.quality_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<NFormItem :label="t('setting.auto_proxy')" path="AutoProxy">
|
||||
<NSwitch v-model:value="formValue.AutoProxy"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.auto_proxy_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="连接数" path="TaskNumber">
|
||||
<NFormItem :label="t('setting.full_intercept')" path="WxAction">
|
||||
<NSwitch v-model:value="formValue.WxAction"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.full_intercept_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<NFormItem :label="t('setting.download_proxy')" path="DownloadProxy">
|
||||
<NSwitch v-model:value="formValue.DownloadProxy"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.download_proxy_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="t('setting.connections')" path="TaskNumber">
|
||||
<NInputNumber v-model:value="formValue.TaskNumber" :min="2" :max="64"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
@@ -131,7 +125,7 @@
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
如不清楚请保持默认,通常CPU核心数*2,用于分片下载
|
||||
{{ t("setting.connections_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
</div>
|
||||
@@ -144,7 +138,7 @@
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
如不清楚请保持默认
|
||||
{{ t("setting.user_agent_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
@@ -156,16 +150,16 @@
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
定义下载时可使用的header参数,逗号分割
|
||||
{{ t("setting.use_headers_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="拦截规则" path="MimeMap">
|
||||
<NFormItem :label="t('setting.mime_map')" path="MimeMap">
|
||||
<NInput
|
||||
v-model:value="MimeMap"
|
||||
type="textarea"
|
||||
rows="11"
|
||||
placeholder='{"content-type": { "Type": "分类名称","Suffix": "后缀"}}'
|
||||
placeholder='{"video/mp4": { "Type": "video","Suffix": ".mp4"}}'
|
||||
/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
@@ -173,7 +167,7 @@
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
拦截规则JSON配置,不清楚请勿改动
|
||||
{{ t("setting.mime_map_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
@@ -186,31 +180,22 @@ import {ref, watch} from "vue"
|
||||
import {useIndexStore} from "@/stores"
|
||||
import type {appType} from "@/types/app"
|
||||
import appApi from "@/api/app"
|
||||
import {computed} from "vue"
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const {t} = useI18n()
|
||||
const store = useIndexStore()
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: 0,
|
||||
label: "默认(推荐)"
|
||||
}, {
|
||||
value: 1,
|
||||
label: "超清"
|
||||
}, {
|
||||
value: 2,
|
||||
label: "高画质"
|
||||
}, {
|
||||
value: 3,
|
||||
label: "中画质"
|
||||
}, {
|
||||
value: 4,
|
||||
label: "低画质"
|
||||
}
|
||||
]
|
||||
const options = computed(() =>
|
||||
t("setting.quality_value")
|
||||
.split(",")
|
||||
.map((value, index) => ({ value: index, label: value }))
|
||||
)
|
||||
|
||||
const formValue = ref<appType.Config>(Object.assign({}, store.globalConfig))
|
||||
|
||||
const MimeMap = ref(formValue.value.MimeMap ? JSON.stringify(formValue.value.MimeMap, null, 2) : "")
|
||||
const renderKey = ref(999)
|
||||
|
||||
watch(formValue.value, () => {
|
||||
store.setConfig(formValue.value)
|
||||
@@ -228,6 +213,11 @@ watch(() => {
|
||||
formValue.value.Theme = store.globalConfig.Theme
|
||||
})
|
||||
|
||||
watch(() => store.globalConfig.Locale, () => {
|
||||
formValue.value.Locale = store.globalConfig.Locale
|
||||
renderKey.value++
|
||||
})
|
||||
|
||||
const selectDir = () => {
|
||||
appApi.openDirectoryDialog().then((res: any) => {
|
||||
if (res.code === 1) {
|
||||
|
||||
@@ -6,12 +6,7 @@ import Components from 'unplugin-vue-components/vite'
|
||||
import {NaiveUiResolver} from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
// export default defineConfig({
|
||||
// plugins: [vue()]
|
||||
// })
|
||||
export default defineConfig((env) => {
|
||||
const viteEnv = loadEnv(env.mode, process.cwd())
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
vue(),
|
||||
|
||||
7
frontend/wailsjs/go/core/Bind.d.ts
vendored
Executable file
7
frontend/wailsjs/go/core/Bind.d.ts
vendored
Executable file
@@ -0,0 +1,7 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {core} from '../models';
|
||||
|
||||
export function AppInfo():Promise<core.ResponseData>;
|
||||
|
||||
export function Config():Promise<core.ResponseData>;
|
||||
11
frontend/wailsjs/go/core/Bind.js
Executable file
11
frontend/wailsjs/go/core/Bind.js
Executable file
@@ -0,0 +1,11 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function AppInfo() {
|
||||
return window['go']['core']['Bind']['AppInfo']();
|
||||
}
|
||||
|
||||
export function Config() {
|
||||
return window['go']['core']['Bind']['Config']();
|
||||
}
|
||||
21
frontend/wailsjs/go/models.ts
Executable file
21
frontend/wailsjs/go/models.ts
Executable file
@@ -0,0 +1,21 @@
|
||||
export namespace core {
|
||||
|
||||
export class ResponseData {
|
||||
code: number;
|
||||
message: string;
|
||||
data: any;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ResponseData(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.code = source["code"];
|
||||
this.message = source["message"];
|
||||
this.data = source["data"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -10,6 +10,7 @@ require (
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68
|
||||
github.com/wailsapp/wails/v2 v2.10.1
|
||||
golang.org/x/net v0.35.0
|
||||
golang.org/x/sys v0.30.0
|
||||
)
|
||||
|
||||
@@ -37,6 +38,5 @@ require (
|
||||
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
|
||||
1
go.sum
1
go.sum
@@ -90,3 +90,4 @@ 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=
|
||||
|
||||
|
||||
11
main.go
11
main.go
@@ -29,6 +29,7 @@ var wailsJson string
|
||||
func main() {
|
||||
// Create an instance of the app structure
|
||||
app := core.GetApp(assets, wailsJson)
|
||||
bind := core.NewBind()
|
||||
isMac := runtime.GOOS == "darwin"
|
||||
// menu
|
||||
appMenu := menu.NewMenu()
|
||||
@@ -41,10 +42,10 @@ func main() {
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: app.AppName,
|
||||
Width: 1024,
|
||||
Width: 1280,
|
||||
MinWidth: 960,
|
||||
Height: 768,
|
||||
MinHeight: 640,
|
||||
Height: 800,
|
||||
MinHeight: 600,
|
||||
Frameless: !isMac,
|
||||
Menu: appMenu,
|
||||
EnableDefaultContextMenu: true,
|
||||
@@ -68,7 +69,9 @@ func main() {
|
||||
OnShutdown: func(ctx context.Context) {
|
||||
app.OnExit()
|
||||
},
|
||||
Bind: []interface{}{},
|
||||
Bind: []interface{}{
|
||||
bind,
|
||||
},
|
||||
Mac: &mac.Options{
|
||||
TitleBar: mac.TitleBarHiddenInset(),
|
||||
About: &mac.AboutInfo{
|
||||
|
||||
Reference in New Issue
Block a user