mirror of
https://github.com/putyy/res-downloader.git
synced 2026-01-13 06:54:56 +08:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
820a2671cf | ||
|
|
3b4443110e | ||
|
|
ffd5b29030 | ||
|
|
779f56dd91 | ||
|
|
2beecdade2 | ||
|
|
bca2e110de | ||
|
|
8d55a86c06 | ||
|
|
f61199bed6 | ||
|
|
2d75bbb5c3 | ||
|
|
55d3f06cb6 | ||
|
|
1809847b8a | ||
|
|
da8e8d9641 | ||
|
|
ead622d95e | ||
|
|
c47fcba36b | ||
|
|
54c0da081c | ||
|
|
4bead0752d | ||
|
|
bd7828b73f | ||
|
|
4706540475 | ||
|
|
0daec66fa6 | ||
|
|
379ae22db7 | ||
|
|
2d1fc4273a | ||
|
|
55b67a0efa | ||
|
|
ace4625a27 | ||
|
|
6fb0474154 | ||
|
|
86ef0d3331 | ||
|
|
cfa9d4929f | ||
|
|
3c40ada451 | ||
|
|
9ec4eca558 | ||
|
|
f295fb6b64 | ||
|
|
3dc4322258 | ||
|
|
3910d0ffb0 | ||
|
|
af75f1ce4f | ||
|
|
a016465bea | ||
|
|
fd5e289c87 | ||
|
|
2a2ca7eb4e | ||
|
|
a7ec61b8e2 | ||
|
|
84c882d573 | ||
|
|
567eb2903d | ||
|
|
31073eb57e | ||
|
|
5613e21138 | ||
|
|
0a516b2f3c | ||
|
|
d3d8983307 | ||
|
|
59e2b1b267 | ||
|
|
4ebbc2347f | ||
|
|
25ab8edd20 | ||
|
|
f4bc3c7b53 | ||
|
|
ec89dc362f | ||
|
|
821d9949ab | ||
|
|
e97120cf06 | ||
|
|
67f11d2b93 | ||
|
|
3be6b8cd91 | ||
|
|
db3ff8e0d2 | ||
|
|
405d0bbdb2 | ||
|
|
6c21e37ce4 | ||
|
|
b74e2a2bf6 | ||
|
|
deb3e83082 | ||
|
|
ff90c4ff03 | ||
|
|
7f3d63532c | ||
|
|
bd2fa75cde | ||
|
|
27f9fb0def | ||
|
|
7a07456b2f | ||
|
|
3bce1f0332 | ||
|
|
5a92d7beb7 | ||
|
|
f28cb69826 | ||
|
|
14d18ad310 | ||
|
|
ee6698a8e8 | ||
|
|
7f2b99b51f |
@@ -32,7 +32,7 @@ Clean UI, easy to use, and supports a wide range of resource sniffing and downlo
|
||||
- 📘 [Online Documentation (Chinese)](https://res.putyy.com/)
|
||||
- 🧩 [Mini Version Ui Display using default browser](https://github.com/putyy/res-downloader) | [Old Electron Version Support Win7](https://github.com/putyy/res-downloader/tree/old)
|
||||
- 💬 [Join the User Group (Chinese)](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)
|
||||
> *If full, you can add WeChat `AmorousWorld` with a note “From GitHub”*
|
||||
> *If full, you can add WeChat `AmorousWorld` with a note “github”*
|
||||
|
||||
## 🧩 Download Links
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
- 📘 [在线文档](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`,请备注“来源”*
|
||||
> *群满时可加微信 `AmorousWorld`,请备注“github”*
|
||||
|
||||
## 🧩 下载地址
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
## 🖼️ 预览
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
BIN
build/.DS_Store
vendored
BIN
build/.DS_Store
vendored
Binary file not shown.
@@ -24,7 +24,7 @@ wails build -platform "linux/amd64" -s -skipbindings
|
||||
|
||||
# 打包debian
|
||||
cp build/bin/res-downloader build/linux/Debian/usr/local/bin/
|
||||
echo "$(cat build/linux/Debian/DEBIAN/.control | sed -e "s/{{Version}}/$(jq -r '.info.productVersion' wails.json)/g")" > build/linux/Debian/DEBIAN/control
|
||||
echo "$(cat build/linux/Debian/DEBIAN/.control | sed -e "s/{{Version}}/$(jq -r '.info.productVersion' wails.json)/g" -e "s/{{Architecture}}/amd64/g")" > build/linux/Debian/DEBIAN/control
|
||||
dpkg-deb --build ./build/linux/Debian build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64.deb
|
||||
|
||||
# 打包AppImage
|
||||
@@ -64,7 +64,7 @@ wails build -platform "linux/arm64" -s -skipbindings
|
||||
|
||||
# 打包debian
|
||||
cp build/bin/res-downloader build/linux/Debian/usr/local/bin/
|
||||
echo "$(cat build/linux/Debian/DEBIAN/.control | sed -e "s/{{Version}}/$(jq -r '.info.productVersion' wails.json)/g")" > build/linux/Debian/DEBIAN/control
|
||||
echo "$(cat build/linux/Debian/DEBIAN/.control | sed -e "s/{{Version}}/$(jq -r '.info.productVersion' wails.json)/g" -e "s/{{Architecture}}/arm64/g")" > build/linux/Debian/DEBIAN/control
|
||||
dpkg-deb --build ./build/linux/Debian build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_arm64.deb
|
||||
|
||||
mv -f build/bin/res-downloader build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_arm64
|
||||
|
||||
@@ -2,7 +2,7 @@ Package: res-downloader
|
||||
Version: {{Version}}
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Architecture: {{Architecture}}
|
||||
Depends: libwebkit2gtk-4.0-37
|
||||
Maintainer: putyy@qq.com
|
||||
Homepage: https://github.com/putyy/res-downloader
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
!define INFO_PRODUCTNAME "res-downloader"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "3.0.6"
|
||||
!define INFO_PRODUCTVERSION "3.1.1"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "Copyright © 2023"
|
||||
|
||||
28
core/app.go
28
core/app.go
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/vrischmann/userdir"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"res-downloader/core/shared"
|
||||
@@ -25,6 +26,7 @@ type App struct {
|
||||
PublicCrt []byte `json:"-"`
|
||||
PrivateKey []byte `json:"-"`
|
||||
IsProxy bool `json:"IsProxy"`
|
||||
IsReset bool `json:"-"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -51,6 +53,7 @@ func GetApp(assets embed.FS, wjs string) *App {
|
||||
Version: version,
|
||||
Description: "res-downloader是一款集网络资源嗅探 + 高速下载功能于一体的软件,高颜值、高性能和多样化,提供个人用户下载自己上传到各大平台的网络资源功能!",
|
||||
Copyright: "Copyright © 2023~" + strconv.Itoa(time.Now().Year()),
|
||||
IsReset: false,
|
||||
PublicCrt: []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDwzCCAqugAwIBAgIUFAnC6268dp/z1DR9E1UepiWgWzkwDQYJKoZIhvcNAQEL
|
||||
BQAwcDELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUNob25ncWluZzESMBAGA1UEBwwJ
|
||||
@@ -129,6 +132,10 @@ func (a *App) Startup(ctx context.Context) {
|
||||
func (a *App) OnExit() {
|
||||
a.UnsetSystemProxy()
|
||||
globalLogger.Close()
|
||||
if appOnce.IsReset {
|
||||
err := a.ResetApp()
|
||||
fmt.Println("err:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) installCert() (string, error) {
|
||||
@@ -179,3 +186,24 @@ func (a *App) lock() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) ResetApp() error {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exePath, err = filepath.Abs(exePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = os.Remove(filepath.Join(appOnce.UserDir, "install.lock"))
|
||||
_ = os.Remove(filepath.Join(appOnce.UserDir, "pass.cache"))
|
||||
_ = os.Remove(filepath.Join(appOnce.UserDir, "config.json"))
|
||||
_ = os.Remove(filepath.Join(appOnce.UserDir, "cert.crt"))
|
||||
|
||||
cmd := exec.Command(exePath)
|
||||
cmd.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
type Bind struct {
|
||||
}
|
||||
|
||||
@@ -14,3 +18,8 @@ func (b *Bind) Config() *ResponseData {
|
||||
func (b *Bind) AppInfo() *ResponseData {
|
||||
return httpServerOnce.buildResp(1, "ok", appOnce)
|
||||
}
|
||||
|
||||
func (b *Bind) ResetApp() {
|
||||
appOnce.IsReset = true
|
||||
runtime.Quit(appOnce.ctx)
|
||||
}
|
||||
|
||||
264
core/config.go
264
core/config.go
@@ -2,8 +2,10 @@ package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
@@ -30,8 +32,10 @@ type Config struct {
|
||||
AutoProxy bool `json:"AutoProxy"`
|
||||
WxAction bool `json:"WxAction"`
|
||||
TaskNumber int `json:"TaskNumber"`
|
||||
DownNumber int `json:"DownNumber"`
|
||||
UserAgent string `json:"UserAgent"`
|
||||
UseHeaders string `json:"UseHeaders"`
|
||||
InsertTail bool `json:"InsertTail"`
|
||||
MimeMap map[string]MimeInfo `json:"MimeMap"`
|
||||
}
|
||||
|
||||
@@ -40,117 +44,167 @@ var (
|
||||
)
|
||||
|
||||
func initConfig() *Config {
|
||||
if globalConfig == nil {
|
||||
def := `
|
||||
{
|
||||
"Host": "127.0.0.1",
|
||||
"Port": "8899",
|
||||
"Theme": "lightTheme",
|
||||
"Locale": "zh",
|
||||
"Quality": 0,
|
||||
"SaveDirectory": "",
|
||||
"FilenameLen": 0,
|
||||
"FilenameTime": true,
|
||||
"UpstreamProxy": "",
|
||||
"OpenProxy": false,
|
||||
"DownloadProxy": false,
|
||||
"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",
|
||||
"UseHeaders": "User-Agent,Referer,Authorization,Cookie",
|
||||
"MimeMap": {
|
||||
"image/png": { "Type": "image", "Suffix": ".png" },
|
||||
"image/webp": { "Type": "image", "Suffix": ".webp" },
|
||||
"image/jpeg": { "Type": "image", "Suffix": ".jpeg" },
|
||||
"image/jpg": { "Type": "image", "Suffix": ".jpg" },
|
||||
"image/gif": { "Type": "image", "Suffix": ".gif" },
|
||||
"image/avif": { "Type": "image", "Suffix": ".avif" },
|
||||
"image/bmp": { "Type": "image", "Suffix": ".bmp" },
|
||||
"image/tiff": { "Type": "image", "Suffix": ".tiff" },
|
||||
"image/heic": { "Type": "image", "Suffix": ".heic" },
|
||||
"image/x-icon": { "Type": "image", "Suffix": ".ico" },
|
||||
"image/svg+xml": { "Type": "image", "Suffix": ".svg" },
|
||||
"image/vnd.adobe.photoshop": { "Type": "image", "Suffix": ".psd" },
|
||||
"image/jp2": { "Type": "image", "Suffix": ".jp2" },
|
||||
"image/jpeg2000": { "Type": "image", "Suffix": ".jp2" },
|
||||
"image/apng": { "Type": "image", "Suffix": ".apng" },
|
||||
"audio/mpeg": { "Type": "audio", "Suffix": ".mp3" },
|
||||
"audio/mp3": { "Type": "audio", "Suffix": ".mp3" },
|
||||
"audio/wav": { "Type": "audio", "Suffix": ".wav" },
|
||||
"audio/aiff": { "Type": "audio", "Suffix": ".aiff" },
|
||||
"audio/x-aiff": { "Type": "audio", "Suffix": ".aiff" },
|
||||
"audio/aac": { "Type": "audio", "Suffix": ".aac" },
|
||||
"audio/ogg": { "Type": "audio", "Suffix": ".ogg" },
|
||||
"audio/flac": { "Type": "audio", "Suffix": ".flac" },
|
||||
"audio/midi": { "Type": "audio", "Suffix": ".mid" },
|
||||
"audio/x-midi": { "Type": "audio", "Suffix": ".mid" },
|
||||
"audio/x-ms-wma": { "Type": "audio", "Suffix": ".wma" },
|
||||
"audio/opus": { "Type": "audio", "Suffix": ".opus" },
|
||||
"audio/webm": { "Type": "audio", "Suffix": ".webm" },
|
||||
"audio/mp4": { "Type": "audio", "Suffix": ".m4a" },
|
||||
"audio/amr": { "Type": "audio", "Suffix": ".amr" },
|
||||
"video/mp4": { "Type": "video", "Suffix": ".mp4" },
|
||||
"video/webm": { "Type": "video", "Suffix": ".webm" },
|
||||
"video/ogg": { "Type": "video", "Suffix": ".ogv" },
|
||||
"video/x-msvideo": { "Type": "video", "Suffix": ".avi" },
|
||||
"video/mpeg": { "Type": "video", "Suffix": ".mpeg" },
|
||||
"video/quicktime": { "Type": "video", "Suffix": ".mov" },
|
||||
"video/x-ms-wmv": { "Type": "video", "Suffix": ".wmv" },
|
||||
"video/3gpp": { "Type": "video", "Suffix": ".3gp" },
|
||||
"video/x-matroska": { "Type": "video", "Suffix": ".mkv" },
|
||||
"audio/video": { "Type": "live", "Suffix": ".flv" },
|
||||
"video/x-flv": { "Type": "live", "Suffix": ".flv" },
|
||||
"application/dash+xml": { "Type": "live", "Suffix": ".mpd" },
|
||||
"application/vnd.apple.mpegurl": { "Type": "m3u8", "Suffix": ".m3u8" },
|
||||
"application/x-mpegurl": { "Type": "m3u8", "Suffix": ".m3u8" },
|
||||
"application/x-mpeg": { "Type": "m3u8", "Suffix": ".m3u8" },
|
||||
"application/pdf": { "Type": "pdf", "Suffix": ".pdf" },
|
||||
"application/vnd.ms-powerpoint": { "Type": "ppt", "Suffix": ".ppt" },
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": { "Type": "ppt", "Suffix": ".pptx" },
|
||||
"application/vnd.ms-excel": { "Type": "xls", "Suffix": ".xls" },
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": { "Type": "xls", "Suffix": ".xlsx" },
|
||||
"text/csv": { "Type": "xls", "Suffix": ".csv" },
|
||||
"application/msword": { "Type": "doc", "Suffix": ".doc" },
|
||||
"application/rtf": { "Type": "doc", "Suffix": ".rtf" },
|
||||
"text/rtf": { "Type": "doc", "Suffix": ".rtf" },
|
||||
"application/vnd.oasis.opendocument.text": { "Type": "doc", "Suffix": ".odt" },
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": { "Type": "doc", "Suffix": ".docx" },
|
||||
"font/woff": { "Type": "font", "Suffix": ".woff" }
|
||||
if globalConfig != nil {
|
||||
return globalConfig
|
||||
}
|
||||
|
||||
defaultConfig := &Config{
|
||||
Theme: "lightTheme",
|
||||
Locale: "zh",
|
||||
Host: "127.0.0.1",
|
||||
Port: "8899",
|
||||
Quality: 0,
|
||||
SaveDirectory: getDefaultDownloadDir(),
|
||||
FilenameLen: 0,
|
||||
FilenameTime: true,
|
||||
UpstreamProxy: "",
|
||||
OpenProxy: false,
|
||||
DownloadProxy: false,
|
||||
AutoProxy: false,
|
||||
WxAction: true,
|
||||
TaskNumber: runtime.NumCPU() * 2,
|
||||
DownNumber: 3,
|
||||
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
|
||||
UseHeaders: "User-Agent,Referer,Authorization,Cookie",
|
||||
InsertTail: true,
|
||||
MimeMap: getDefaultMimeMap(),
|
||||
}
|
||||
|
||||
rawDefaults, err := json.Marshal(defaultConfig)
|
||||
if err != nil {
|
||||
return globalConfig
|
||||
}
|
||||
|
||||
storage := NewStorage("config.json", rawDefaults)
|
||||
defaultConfig.storage = storage
|
||||
globalConfig = defaultConfig
|
||||
|
||||
data, err := storage.Load()
|
||||
if err != nil {
|
||||
globalLogger.Esg(err, "load config failed, using defaults")
|
||||
return globalConfig
|
||||
}
|
||||
|
||||
var cacheMap map[string]interface{}
|
||||
if err := json.Unmarshal(data, &cacheMap); err != nil {
|
||||
globalLogger.Esg(err, "parse cached config failed, using defaults")
|
||||
return globalConfig
|
||||
}
|
||||
|
||||
var defaultMap map[string]interface{}
|
||||
defaultBytes, _ := json.Marshal(defaultConfig)
|
||||
_ = json.Unmarshal(defaultBytes, &defaultMap)
|
||||
|
||||
for k, v := range cacheMap {
|
||||
if _, ok := defaultMap[k]; ok {
|
||||
defaultMap[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
finalBytes, err := json.Marshal(defaultMap)
|
||||
if err != nil {
|
||||
globalLogger.Esg(err, "marshal merged config failed")
|
||||
return globalConfig
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(finalBytes, globalConfig); err != nil {
|
||||
globalLogger.Esg(err, "unmarshal merged config to struct failed")
|
||||
}
|
||||
|
||||
return globalConfig
|
||||
}
|
||||
|
||||
func getDefaultMimeMap() map[string]MimeInfo {
|
||||
return map[string]MimeInfo{
|
||||
"image/png": {Type: "image", Suffix: ".png"},
|
||||
"image/webp": {Type: "image", Suffix: ".webp"},
|
||||
"image/jpeg": {Type: "image", Suffix: ".jpeg"},
|
||||
"image/jpg": {Type: "image", Suffix: ".jpg"},
|
||||
"image/gif": {Type: "image", Suffix: ".gif"},
|
||||
"image/avif": {Type: "image", Suffix: ".avif"},
|
||||
"image/bmp": {Type: "image", Suffix: ".bmp"},
|
||||
"image/tiff": {Type: "image", Suffix: ".tiff"},
|
||||
"image/heic": {Type: "image", Suffix: ".heic"},
|
||||
"image/x-icon": {Type: "image", Suffix: ".ico"},
|
||||
"image/svg+xml": {Type: "image", Suffix: ".svg"},
|
||||
"image/vnd.adobe.photoshop": {Type: "image", Suffix: ".psd"},
|
||||
"image/jp2": {Type: "image", Suffix: ".jp2"},
|
||||
"image/jpeg2000": {Type: "image", Suffix: ".jp2"},
|
||||
"image/apng": {Type: "image", Suffix: ".apng"},
|
||||
"audio/mpeg": {Type: "audio", Suffix: ".mp3"},
|
||||
"audio/mp3": {Type: "audio", Suffix: ".mp3"},
|
||||
"audio/wav": {Type: "audio", Suffix: ".wav"},
|
||||
"audio/aiff": {Type: "audio", Suffix: ".aiff"},
|
||||
"audio/x-aiff": {Type: "audio", Suffix: ".aiff"},
|
||||
"audio/aac": {Type: "audio", Suffix: ".aac"},
|
||||
"audio/ogg": {Type: "audio", Suffix: ".ogg"},
|
||||
"audio/flac": {Type: "audio", Suffix: ".flac"},
|
||||
"audio/midi": {Type: "audio", Suffix: ".mid"},
|
||||
"audio/x-midi": {Type: "audio", Suffix: ".mid"},
|
||||
"audio/x-ms-wma": {Type: "audio", Suffix: ".wma"},
|
||||
"audio/opus": {Type: "audio", Suffix: ".opus"},
|
||||
"audio/webm": {Type: "audio", Suffix: ".webm"},
|
||||
"audio/mp4": {Type: "audio", Suffix: ".m4a"},
|
||||
"audio/amr": {Type: "audio", Suffix: ".amr"},
|
||||
"video/mp4": {Type: "video", Suffix: ".mp4"},
|
||||
"video/webm": {Type: "video", Suffix: ".webm"},
|
||||
"video/ogg": {Type: "video", Suffix: ".ogv"},
|
||||
"video/x-msvideo": {Type: "video", Suffix: ".avi"},
|
||||
"video/mpeg": {Type: "video", Suffix: ".mpeg"},
|
||||
"video/quicktime": {Type: "video", Suffix: ".mov"},
|
||||
"video/x-ms-wmv": {Type: "video", Suffix: ".wmv"},
|
||||
"video/3gpp": {Type: "video", Suffix: ".3gp"},
|
||||
"video/x-matroska": {Type: "video", Suffix: ".mkv"},
|
||||
"audio/video": {Type: "live", Suffix: ".flv"},
|
||||
"video/x-flv": {Type: "live", Suffix: ".flv"},
|
||||
"application/dash+xml": {Type: "live", Suffix: ".mpd"},
|
||||
"application/vnd.apple.mpegurl": {Type: "m3u8", Suffix: ".m3u8"},
|
||||
"application/x-mpegurl": {Type: "m3u8", Suffix: ".m3u8"},
|
||||
"application/x-mpeg": {Type: "m3u8", Suffix: ".m3u8"},
|
||||
"application/pdf": {Type: "pdf", Suffix: ".pdf"},
|
||||
"application/vnd.ms-powerpoint": {Type: "ppt", Suffix: ".ppt"},
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": {Type: "ppt", Suffix: ".pptx"},
|
||||
"application/vnd.ms-excel": {Type: "xls", Suffix: ".xls"},
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {Type: "xls", Suffix: ".xlsx"},
|
||||
"text/csv": {Type: "xls", Suffix: ".csv"},
|
||||
"application/msword": {Type: "doc", Suffix: ".doc"},
|
||||
"application/rtf": {Type: "doc", Suffix: ".rtf"},
|
||||
"text/rtf": {Type: "doc", Suffix: ".rtf"},
|
||||
"application/vnd.oasis.opendocument.text": {Type: "doc", Suffix: ".odt"},
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {Type: "doc", Suffix: ".docx"},
|
||||
"font/woff": {Type: "font", Suffix: ".woff"},
|
||||
}
|
||||
}
|
||||
`
|
||||
def = strings.ReplaceAll(def, "__TaskNumber__", strconv.Itoa(runtime.NumCPU()*2))
|
||||
globalConfig = &Config{
|
||||
storage: NewStorage("config.json", []byte(def)),
|
||||
}
|
||||
|
||||
defaultMap := make(map[string]interface{})
|
||||
_ = json.Unmarshal([]byte(def), &defaultMap)
|
||||
func getDefaultDownloadDir() string {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
data, err := globalConfig.storage.Load()
|
||||
if err == nil {
|
||||
var loadedMap map[string]interface{}
|
||||
_ = json.Unmarshal(data, &loadedMap)
|
||||
homeDir := usr.HomeDir
|
||||
var downloadDir string
|
||||
|
||||
for key, val := range defaultMap {
|
||||
if _, ok := loadedMap[key]; !ok {
|
||||
loadedMap[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
finalBytes, _ := json.Marshal(loadedMap)
|
||||
_ = json.Unmarshal(finalBytes, &globalConfig)
|
||||
|
||||
} else {
|
||||
globalLogger.Esg(err, "load config err")
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin":
|
||||
downloadDir = filepath.Join(homeDir, "Downloads")
|
||||
case "linux":
|
||||
downloadDir = filepath.Join(homeDir, "Downloads")
|
||||
if xdgDir := os.Getenv("XDG_DOWNLOAD_DIR"); xdgDir != "" {
|
||||
downloadDir = xdgDir
|
||||
}
|
||||
}
|
||||
return globalConfig
|
||||
|
||||
if stat, err := os.Stat(downloadDir); err == nil && stat.IsDir() {
|
||||
return downloadDir
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Config) setConfig(config Config) {
|
||||
oldProxy := c.UpstreamProxy
|
||||
openProxy := c.OpenProxy
|
||||
c.Host = config.Host
|
||||
c.Port = config.Port
|
||||
c.Theme = config.Theme
|
||||
@@ -165,9 +219,11 @@ func (c *Config) setConfig(config Config) {
|
||||
c.DownloadProxy = config.DownloadProxy
|
||||
c.AutoProxy = config.AutoProxy
|
||||
c.TaskNumber = config.TaskNumber
|
||||
c.DownNumber = config.DownNumber
|
||||
c.WxAction = config.WxAction
|
||||
c.UseHeaders = config.UseHeaders
|
||||
if oldProxy != c.UpstreamProxy {
|
||||
c.InsertTail = config.InsertTail
|
||||
if oldProxy != c.UpstreamProxy || openProxy != c.OpenProxy {
|
||||
proxyOnce.setTransport()
|
||||
}
|
||||
|
||||
@@ -211,10 +267,14 @@ func (c *Config) getConfig(key string) interface{} {
|
||||
return c.AutoProxy
|
||||
case "TaskNumber":
|
||||
return c.TaskNumber
|
||||
case "DownNumber":
|
||||
return c.DownNumber
|
||||
case "WxAction":
|
||||
return c.WxAction
|
||||
case "UseHeaders":
|
||||
return c.UseHeaders
|
||||
case "InsertTail":
|
||||
return c.InsertTail
|
||||
case "MimeMap":
|
||||
mimeMux.RLock()
|
||||
defer mimeMux.RUnlock()
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"res-downloader/core/shared"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -44,20 +45,27 @@ type FileDownloader struct {
|
||||
totalTasks int
|
||||
TotalSize int64
|
||||
IsMultiPart bool
|
||||
RetryOnError bool
|
||||
Headers map[string]string
|
||||
DownloadTaskList []*DownloadTask
|
||||
progressCallback ProgressCallback
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
func NewFileDownloader(url, filename string, totalTasks int, headers map[string]string) *FileDownloader {
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
return &FileDownloader{
|
||||
Url: url,
|
||||
FileName: filename,
|
||||
totalTasks: totalTasks,
|
||||
IsMultiPart: false,
|
||||
RetryOnError: false,
|
||||
TotalSize: 0,
|
||||
Headers: headers,
|
||||
DownloadTaskList: make([]*DownloadTask, 0),
|
||||
ctx: ctx,
|
||||
cancelFunc: cancelFunc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +79,6 @@ func (fd *FileDownloader) buildClient() *http.Client {
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,10 +139,9 @@ func (fd *FileDownloader) init() error {
|
||||
|
||||
fd.TotalSize = resp.ContentLength
|
||||
if fd.TotalSize <= 0 {
|
||||
return errors.New("invalid file size")
|
||||
}
|
||||
|
||||
if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > MinPartSize {
|
||||
fd.IsMultiPart = false
|
||||
fd.TotalSize = -1
|
||||
} else if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > MinPartSize {
|
||||
fd.IsMultiPart = true
|
||||
}
|
||||
|
||||
@@ -143,13 +149,18 @@ func (fd *FileDownloader) init() error {
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("create directory failed: %w", err)
|
||||
}
|
||||
|
||||
fd.FileName = shared.GetUniqueFileName(fd.FileName)
|
||||
|
||||
fd.File, err = os.OpenFile(fd.FileName, os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file open failed: %w", err)
|
||||
}
|
||||
if err := fd.File.Truncate(fd.TotalSize); err != nil {
|
||||
fd.File.Close()
|
||||
return fmt.Errorf("file truncate failed: %w", err)
|
||||
if fd.TotalSize > 0 {
|
||||
if err := fd.File.Truncate(fd.TotalSize); err != nil {
|
||||
fd.File.Close()
|
||||
return fmt.Errorf("file truncate failed: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -182,10 +193,14 @@ func (fd *FileDownloader) createDownloadTasks() {
|
||||
}
|
||||
} else {
|
||||
fd.totalTasks = 1
|
||||
rangeEnd := int64(-1)
|
||||
if fd.TotalSize > 0 {
|
||||
rangeEnd = fd.TotalSize - 1
|
||||
}
|
||||
fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{
|
||||
taskID: 0,
|
||||
rangeStart: 0,
|
||||
rangeEnd: fd.TotalSize - 1,
|
||||
rangeEnd: rangeEnd,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -233,6 +248,15 @@ func (fd *FileDownloader) startDownload() error {
|
||||
}
|
||||
|
||||
if len(errArr) > 0 {
|
||||
if !fd.RetryOnError && fd.IsMultiPart {
|
||||
// 降级
|
||||
fd.RetryOnError = true
|
||||
fd.DownloadTaskList = []*DownloadTask{}
|
||||
fd.totalTasks = 1
|
||||
fd.IsMultiPart = false
|
||||
fd.createDownloadTasks()
|
||||
return fd.startDownload()
|
||||
}
|
||||
return fmt.Errorf("download failed with %d errors: %v", len(errArr), errArr[0])
|
||||
}
|
||||
|
||||
@@ -253,11 +277,21 @@ func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan cha
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "cancelled") {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
task.err = err
|
||||
globalLogger.Warn().Msgf("Task %d failed (attempt %d/%d): %v", task.taskID, retries+1, MaxRetries, err)
|
||||
|
||||
if retries < MaxRetries-1 {
|
||||
time.Sleep(RetryDelay)
|
||||
select {
|
||||
case <-fd.ctx.Done():
|
||||
errorChan <- fmt.Errorf("task %d cancelled during retry", task.taskID)
|
||||
return
|
||||
case <-time.After(RetryDelay):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +299,13 @@ func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan cha
|
||||
}
|
||||
|
||||
func (fd *FileDownloader) doDownloadTask(progressChan chan ProgressChan, task *DownloadTask) error {
|
||||
request, err := http.NewRequest("GET", fd.Url, nil)
|
||||
select {
|
||||
case <-fd.ctx.Done():
|
||||
return fmt.Errorf("download cancelled")
|
||||
default:
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(fd.ctx, "GET", fd.Url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request failed: %w", err)
|
||||
}
|
||||
@@ -292,33 +332,31 @@ func (fd *FileDownloader) doDownloadTask(progressChan chan ProgressChan, task *D
|
||||
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
select {
|
||||
case <-fd.ctx.Done():
|
||||
return fmt.Errorf("download cancelled")
|
||||
default:
|
||||
}
|
||||
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
remain := task.rangeEnd - (task.rangeStart + task.downloadedSize) + 1
|
||||
writeSize := int64(n)
|
||||
if writeSize > remain {
|
||||
writeSize = remain
|
||||
}
|
||||
|
||||
_, writeErr := fd.File.WriteAt(buf[:writeSize], task.rangeStart+task.downloadedSize)
|
||||
offset := task.rangeStart + task.downloadedSize
|
||||
_, writeErr := fd.File.WriteAt(buf[:writeSize], offset)
|
||||
if writeErr != nil {
|
||||
return fmt.Errorf("write file failed at offset %d: %w", task.rangeStart+task.downloadedSize, writeErr)
|
||||
return fmt.Errorf("write file failed at offset %d: %w", offset, writeErr)
|
||||
}
|
||||
|
||||
task.downloadedSize += writeSize
|
||||
progressChan <- ProgressChan{taskID: task.taskID, bytes: writeSize}
|
||||
|
||||
if task.rangeStart+task.downloadedSize-1 >= task.rangeEnd {
|
||||
if fd.TotalSize > 0 && task.rangeStart+task.downloadedSize-1 >= task.rangeEnd {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
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
|
||||
}
|
||||
return fmt.Errorf("read response failed: %w", err)
|
||||
@@ -331,22 +369,15 @@ func (fd *FileDownloader) verifyDownload() error {
|
||||
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)
|
||||
if fd.TotalSize > 0 {
|
||||
_, err := fd.File.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get file info failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -364,3 +395,17 @@ func (fd *FileDownloader) Start() error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (fd *FileDownloader) Cancel() {
|
||||
if fd.cancelFunc != nil {
|
||||
fd.cancelFunc()
|
||||
}
|
||||
|
||||
if fd.File != nil {
|
||||
fd.File.Close()
|
||||
}
|
||||
|
||||
if fd.FileName != "" {
|
||||
_ = os.Remove(fd.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
78
core/http.go
78
core/http.go
@@ -4,18 +4,17 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"res-downloader/core/shared"
|
||||
sysRuntime "runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
type respData map[string]interface{}
|
||||
@@ -85,8 +84,6 @@ func (h *HttpServer) preview(w http.ResponseWriter, r *http.Request) {
|
||||
request.Header.Set("Range", rangeHeader)
|
||||
}
|
||||
|
||||
//request.Header.Set("User-Agent", "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")
|
||||
//request.Header.Set("Referer", parsedURL.Scheme+"://"+parsedURL.Host+"/")
|
||||
resp, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch the resource", http.StatusInternalServerError)
|
||||
@@ -94,12 +91,15 @@ func (h *HttpServer) preview(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
if contentRange := resp.Header.Get("Content-Range"); contentRange != "" {
|
||||
w.Header().Set("Content-Range", contentRange)
|
||||
for k, v := range resp.Header {
|
||||
if strings.ToLower(k) == "access-control-allow-origin" {
|
||||
continue
|
||||
}
|
||||
for _, vv := range v {
|
||||
w.Header().Add(k, vv)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
@@ -206,42 +206,14 @@ func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
filePath := data.FilePath
|
||||
var cmd *exec.Cmd
|
||||
|
||||
switch sysRuntime.GOOS {
|
||||
case "darwin":
|
||||
cmd = exec.Command("open", "-R", filePath)
|
||||
case "windows":
|
||||
cmd = exec.Command("explorer", "/select,", filePath)
|
||||
case "linux":
|
||||
cmd = exec.Command("nautilus", filePath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
cmd = exec.Command("thunar", filePath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
cmd = exec.Command("dolphin", filePath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
cmd = exec.Command("pcmanfm", filePath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
globalLogger.Err(err)
|
||||
h.error(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
h.error(w, "unsupported platform")
|
||||
return
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
err = shared.OpenFolder(data.FilePath)
|
||||
if err != nil {
|
||||
globalLogger.Err(err)
|
||||
h.error(w, err.Error())
|
||||
return
|
||||
}
|
||||
h.success(w)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *HttpServer) install(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -363,7 +335,7 @@ func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
|
||||
var data struct {
|
||||
MediaInfo
|
||||
shared.MediaInfo
|
||||
DecodeStr string `json:"decodeStr"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||
@@ -374,9 +346,27 @@ func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
|
||||
h.success(w)
|
||||
}
|
||||
|
||||
func (h *HttpServer) cancel(w http.ResponseWriter, r *http.Request) {
|
||||
var data struct {
|
||||
shared.MediaInfo
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||
h.error(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err := resourceOnce.cancel(data.Id)
|
||||
if err != nil {
|
||||
h.error(w, err.Error())
|
||||
return
|
||||
}
|
||||
h.success(w)
|
||||
}
|
||||
|
||||
func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) {
|
||||
var data struct {
|
||||
MediaInfo
|
||||
shared.MediaInfo
|
||||
Filename string `json:"filename"`
|
||||
DecodeStr string `json:"decodeStr"`
|
||||
}
|
||||
@@ -394,7 +384,7 @@ func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *HttpServer) batchImport(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *HttpServer) batchExport(w http.ResponseWriter, r *http.Request) {
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
@@ -408,6 +398,8 @@ func (h *HttpServer) batchImport(w http.ResponseWriter, r *http.Request) {
|
||||
h.error(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_ = shared.OpenFolder(fileName)
|
||||
h.success(w, respData{
|
||||
"file_name": fileName,
|
||||
})
|
||||
|
||||
@@ -17,8 +17,10 @@ func Middleware(next http.Handler) http.Handler {
|
||||
func HandleApi(w http.ResponseWriter, r *http.Request) bool {
|
||||
if strings.HasPrefix(r.URL.Path, "/api") {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
if r.URL.Path != "/api/preview" {
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
}
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return true
|
||||
@@ -56,10 +58,12 @@ func HandleApi(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpServerOnce.delete(w, r)
|
||||
case "/api/download":
|
||||
httpServerOnce.download(w, r)
|
||||
case "/api/cancel":
|
||||
httpServerOnce.cancel(w, r)
|
||||
case "/api/wx-file-decode":
|
||||
httpServerOnce.wxFileDecode(w, r)
|
||||
case "/api/batch-import":
|
||||
httpServerOnce.batchImport(w, r)
|
||||
case "/api/batch-export":
|
||||
httpServerOnce.batchExport(w, r)
|
||||
case "/api/cert":
|
||||
httpServerOnce.downCert(w, r)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (p *DefaultPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *
|
||||
Url: rawUrl,
|
||||
UrlSign: urlSign,
|
||||
CoverUrl: "",
|
||||
Size: shared.FormatSize(value),
|
||||
Size: value,
|
||||
Domain: shared.GetTopLevelDomain(rawUrl),
|
||||
Classify: classify,
|
||||
Suffix: suffix,
|
||||
|
||||
@@ -14,6 +14,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var qqMediaRegex = regexp.MustCompile(`get\s*media\(\)\{`)
|
||||
var qqCommentRegex = regexp.MustCompile(`async\s*finderGetCommentDetail\((\w+)\)\s*\{return(.*?)\s*}\s*async`)
|
||||
|
||||
type QqPlugin struct {
|
||||
bridge *shared.Bridge
|
||||
}
|
||||
@@ -50,6 +53,9 @@ func (p *QqPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.
|
||||
|
||||
classify, _ := p.bridge.TypeSuffix(resp.Header.Get("Content-Type"))
|
||||
if classify == "video" && strings.HasSuffix(host, "finder.video.qq.com") {
|
||||
if strings.Contains(resp.Request.Header.Get("Origin"), "mp.weixin.qq.com") {
|
||||
return nil
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
@@ -72,7 +78,7 @@ func (p *QqPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.
|
||||
return respTemp
|
||||
}
|
||||
bodyStr := string(body)
|
||||
newBody := regexp.MustCompile(`get\s*media\(\)\{`).
|
||||
newBody := qqMediaRegex.
|
||||
ReplaceAllString(bodyStr, `
|
||||
get media(){
|
||||
if(this.objectDesc){
|
||||
@@ -85,7 +91,7 @@ func (p *QqPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.
|
||||
|
||||
`)
|
||||
|
||||
newBody = regexp.MustCompile(`async\s*finderGetCommentDetail\((\w+)\)\s*\{return(.*?)\s*}\s*async`).
|
||||
newBody = qqCommentRegex.
|
||||
ReplaceAllString(newBody, `
|
||||
async finderGetCommentDetail($1) {
|
||||
var res = await$2;
|
||||
@@ -119,13 +125,6 @@ func (p *QqPlugin) handleWechatRequest(r *http.Request, ctx *goproxy.ProxyCtx) (
|
||||
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)
|
||||
@@ -167,7 +166,7 @@ func (p *QqPlugin) handleMedia(body []byte) {
|
||||
Url: rawUrl,
|
||||
UrlSign: urlSign,
|
||||
CoverUrl: "",
|
||||
Size: "0",
|
||||
Size: 0,
|
||||
Domain: shared.GetTopLevelDomain(rawUrl),
|
||||
Classify: "video",
|
||||
Suffix: ".mp4",
|
||||
@@ -185,16 +184,27 @@ func (p *QqPlugin) handleMedia(body []byte) {
|
||||
res.ContentType = "image/png"
|
||||
}
|
||||
|
||||
isAll, _ := p.bridge.GetResType("all")
|
||||
isImage, _ := p.bridge.GetResType("image")
|
||||
if res.Classify == "image" && !isImage && !isAll {
|
||||
return
|
||||
}
|
||||
|
||||
isVideo, _ := p.bridge.GetResType("video")
|
||||
if res.Classify == "video" && !isVideo && !isAll {
|
||||
return
|
||||
}
|
||||
|
||||
if urlToken, ok := firstMedia["urlToken"].(string); ok {
|
||||
res.Url += urlToken
|
||||
}
|
||||
|
||||
switch size := firstMedia["fileSize"].(type) {
|
||||
case float64:
|
||||
res.Size = shared.FormatSize(size)
|
||||
res.Size = size
|
||||
case string:
|
||||
if value, err := strconv.ParseFloat(size, 64); err == nil {
|
||||
res.Size = shared.FormatSize(value)
|
||||
res.Size = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,23 +21,6 @@ type Proxy struct {
|
||||
Is bool
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var pluginRegistry = make(map[string]shared.Plugin)
|
||||
|
||||
func init() {
|
||||
@@ -105,7 +88,6 @@ func (p *Proxy) Startup() {
|
||||
func (p *Proxy) setCa() error {
|
||||
ca, err := tls.X509KeyPair(appOnce.PublicCrt, appOnce.PrivateKey)
|
||||
if err != nil {
|
||||
DialogErr("Failed to start proxy service 1")
|
||||
return err
|
||||
}
|
||||
if ca.Leaf, err = x509.ParseCertificate(ca.Certificate[0]); err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
@@ -22,29 +23,33 @@ type WxFileDecodeResult struct {
|
||||
|
||||
type Resource struct {
|
||||
mediaMark sync.Map
|
||||
tasks sync.Map
|
||||
resType map[string]bool
|
||||
resTypeMux sync.RWMutex
|
||||
}
|
||||
|
||||
func initResource() *Resource {
|
||||
if resourceOnce == nil {
|
||||
resourceOnce = &Resource{
|
||||
resType: map[string]bool{
|
||||
"all": true,
|
||||
"image": true,
|
||||
"audio": true,
|
||||
"video": true,
|
||||
"m3u8": true,
|
||||
"live": true,
|
||||
"xls": true,
|
||||
"doc": true,
|
||||
"pdf": true,
|
||||
},
|
||||
}
|
||||
resourceOnce = &Resource{}
|
||||
resourceOnce.resType = resourceOnce.buildResType(globalConfig.MimeMap)
|
||||
}
|
||||
return resourceOnce
|
||||
}
|
||||
|
||||
func (r *Resource) buildResType(mime map[string]MimeInfo) map[string]bool {
|
||||
t := map[string]bool{
|
||||
"all": true,
|
||||
}
|
||||
|
||||
for _, item := range mime {
|
||||
if _, ok := t[item.Type]; !ok {
|
||||
t[item.Type] = true
|
||||
}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func (r *Resource) mediaIsMarked(key string) bool {
|
||||
_, loaded := r.mediaMark.Load(key)
|
||||
return loaded
|
||||
@@ -56,29 +61,23 @@ func (r *Resource) markMedia(key string) {
|
||||
|
||||
func (r *Resource) getResType(key string) (bool, bool) {
|
||||
r.resTypeMux.RLock()
|
||||
defer r.resTypeMux.RUnlock()
|
||||
value, ok := r.resType[key]
|
||||
r.resTypeMux.RUnlock()
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func (r *Resource) setResType(n []string) {
|
||||
r.resTypeMux.Lock()
|
||||
defer r.resTypeMux.Unlock()
|
||||
r.resType = map[string]bool{
|
||||
"all": false,
|
||||
"image": false,
|
||||
"audio": false,
|
||||
"video": false,
|
||||
"m3u8": false,
|
||||
"live": false,
|
||||
"xls": false,
|
||||
"doc": false,
|
||||
"pdf": false,
|
||||
for key := range r.resType {
|
||||
r.resType[key] = false
|
||||
}
|
||||
|
||||
for _, value := range n {
|
||||
r.resType[value] = true
|
||||
if _, ok := r.resType[value]; ok {
|
||||
r.resType[value] = true
|
||||
}
|
||||
}
|
||||
r.resTypeMux.Unlock()
|
||||
}
|
||||
|
||||
func (r *Resource) clear() {
|
||||
@@ -89,13 +88,27 @@ func (r *Resource) delete(sign string) {
|
||||
r.mediaMark.Delete(sign)
|
||||
}
|
||||
|
||||
func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
|
||||
func (r *Resource) cancel(id string) error {
|
||||
if d, ok := r.tasks.Load(id); ok {
|
||||
d.(*FileDownloader).Cancel()
|
||||
r.tasks.Delete(id) // 可选:取消后清理
|
||||
return nil
|
||||
}
|
||||
return errors.New("task not found")
|
||||
}
|
||||
|
||||
func (r *Resource) download(mediaInfo shared.MediaInfo, decodeStr string) {
|
||||
if globalConfig.SaveDirectory == "" {
|
||||
return
|
||||
}
|
||||
go func(mediaInfo MediaInfo) {
|
||||
go func(mediaInfo shared.MediaInfo) {
|
||||
rawUrl := mediaInfo.Url
|
||||
fileName := shared.Md5(rawUrl)
|
||||
|
||||
if v := shared.GetFileNameFromURL(rawUrl); v != "" {
|
||||
fileName = v
|
||||
}
|
||||
|
||||
if mediaInfo.Description != "" {
|
||||
fileName = regexp.MustCompile(`[^\w\p{Han}]`).ReplaceAllString(mediaInfo.Description, "")
|
||||
fileLen := globalConfig.FilenameLen
|
||||
@@ -110,9 +123,13 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
|
||||
}
|
||||
|
||||
if globalConfig.FilenameTime {
|
||||
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+"_"+shared.GetCurrentDateTimeFormatted()+mediaInfo.Suffix)
|
||||
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+"_"+shared.GetCurrentDateTimeFormatted())
|
||||
} else {
|
||||
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+mediaInfo.Suffix)
|
||||
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(mediaInfo.SavePath, mediaInfo.Suffix) {
|
||||
mediaInfo.SavePath = mediaInfo.SavePath + mediaInfo.Suffix
|
||||
}
|
||||
|
||||
if strings.Contains(rawUrl, "qq.com") {
|
||||
@@ -143,9 +160,13 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
|
||||
downloader.progressCallback = func(totalDownloaded, totalSize float64, taskID int, taskProgress float64) {
|
||||
r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded*100/totalSize))+"%", shared.DownloadStatusRunning)
|
||||
}
|
||||
r.tasks.Store(mediaInfo.Id, downloader)
|
||||
err := downloader.Start()
|
||||
mediaInfo.SavePath = downloader.FileName
|
||||
if err != nil {
|
||||
r.progressEventsEmit(mediaInfo, err.Error())
|
||||
if !strings.Contains(err.Error(), "cancelled") {
|
||||
r.progressEventsEmit(mediaInfo, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
if decodeStr != "" {
|
||||
@@ -159,7 +180,7 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
|
||||
}(mediaInfo)
|
||||
}
|
||||
|
||||
func (r *Resource) parseHeaders(mediaInfo MediaInfo) (map[string]string, error) {
|
||||
func (r *Resource) parseHeaders(mediaInfo shared.MediaInfo) (map[string]string, error) {
|
||||
headers := make(map[string]string)
|
||||
|
||||
if hh, ok := mediaInfo.OtherData["headers"]; ok {
|
||||
@@ -178,7 +199,7 @@ func (r *Resource) parseHeaders(mediaInfo MediaInfo) (map[string]string, error)
|
||||
return headers, nil
|
||||
}
|
||||
|
||||
func (r *Resource) wxFileDecode(mediaInfo MediaInfo, fileName, decodeStr string) (string, error) {
|
||||
func (r *Resource) wxFileDecode(mediaInfo shared.MediaInfo, fileName, decodeStr string) (string, error) {
|
||||
sourceFile, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -203,7 +224,7 @@ func (r *Resource) wxFileDecode(mediaInfo MediaInfo, fileName, decodeStr string)
|
||||
return mediaInfo.SavePath, nil
|
||||
}
|
||||
|
||||
func (r *Resource) progressEventsEmit(mediaInfo MediaInfo, args ...string) {
|
||||
func (r *Resource) progressEventsEmit(mediaInfo shared.MediaInfo, args ...string) {
|
||||
Status := shared.DownloadStatusError
|
||||
Message := "ok"
|
||||
|
||||
@@ -236,10 +257,15 @@ func (r *Resource) decodeWxFile(fileName, decodeStr string) error {
|
||||
|
||||
byteCount := len(decodedBytes)
|
||||
fileBytes := make([]byte, byteCount)
|
||||
_, err = file.Read(fileBytes)
|
||||
n, err := file.Read(fileBytes)
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
if n < byteCount {
|
||||
byteCount = n
|
||||
}
|
||||
|
||||
xorResult := make([]byte, byteCount)
|
||||
for i := 0; i < byteCount; i++ {
|
||||
xorResult[i] = decodedBytes[i] ^ fileBytes[i]
|
||||
|
||||
@@ -5,7 +5,7 @@ type MediaInfo struct {
|
||||
Url string
|
||||
UrlSign string
|
||||
CoverUrl string
|
||||
Size string
|
||||
Size float64
|
||||
Domain string
|
||||
Classify string
|
||||
Suffix string
|
||||
|
||||
@@ -3,10 +3,17 @@ package shared
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
sysRuntime "runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -58,6 +65,45 @@ func IsDevelopment() bool {
|
||||
return os.Getenv("APP_ENV") == "development"
|
||||
}
|
||||
|
||||
func GetFileNameFromURL(rawUrl string) string {
|
||||
parsedURL, err := url.Parse(rawUrl)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
fileName := path.Base(parsedURL.Path)
|
||||
if fileName == "" || fileName == "/" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if decoded, err := url.QueryUnescape(fileName); err == nil {
|
||||
fileName = decoded
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`[<>:"/\\|?*]`)
|
||||
fileName = re.ReplaceAllString(fileName, "_")
|
||||
|
||||
fileName = strings.TrimRightFunc(fileName, func(r rune) bool {
|
||||
return r == '.' || r == ' '
|
||||
})
|
||||
|
||||
const maxFileNameLen = 255
|
||||
runes := []rune(fileName)
|
||||
if len(runes) > maxFileNameLen {
|
||||
ext := path.Ext(fileName)
|
||||
name := strings.TrimSuffix(fileName, ext)
|
||||
|
||||
runes = []rune(name)
|
||||
if len(runes) > maxFileNameLen-len(ext) {
|
||||
runes = runes[:maxFileNameLen-len(ext)]
|
||||
}
|
||||
name = string(runes)
|
||||
fileName = name + ext
|
||||
}
|
||||
|
||||
return fileName
|
||||
}
|
||||
|
||||
func GetCurrentDateTimeFormatted() string {
|
||||
now := time.Now()
|
||||
return fmt.Sprintf("%04d%02d%02d%02d%02d%02d",
|
||||
@@ -68,3 +114,50 @@ func GetCurrentDateTimeFormatted() string {
|
||||
now.Minute(),
|
||||
now.Second())
|
||||
}
|
||||
|
||||
func GetUniqueFileName(filePath string) string {
|
||||
if !FileExist(filePath) {
|
||||
return filePath
|
||||
}
|
||||
|
||||
ext := filepath.Ext(filePath)
|
||||
baseName := strings.TrimSuffix(filePath, ext)
|
||||
count := 1
|
||||
|
||||
for {
|
||||
newFileName := fmt.Sprintf("%s(%d)%s", baseName, count, ext)
|
||||
if !FileExist(newFileName) {
|
||||
return newFileName
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
func OpenFolder(filePath string) error {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
switch sysRuntime.GOOS {
|
||||
case "darwin":
|
||||
cmd = exec.Command("open", "-R", filePath)
|
||||
case "windows":
|
||||
cmd = exec.Command("explorer", "/select,", filePath)
|
||||
case "linux":
|
||||
cmd = exec.Command("nautilus", filePath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
cmd = exec.Command("thunar", filePath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
cmd = exec.Command("dolphin", filePath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
cmd = exec.Command("pcmanfm", filePath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported platform")
|
||||
}
|
||||
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
@@ -88,13 +88,18 @@ func (s *SystemSetup) installCert() (string, error) {
|
||||
}
|
||||
|
||||
certName := appOnce.AppName + ".crt"
|
||||
|
||||
var certPath string
|
||||
if distro == "deepin" {
|
||||
var updateCmd = []string{"update-ca-certificates"}
|
||||
|
||||
switch distro {
|
||||
case "deepin":
|
||||
certDir := "/usr/share/ca-certificates/" + appOnce.AppName
|
||||
certPath = certDir + "/" + certName
|
||||
s.runCommand([]string{"mkdir", "-p", certDir}, true)
|
||||
} else {
|
||||
case "arch":
|
||||
certPath = "/usr/share/ca-certificates/trust-source/" + certName
|
||||
updateCmd = []string{"update-ca-trust"}
|
||||
default:
|
||||
certPath = "/usr/local/share/ca-certificates/" + certName
|
||||
}
|
||||
|
||||
@@ -112,7 +117,7 @@ func (s *SystemSetup) installCert() (string, error) {
|
||||
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)}
|
||||
echoCmd := []string{"bash", "-c", fmt.Sprintf("echo '%s/%s' >> %s", appOnce.AppName, certName, confPath)}
|
||||
if output, err := s.runCommand(echoCmd, true); err != nil {
|
||||
errs.WriteString(fmt.Sprintf("append conf failed: %s\n%s\n", err.Error(), output))
|
||||
} else {
|
||||
@@ -122,7 +127,7 @@ func (s *SystemSetup) installCert() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if output, err := s.runCommand([]string{"update-ca-certificates"}, true); err != nil {
|
||||
if output, err := s.runCommand(updateCmd, true); err != nil {
|
||||
errs.WriteString(fmt.Sprintf("update failed: %s\n%s\n", err.Error(), output))
|
||||
} else {
|
||||
isSuccess = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
## 开启代理
|
||||
- 安装完成后开启代理,如图:
|
||||
- 安装完成后开启代理 (最新版本为“开启抓取”),如图:
|
||||

|
||||
|
||||
## 拦截资源
|
||||
|
||||
@@ -11,4 +11,8 @@
|
||||
- 打开要捕获的源, 如:视频号、网页、小程序等等
|
||||
- 返回软件首页即可看到资源列
|
||||
|
||||
!> windows安装,先关闭所有安全管家之类的软件,安装完成后首次使用需右键管理员打开
|
||||
|
||||
!> Mac如果无法拦截 请关闭防火墙
|
||||
|
||||

|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 30 KiB |
@@ -1,5 +1,5 @@
|
||||
## 下载安装文件
|
||||
- windows下载.exe结尾的,根据自己的系统架构下载合适的安装文件,通常下载带有“x64-installer.exe”结尾的文件
|
||||
- windows下载.exe结尾的,根据自己的系统架构下载合适的安装文件,通常下载带有“win_amd64.exe”或“x64-installer.exe”结尾的文件
|
||||
- Mac下载.dmg结尾即可
|
||||
- Linux根据系统类型下载对应的执行文件或安装文件
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
- 比如只需要视频时就选择视频类型,可以多选
|
||||

|
||||
|
||||
## 批量导出、批量导入使用场景
|
||||
- 导出resd格式数据,将txt文件发送到另外的电脑,打开文件复制内容,使用批量导入导入到新电脑
|
||||
|
||||
## 复制链接、视频解密
|
||||
- 复制链接可用于第三方软件进行下载,下载完成后对该视频解密,点击“视频解密”选择用其他软件下载完成后的视频文件进行解密
|
||||

|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
> 设置里面关闭全量拦截,将视频转发好友后打开
|
||||
|
||||
## 某某网址拦截不了?
|
||||
> 本软件实现原理 & 初衷如下,并非万能的,所以有一些应用拦截不了很正常
|
||||
> 本软件并非万能的,所以有一些应用拦截不了很正常,实现原理 & 初衷如下,
|
||||
```
|
||||
本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。
|
||||
```
|
||||
@@ -10,7 +10,7 @@
|
||||
## 软件打不开了?之前可以打开
|
||||
> 删除对应目录, 然后重启
|
||||
```
|
||||
## Mac执行
|
||||
## Mac终端执行
|
||||
rm -rf /Users/$(whoami)/Library/Preferences/res-downloader
|
||||
|
||||
## Windows手动删除以下目录,Administrator为用户名 通常如下:
|
||||
@@ -51,7 +51,8 @@ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keyc
|
||||
|
||||
## 拦截不到小程序中的资源
|
||||
清理微信缓存,删除小程序后,重新打开
|
||||
> 设置->存储空间->缓存
|
||||
> 1.设置->存储空间->缓存
|
||||
> 2.删除小程序相关缓存目录(自行搜索)
|
||||
|
||||
## 只拦截打开的视频号视频
|
||||
关闭全量拦截,打开视频号视频详情,通常分享好友后打开的页面属于详情页
|
||||
|
||||
10
frontend/components.d.ts
vendored
10
frontend/components.d.ts
vendored
@@ -7,11 +7,15 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Action: typeof import('./src/components/Action.vue')['default']
|
||||
ActionDesc: typeof import('./src/components/ActionDesc.vue')['default']
|
||||
Footer: typeof import('./src/components/Footer.vue')['default']
|
||||
ImportJson: typeof import('./src/components/ImportJson.vue')['default']
|
||||
Index: typeof import('./src/components/layout/Index.vue')['default']
|
||||
NaiveProvider: typeof import('./src/components/NaiveProvider.vue')['default']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
@@ -31,14 +35,16 @@ declare module 'vue' {
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
Password: typeof import('./src/components/Password.vue')['default']
|
||||
Preview: typeof import('./src/components/Preview.vue')['default']
|
||||
ResAction: typeof import('./src/components/ResAction.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Screen: typeof import('./src/components/Screen.vue')['default']
|
||||
|
||||
BIN
frontend/src/.DS_Store
vendored
BIN
frontend/src/.DS_Store
vendored
Binary file not shown.
@@ -2,8 +2,6 @@
|
||||
<NConfigProvider class="h-full" :theme="theme" :locale="uiLocale">
|
||||
<NaiveProvider>
|
||||
<RouterView/>
|
||||
<ShowLoading :isLoading="loading"/>
|
||||
<Password v-model:showModal="showPassword" @submit="handlePassword"/>
|
||||
</NaiveProvider>
|
||||
<NGlobalStyle/>
|
||||
<NModalProvider/>
|
||||
@@ -14,19 +12,14 @@
|
||||
import NaiveProvider from '@/components/NaiveProvider.vue'
|
||||
import {darkTheme, lightTheme, zhCN, enUS} from 'naive-ui'
|
||||
import {useIndexStore} from "@/stores"
|
||||
import {computed, onMounted, ref} from "vue"
|
||||
import {computed, onMounted} from "vue"
|
||||
import {useEventStore} from "@/stores/event"
|
||||
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 {locale} = useI18n()
|
||||
|
||||
const theme = computed(() => {
|
||||
if (store.globalConfig.Theme === "darkTheme") {
|
||||
@@ -47,11 +40,6 @@ const uiLocale = computed(() => {
|
||||
|
||||
onMounted(async () => {
|
||||
await store.init()
|
||||
loading.value = true
|
||||
handleInstall().then((is: boolean)=>{
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
|
||||
eventStore.init()
|
||||
eventStore.addHandle({
|
||||
@@ -68,34 +56,4 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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>
|
||||
@@ -92,6 +92,13 @@ export default {
|
||||
data: data
|
||||
})
|
||||
},
|
||||
cancel(data: object) {
|
||||
return request({
|
||||
url: 'api/cancel',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
},
|
||||
download(data: object) {
|
||||
return request({
|
||||
url: 'api/download',
|
||||
@@ -106,9 +113,9 @@ export default {
|
||||
data: data
|
||||
})
|
||||
},
|
||||
batchImport(data: object) {
|
||||
batchExport(data: object) {
|
||||
return request({
|
||||
url: 'api/batch-import',
|
||||
url: 'api/batch-export',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type {AxiosResponse, InternalAxiosRequestConfig} from 'axios'
|
||||
import axios from 'axios'
|
||||
import {useIndexStore} from "@/stores";
|
||||
import {computed} from "vue";
|
||||
|
||||
interface RequestOptions {
|
||||
url: string
|
||||
@@ -12,6 +10,7 @@ interface RequestOptions {
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: "/",
|
||||
timeout: 180000
|
||||
})
|
||||
|
||||
instance.interceptors.request.use(
|
||||
|
||||
109
frontend/src/components/Action.vue
Normal file
109
frontend/src/components/Action.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div style="--wails-draggable:no-drag" class="grid grid-cols-3 gap-1.5">
|
||||
<n-icon
|
||||
size="30"
|
||||
class="text-emerald-600 dark:text-emerald-400 bg-emerald-500/20 dark:bg-emerald-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-emerald-500/40 transition-colors"
|
||||
@click="action('down')"
|
||||
>
|
||||
<DownloadOutline/>
|
||||
</n-icon>
|
||||
|
||||
<n-icon
|
||||
size="28"
|
||||
class="text-red-500 dark:text-red-300 bg-red-500/20 dark:bg-red-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-red-500/40 transition-colors"
|
||||
@click="action('delete')"
|
||||
>
|
||||
<TrashOutline/>
|
||||
</n-icon>
|
||||
|
||||
<NPopover placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="30" class="text-sky-500 dark:text-sky-300 bg-sky-500/20 dark:bg-sky-200/30 rounded-full flex items-center justify-center p-2 cursor-pointer hover:bg-sky-200/40 transition-colors">
|
||||
<GridSharp/>
|
||||
</NIcon>
|
||||
</template>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center justify-start p-1.5 cursor-pointer" v-if="row.Status === 'running'" @click="action('cancel')">
|
||||
<n-icon
|
||||
size="28"
|
||||
class="text-red-500 dark:text-red-300 bg-red-500/20 dark:bg-red-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-red-500/40 transition-colors"
|
||||
>
|
||||
<CloseOutline/>
|
||||
</n-icon>
|
||||
<span class="ml-1">{{ t("index.cancel_down") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-start p-1.5 cursor-pointer" @click="action('copy')">
|
||||
<n-icon
|
||||
size="28"
|
||||
class="text-blue-300 dark:text-blue-300 bg-blue-300/20 dark:bg-blue-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-blue-300/40 transition-colors"
|
||||
>
|
||||
<LinkOutline/>
|
||||
</n-icon>
|
||||
<span class="ml-1">{{ t("index.copy_link") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-start p-1.5 cursor-pointer" v-if="row.Classify !== 'live' && row.Classify !== 'm3u8'" @click="action('open')">
|
||||
<n-icon
|
||||
size="28"
|
||||
class="text-blue-500 dark:text-blue-200 bg-blue-400/20 dark:bg-blue-400/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-blue-400/40 transition-colors"
|
||||
>
|
||||
<GlobeOutline/>
|
||||
</n-icon>
|
||||
<span class="ml-1">{{ t("index.open_link") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-start p-1.5 cursor-pointer" v-if="row.DecodeKey" @click="action('decode')">
|
||||
<n-icon
|
||||
size="28"
|
||||
class="text-orange-400 dark:text-red-300 bg-orange-500/20 dark:bg-orange-200/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-orange-200/40 transition-colors"
|
||||
>
|
||||
<LockOpenSharp/>
|
||||
</n-icon>
|
||||
<span class="ml-1">{{ t("index.video_decode") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-start p-1.5 cursor-pointer" @click="action('json')">
|
||||
<n-icon
|
||||
size="28"
|
||||
class="text-sky-400 dark:text-sky-200 bg-sky-500/20 dark:bg-sky-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-sky-500/40 transition-colors"
|
||||
>
|
||||
<CopyOutline/>
|
||||
</n-icon>
|
||||
<span class="ml-1">{{ t("index.copy_data") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</NPopover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {
|
||||
DownloadOutline,
|
||||
CopyOutline,
|
||||
GlobeOutline,
|
||||
LockOpenSharp,
|
||||
LinkOutline,
|
||||
GridSharp,
|
||||
CloseOutline,
|
||||
TrashOutline
|
||||
} from "@vicons/ionicons5"
|
||||
|
||||
const {t} = useI18n()
|
||||
const props = defineProps<{
|
||||
row: any,
|
||||
index: number,
|
||||
}>()
|
||||
|
||||
const emits = defineEmits(["action"])
|
||||
|
||||
const action = (type: string) => {
|
||||
if (type === 'down' && (props.row.Classify === 'live' || props.row.Classify === 'm3u8')) {
|
||||
window?.$message?.error(t("index.download_no_tip"))
|
||||
return
|
||||
}
|
||||
emits('action', props.row, props.index, type)
|
||||
}
|
||||
|
||||
</script>
|
||||
107
frontend/src/components/ActionDesc.vue
Normal file
107
frontend/src/components/ActionDesc.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<span>
|
||||
{{ t('index.operation') }}
|
||||
</span>
|
||||
<NPopover trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center justify-start p-1.5">
|
||||
<n-icon size="28"
|
||||
class="text-emerald-600 dark:text-emerald-400 bg-emerald-500/20 dark:bg-emerald-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-emerald-500/40 transition-colors">
|
||||
<DownloadOutline/>
|
||||
</n-icon>
|
||||
<span class="ml-1">{{ t("index.direct_download") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-start p-1.5">
|
||||
<n-icon size="28"
|
||||
class="text-red-500 dark:text-red-300 bg-red-500/20 dark:bg-red-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-red-500/40 transition-colors">
|
||||
<CloseOutline/>
|
||||
</n-icon>
|
||||
<span class="ml-1">{{ t("index.cancel_down") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-start p-1.5">
|
||||
<n-icon
|
||||
size="28"
|
||||
class="text-blue-600 dark:text-blue-300 bg-blue-500/20 dark:bg-blue-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-blue-500/40 transition-colors"
|
||||
>
|
||||
<LinkOutline/>
|
||||
</n-icon>
|
||||
<span class="ml-1">{{ t("index.copy_link") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-start p-1.5">
|
||||
<n-icon
|
||||
size="28"
|
||||
class="text-blue-500 dark:text-blue-200 bg-blue-400/20 dark:bg-blue-400/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-blue-400/40 transition-colors"
|
||||
>
|
||||
<GlobeOutline/>
|
||||
</n-icon>
|
||||
<span class="ml-1">{{ t("index.open_link") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-start p-1.5">
|
||||
<n-icon
|
||||
size="28"
|
||||
class="text-orange-400 dark:text-red-300 bg-orange-500/20 dark:bg-orange-200/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-orange-200/40 transition-colors"
|
||||
>
|
||||
<LockOpenSharp/>
|
||||
</n-icon>
|
||||
<span class="ml-1">{{ t("index.video_decode") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-start p-1.5">
|
||||
<n-icon
|
||||
size="28"
|
||||
class="text-sky-400 dark:text-sky-200 bg-sky-500/20 dark:bg-sky-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-sky-500/40 transition-colors"
|
||||
>
|
||||
<CopyOutline/>
|
||||
</n-icon>
|
||||
<span class="ml-1">{{ t("index.copy_data") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-start p-1.5">
|
||||
<n-icon
|
||||
size="28"
|
||||
class="text-red-500 dark:text-red-300 bg-red-500/20 dark:bg-red-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-red-500/40 transition-colors"
|
||||
>
|
||||
<TrashOutline/>
|
||||
</n-icon>
|
||||
<span class="ml-1">{{ t("index.delete_row") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-start p-1.5">
|
||||
<n-icon
|
||||
size="28"
|
||||
class="text-sky-500 dark:text-sky-300 bg-sky-500/20 dark:bg-sky-200/30 rounded-full flex items-center justify-center p-2 cursor-pointer hover:bg-sky-200/40 transition-colors"
|
||||
>
|
||||
<GridSharp/>
|
||||
</n-icon>
|
||||
<span class="ml-1">{{ t("index.more_operation") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</NPopover>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {useI18n} from "vue-i18n"
|
||||
import {
|
||||
CopyOutline,
|
||||
DownloadOutline,
|
||||
GlobeOutline,
|
||||
HelpCircleOutline,
|
||||
LinkOutline,
|
||||
LockOpenSharp,
|
||||
GridSharp,
|
||||
CloseOutline,
|
||||
TrashOutline
|
||||
} from "@vicons/ionicons5"
|
||||
|
||||
const {t} = useI18n()
|
||||
</script>
|
||||
@@ -38,7 +38,7 @@
|
||||
<div>{{ store.appInfo.Copyright }}</div>
|
||||
<div class="flex">
|
||||
<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(certUrl)">{{ t('footer.cert_download') }}</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>
|
||||
|
||||
@@ -84,7 +84,7 @@ const playFlvStream = () => {
|
||||
try {
|
||||
if (!flvjs.isSupported() || !videoPlayer.value) return
|
||||
|
||||
flvPlayer = flvjs.createPlayer({ type: "flv", url: props.previewRow.Url })
|
||||
flvPlayer = flvjs.createPlayer({ type: "flv", url: window?.$baseUrl + "/api/preview?url=" + encodeURIComponent(props.previewRow.Url) })
|
||||
flvPlayer.attachMediaElement(videoPlayer.value)
|
||||
flvPlayer.load()
|
||||
flvPlayer.play()
|
||||
@@ -105,7 +105,7 @@ const setupVideoJsPlayer = () => {
|
||||
}
|
||||
|
||||
player.src({
|
||||
src: props.previewRow.Url,
|
||||
src: window?.$baseUrl + "/api/preview?url=" + encodeURIComponent(props.previewRow.Url),
|
||||
type: props.previewRow.ContentType,
|
||||
withCredentials: true,
|
||||
})
|
||||
@@ -113,7 +113,7 @@ const setupVideoJsPlayer = () => {
|
||||
}
|
||||
|
||||
const playVideoWithoutTotalLength = () => {
|
||||
rowUrl = buildUrlWithParams(props.previewRow.Url)
|
||||
rowUrl = window?.$baseUrl + "/api/preview?url=" + encodeURIComponent(buildUrlWithParams(props.previewRow.Url))
|
||||
mediaSource = new MediaSource()
|
||||
videoPlayer.value.src = URL.createObjectURL(mediaSource)
|
||||
videoPlayer.value.play()
|
||||
@@ -141,7 +141,6 @@ const buildUrlWithParams = (url: string) => {
|
||||
}
|
||||
|
||||
const handleSeeking = () => {
|
||||
console.log('handleSeeking')
|
||||
const currentTime = videoPlayer.value.currentTime
|
||||
const bufferedEnd = videoPlayer.value.buffered.end(videoPlayer.value.buffered.length - 1)
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<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,
|
||||
}>()
|
||||
|
||||
const emits = defineEmits(["action"])
|
||||
|
||||
const action = (type: string) => {
|
||||
emits('action', props.row, props.index, type)
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -2,7 +2,12 @@
|
||||
<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" :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="relative flex items-center justify-center cursor-pointer" @click="handleFooterUpdate('github')">
|
||||
<img class="w-12 h-12 rounded-full transition-transform duration-300 hover:scale-105 dark" src="@/assets/image/logo.png" alt="res-downloader logo"/>
|
||||
<span class="absolute right-[-25px] top-0 font-semibold rounded-full bg-red-500 text-white dark:bg-red-600 dark:text-gray-100 text-[10px] px-1.5 py-0.5 animate-pulse" v-if="showUpdate">
|
||||
New
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
@@ -15,7 +20,7 @@
|
||||
:on-after-leave="() => { showAppName = false }"
|
||||
:collapsed-width="70"
|
||||
:collapsed="collapsed"
|
||||
:width="140"
|
||||
:width="envInfo.platform==='linux' ? 160 : 140"
|
||||
:native-scrollbar="false"
|
||||
:inverted="inverted"
|
||||
:on-update:collapsed="collapsedChange"
|
||||
@@ -66,6 +71,8 @@ import Footer from "@/components/Footer.vue"
|
||||
import Screen from "@/components/Screen.vue"
|
||||
import {BrowserOpenURL} from "../../../wailsjs/runtime"
|
||||
import {useI18n} from "vue-i18n"
|
||||
import request from "@/api/request"
|
||||
import {compareVersions} from "@/func"
|
||||
|
||||
const {t} = useI18n()
|
||||
const route = useRoute()
|
||||
@@ -77,6 +84,7 @@ const showAppInfo = ref(false)
|
||||
const menuValue = ref(route.fullPath.substring(1))
|
||||
const store = useIndexStore()
|
||||
const is = ref(false)
|
||||
const showUpdate = ref(false)
|
||||
|
||||
const envInfo = store.envInfo
|
||||
|
||||
@@ -98,6 +106,13 @@ onMounted(()=>{
|
||||
collapsed.value = JSON.parse(collapsedCache).collapsed
|
||||
}
|
||||
is.value = true
|
||||
|
||||
request({
|
||||
url: 'https://res.putyy.com/version.json?v=' + Date.now(),
|
||||
method: 'get',
|
||||
}).then((res)=>{
|
||||
showUpdate.value = compareVersions(res.version, store.appInfo.Version) === 1
|
||||
})
|
||||
})
|
||||
|
||||
const renderIcon = (icon: any) => {
|
||||
@@ -179,8 +194,24 @@ const handleFooterUpdate = (key: string, item?: MenuOption) => {
|
||||
}
|
||||
|
||||
const collapsedChange = (value: boolean)=>{
|
||||
console.log("collapsedChange",value)
|
||||
collapsed.value = value
|
||||
localStorage.setItem("collapsed", JSON.stringify({collapsed: value}))
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
<style scoped>
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +0,0 @@
|
||||
export const DwStatus = {
|
||||
ready: "就绪",
|
||||
running: "运行中",
|
||||
error: "错误",
|
||||
done: "完成",
|
||||
handle: "已下载,后续处理",
|
||||
}
|
||||
40
frontend/src/func.ts
Normal file
40
frontend/src/func.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
const ipv4Regex = /^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)){3}$/
|
||||
const domainRegex = /^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9][a-zA-Z0-9-_]+\.[a-zA-Z]{2,11}?$/
|
||||
const localhostRegex = /^localhost$/
|
||||
|
||||
export const compareVersions = (v1: string, v2: string) => {
|
||||
const parts1 = v1.split('.').map(Number)
|
||||
const parts2 = v2.split('.').map(Number)
|
||||
|
||||
const maxLength = Math.max(parts1.length, parts2.length)
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const num1 = parts1[i] || 0
|
||||
const num2 = parts2[i] || 0
|
||||
|
||||
if (num1 < num2) return -1
|
||||
if (num1 > num2) return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
export const isValidHost = (host: string) => {
|
||||
return ipv4Regex.test(host) || domainRegex.test(host) || localhostRegex.test(host)
|
||||
}
|
||||
|
||||
export const isValidPort = (port: number) => {
|
||||
const portNumber = Number(port)
|
||||
return Number.isInteger(portNumber) && portNumber > 1024 && portNumber < 65535
|
||||
}
|
||||
|
||||
export const formatSize = (size: number | string) => {
|
||||
if (typeof size === "string") return size
|
||||
if (size > 1048576) {
|
||||
return (size / 1048576).toFixed(2) + 'MB';
|
||||
}
|
||||
if (size > 1024) {
|
||||
return (size / 1024).toFixed(2) + 'KB';
|
||||
}
|
||||
return Math.floor(size) + 'b';
|
||||
}
|
||||
@@ -33,10 +33,14 @@
|
||||
"close_grab": "Stop Grabbing",
|
||||
"grab_type": "Grab Type",
|
||||
"clear_list": "Clear List",
|
||||
"clear_list_tip": "Clear all records?",
|
||||
"remember_clear_choice": "Remember this selection and clear it next time",
|
||||
"batch_download": "Batch Download",
|
||||
"batch_export": "Batch Export",
|
||||
"batch_import": "Batch Import",
|
||||
"export_url": "Export Url",
|
||||
"import_success": "Export Success",
|
||||
"total_resources": "total of {count} resources",
|
||||
"all": "All",
|
||||
"image": "Image",
|
||||
"audio": "Audio",
|
||||
@@ -48,6 +52,7 @@
|
||||
"pdf": "PDF",
|
||||
"font": "Font",
|
||||
"domain": "Domain",
|
||||
"choice": "choice",
|
||||
"type": "Type",
|
||||
"preview": "Preview",
|
||||
"preview_tip": "Preview not supported",
|
||||
@@ -58,23 +63,37 @@
|
||||
"save_path_empty": "Please set save location",
|
||||
"operation": "Operation",
|
||||
"ready": "Ready",
|
||||
"pending": "Pending",
|
||||
"running": "Running",
|
||||
"error": "Error",
|
||||
"done": "Done",
|
||||
"handle": "Post Processing",
|
||||
"direct_download": "Download",
|
||||
"download_success": "Download Success",
|
||||
"download_no_tip": "This type of download is not supported yet. Please copy the link and use other tools to download.",
|
||||
"copy_link": "Copy Link",
|
||||
"copy_data": "Copy Data",
|
||||
"open_link": "Open Link",
|
||||
"open_file": "Open File",
|
||||
"delete_row": "Delete Row",
|
||||
"cancel_down": "Cancel Download",
|
||||
"more_operation": "More Operations",
|
||||
"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"
|
||||
"import_empty": "Please enter data to import",
|
||||
"win_install_tip": "For the first time using this software, please right-click and select 'Run as administrator'",
|
||||
"download_queued": "has been added to the queue, current queue length:{count}",
|
||||
"search": "Search",
|
||||
"search_description": "Keyword Search...",
|
||||
"start_err_tip": "Error Message",
|
||||
"start_err_content": "The current startup process has encountered an issue. Do you want to reset the application?",
|
||||
"start_err_positiveText": "Clear cache and restart",
|
||||
"start_err_negativeText": "Close the software",
|
||||
"reset_app_tip": "This operation will delete intercepted data and data related to this application. Please proceed with caution!"
|
||||
},
|
||||
"setting": {
|
||||
"restart_tip": "Keep default if unsure, please restart software after modification",
|
||||
@@ -88,6 +107,8 @@
|
||||
"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",
|
||||
"insert_tail": "Insert tail",
|
||||
"insert_tail_tip": "Intercept whether new data is added to the end of the list",
|
||||
"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",
|
||||
@@ -95,9 +116,15 @@
|
||||
"user_agent_tip": "Keep default if unsure",
|
||||
"connections": "Connections",
|
||||
"connections_tip": "Keep default if unsure, usually CPU cores * 2, for faster downloads",
|
||||
"down_number": "Download Number",
|
||||
"down_number_tip": "Number of downloads executed simultaneously",
|
||||
"use_headers_tip": "Define headers for downloads, comma separated",
|
||||
"mime_map": "Intercept Rules",
|
||||
"mime_map_tip": "JSON format, keep default if unsure"
|
||||
"mime_map_tip": "JSON format, keep default if unsure, please restart software after modification",
|
||||
"port_format_error": "port format error",
|
||||
"host_format_error": "host format error",
|
||||
"basic_setting": "Basic Setting",
|
||||
"advanced_setting": "Advanced Setting"
|
||||
},
|
||||
"footer": {
|
||||
"title": "About Us",
|
||||
@@ -105,7 +132,7 @@
|
||||
"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",
|
||||
"cert_download": "Certificate Download",
|
||||
"source_code": "Source Code",
|
||||
"help": "Issues",
|
||||
"update_log": "Update Log"
|
||||
|
||||
@@ -33,10 +33,14 @@
|
||||
"close_grab": "关闭抓取",
|
||||
"grab_type": "抓取类型",
|
||||
"clear_list": "清空列表",
|
||||
"clear_list_tip": "清空所有记录?",
|
||||
"remember_clear_choice": "记住此选择,下次直接清除",
|
||||
"batch_download": "批量下载",
|
||||
"batch_export": "批量导出",
|
||||
"batch_import": "批量导入",
|
||||
"export_url": "导出链接",
|
||||
"import_success": "导出成功",
|
||||
"total_resources": "共{count}个资源",
|
||||
"all": "全部",
|
||||
"image": "图片",
|
||||
"audio": "音频",
|
||||
@@ -48,6 +52,7 @@
|
||||
"pdf": "pdf",
|
||||
"font": "字体",
|
||||
"domain": "域",
|
||||
"choice": "已选",
|
||||
"type": "类型",
|
||||
"preview": "预览",
|
||||
"preview_tip": "暂不支持预览",
|
||||
@@ -58,23 +63,37 @@
|
||||
"save_path_empty": "请设置保存位置",
|
||||
"operation": "操作",
|
||||
"ready": "就绪",
|
||||
"pending": "待处理",
|
||||
"running": "运行中",
|
||||
"error": "错误",
|
||||
"done": "完成",
|
||||
"handle": "后续处理",
|
||||
"direct_download": "直接下载",
|
||||
"download_success": "下载成功",
|
||||
"download_no_tip": "该类型暂不支持下载,请复制链接后使用其他工具下载",
|
||||
"copy_link": "复制链接",
|
||||
"copy_data": "复制数据",
|
||||
"open_link": "打开链接",
|
||||
"open_file": "打开文件",
|
||||
"delete_row": "删除记录",
|
||||
"cancel_down": "取消下载",
|
||||
"more_operation": "更多操作",
|
||||
"video_decode": "视频解密",
|
||||
"video_decode_loading": "解密中",
|
||||
"video_decode_no": "无法解密",
|
||||
"video_decode_success": "解密成功",
|
||||
"use_data": "请选择需要的数据",
|
||||
"import_placeholder": "添加多个时,请确保每行只有一个(每个链接回车换行)",
|
||||
"import_empty": "请输入需要导入的数据"
|
||||
"import_empty": "请输入需要导入的数据",
|
||||
"win_install_tip": "首次启用本软件,请使用鼠标右键选择以管理员身份运行",
|
||||
"download_queued": "已加入队列,当前队列长度:{count}",
|
||||
"search": "搜索",
|
||||
"search_description": "关键字搜索...",
|
||||
"start_err_tip": "错误提示",
|
||||
"start_err_content": "当前启动过程遇到了问题,是否重置应用?",
|
||||
"start_err_positiveText": "清理缓存并重启",
|
||||
"start_err_negativeText": "关闭软件",
|
||||
"reset_app_tip": "此操作会删除已拦截数据以及本应用相关数据,请谨慎操作!"
|
||||
},
|
||||
"setting": {
|
||||
"restart_tip": "如果不清楚保持默认就行,修改后请重启软件",
|
||||
@@ -82,12 +101,14 @@
|
||||
"filename_rules": "文件命名",
|
||||
"filename_rules_tip": "输入框控制文件命名的长度(不含时间、0为无效,此选项有描述信息时有效),开关控制文件末尾是否添加时间标识",
|
||||
"auto_proxy": "自动拦截",
|
||||
"auto_proxy_tip": "打开软件时动启用拦截",
|
||||
"auto_proxy_tip": "打开软件时自动启用拦截",
|
||||
"quality": "清晰度",
|
||||
"quality_value": "默认(推荐),超清,高画质,中画质,低画质",
|
||||
"quality_tip": "视频号有效",
|
||||
"full_intercept": "全量拦截",
|
||||
"full_intercept_tip": "微信视频号是否全量拦截,否:只拦截视频详情",
|
||||
"insert_tail": "添入尾部",
|
||||
"insert_tail_tip": "拦截到新数据是否添加到列表尾部",
|
||||
"upstream_proxy": "上游代理",
|
||||
"upstream_proxy_tip": "用于结合其他代理工具,格式: http://username:password@your.proxy.server:port",
|
||||
"download_proxy": "下载代理",
|
||||
@@ -95,9 +116,15 @@
|
||||
"user_agent_tip": "如不清楚请保持默认",
|
||||
"connections": "连接数",
|
||||
"connections_tip": "如不清楚请保持默认,通常CPU核心数*2,用于加速下载",
|
||||
"down_number": "下载数",
|
||||
"down_number_tip": "同时进行的下载数量",
|
||||
"use_headers_tip": "定义下载时可使用的header参数,逗号分割",
|
||||
"mime_map": "拦截规则",
|
||||
"mime_map_tip": "json格式,如果不清楚保持默认就行"
|
||||
"mime_map_tip": "json格式,如果不清楚保持默认就行,修改后请重启软件",
|
||||
"port_format_error": "port 格式错误",
|
||||
"host_format_error": "host 格式错误",
|
||||
"basic_setting": "基础设置",
|
||||
"advanced_setting": "高级设置"
|
||||
},
|
||||
"footer": {
|
||||
"title": "关于我们",
|
||||
@@ -105,7 +132,7 @@
|
||||
"support": "支持市面上几乎所有的网络应用",
|
||||
"application": "抖音,快手,小红书,视频号,小程序,公众号,酷狗音乐,QQ音乐,QQ微视,......",
|
||||
"forum": "论坛",
|
||||
"cert": "证书",
|
||||
"cert_download": "证书下载",
|
||||
"source_code": "软件源码",
|
||||
"help": "帮助支持",
|
||||
"update_log": "更新日志"
|
||||
|
||||
@@ -29,8 +29,10 @@ export const useIndexStore = defineStore("index-store", () => {
|
||||
AutoProxy: false,
|
||||
WxAction: false,
|
||||
TaskNumber: 8,
|
||||
DownNumber: 3,
|
||||
UserAgent: "",
|
||||
UseHeaders: "",
|
||||
InsertTail: true,
|
||||
MimeMap: {}
|
||||
})
|
||||
|
||||
@@ -40,8 +42,6 @@ export const useIndexStore = defineStore("index-store", () => {
|
||||
arch: "",
|
||||
});
|
||||
|
||||
const tableHeight = ref(800)
|
||||
|
||||
const isProxy = ref(false)
|
||||
const baseUrl = ref("")
|
||||
|
||||
@@ -59,10 +59,8 @@ export const useIndexStore = defineStore("index-store", () => {
|
||||
globalConfig.value = Object.assign({}, globalConfig.value, res.data)
|
||||
})
|
||||
|
||||
baseUrl.value = "http://"+globalConfig.value.Host + ":" +globalConfig.value.Port
|
||||
baseUrl.value = "http://127.0.0.1:" +globalConfig.value.Port
|
||||
window.$baseUrl = baseUrl.value
|
||||
window.addEventListener("resize", handleResize);
|
||||
handleResize()
|
||||
}
|
||||
|
||||
const setConfig = (formValue: Object) => {
|
||||
@@ -70,10 +68,6 @@ export const useIndexStore = defineStore("index-store", () => {
|
||||
appApi.setConfig(globalConfig.value)
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
tableHeight.value = document.documentElement.clientHeight || window.innerHeight
|
||||
}
|
||||
|
||||
const openProxy = async () => {
|
||||
return appApi.openSystemProxy().then(handleProxy)
|
||||
}
|
||||
@@ -93,7 +87,6 @@ export const useIndexStore = defineStore("index-store", () => {
|
||||
return {
|
||||
appInfo,
|
||||
globalConfig,
|
||||
tableHeight,
|
||||
isProxy,
|
||||
envInfo,
|
||||
baseUrl,
|
||||
|
||||
4
frontend/src/types/app.d.ts
vendored
4
frontend/src/types/app.d.ts
vendored
@@ -26,8 +26,10 @@ export namespace appType {
|
||||
AutoProxy: boolean
|
||||
WxAction: boolean
|
||||
TaskNumber: number
|
||||
DownNumber: number
|
||||
UserAgent: string
|
||||
UseHeaders: string
|
||||
InsertTail: boolean
|
||||
MimeMap: { [key: string]: MimeMap }
|
||||
}
|
||||
|
||||
@@ -36,7 +38,7 @@ export namespace appType {
|
||||
Url: string
|
||||
UrlSign: string
|
||||
CoverUrl: string
|
||||
Size: string
|
||||
Size: number
|
||||
Domain: string
|
||||
Classify: string
|
||||
Suffix: string
|
||||
|
||||
@@ -1,33 +1,114 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col p-5 overflow-y-auto [&::-webkit-scrollbar]:hidden">
|
||||
<div class="pb-2 z-40">
|
||||
<div class="h-full flex flex-col px-5 pt-5 overflow-y-auto [&::-webkit-scrollbar]:hidden">
|
||||
<div class="pb-2 z-40" id="header">
|
||||
<NSpace>
|
||||
<NButton v-if="isProxy" secondary type="primary" @click.stop="close" style="--wails-draggable:no-drag">
|
||||
{{ t("index.close_grab") }}
|
||||
<span class="inline-block w-1.5 h-1.5 bg-red-600 rounded-full mr-1 animate-pulse"></span>
|
||||
{{ t("index.close_grab") }}{{ data.length > 0 ? ` ${t('index.total_resources', {count: data.length})}` : '' }}
|
||||
</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") }}
|
||||
{{ t("index.open_grab") }}{{ data.length > 0 ? ` ${t('index.total_resources', {count: data.length})}` : '' }}
|
||||
</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>
|
||||
<NButtonGroup style="--wails-draggable:no-drag">
|
||||
|
||||
<NButton v-if="rememberChoice" tertiary type="error" @click.stop="clear" style="--wails-draggable:no-drag">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<TrashOutline/>
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t("index.clear_list") }}
|
||||
</NButton>
|
||||
<n-popconfirm
|
||||
v-else
|
||||
@positive-click="()=>{rememberChoice=rememberChoiceTmp;clear()}"
|
||||
:show-icon="false"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton tertiary type="error" style="--wails-draggable:no-drag">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<TrashOutline/>
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t("index.clear_list") }}
|
||||
</NButton>
|
||||
</template>
|
||||
<div>
|
||||
<div class="flex flex-row items-center text-red-700 my-2 text-base">
|
||||
<n-icon>
|
||||
<TrashOutline/>
|
||||
</n-icon>
|
||||
<p class="ml-1">{{ t("index.clear_list_tip") }}</p>
|
||||
</div>
|
||||
<NCheckbox
|
||||
v-model:checked="rememberChoiceTmp"
|
||||
>
|
||||
<span class="text-gray-400">{{ t('index.remember_clear_choice') }}</span>
|
||||
</NCheckbox>
|
||||
</div>
|
||||
</n-popconfirm>
|
||||
|
||||
<NButton tertiary type="primary" @click.stop="batchDown">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<DownloadOutline/>
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('index.batch_download') }}
|
||||
</NButton>
|
||||
<NButton tertiary type="info">
|
||||
<NPopover placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="">
|
||||
<Apps/>
|
||||
</NIcon>
|
||||
</template>
|
||||
<div class="flex flex-col">
|
||||
<NButton tertiary type="error" @click.stop="batchCancel" class="my-1">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<CloseOutline/>
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('index.cancel_down') }}
|
||||
</NButton>
|
||||
<NButton tertiary type="warning" @click.stop="batchExport()" class="my-1">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowRedoCircleOutline/>
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('index.batch_export') }}
|
||||
</NButton>
|
||||
<NButton tertiary type="info" @click.stop="showImport=true" class="my-1">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ServerOutline/>
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('index.batch_import') }}
|
||||
</NButton>
|
||||
<NButton tertiary type="primary" @click.stop="batchExport('url')" class="my-1">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowRedoCircleOutline/>
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('index.export_url') }}
|
||||
</NButton>
|
||||
</div>
|
||||
</NPopover>
|
||||
</NButton>
|
||||
</NButtonGroup>
|
||||
</NSpace>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<NDataTable
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:data="filteredData"
|
||||
:bordered="false"
|
||||
:max-height="tableHeight"
|
||||
:row-key="rowKey"
|
||||
@@ -36,9 +117,16 @@
|
||||
:height-for-row="()=> 48"
|
||||
:checked-row-keys="checkedRowKeysValue"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
@update:filters="updateFilters"
|
||||
style="--wails-draggable:no-drag"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center items-center text-blue-400" id="bottom">
|
||||
<span class="cursor-pointer px-2 py-1" @click="BrowserOpenURL(certUrl)">{{ t('footer.cert_download') }}</span>
|
||||
<span class="cursor-pointer px-2 py-1" @click="BrowserOpenURL('https://github.com/putyy/res-downloader')">{{ t('footer.source_code') }}</span>
|
||||
<span class="cursor-pointer px-2 py-1" @click="BrowserOpenURL('https://github.com/putyy/res-downloader/issues')">{{ t('footer.help') }}</span>
|
||||
<span class="cursor-pointer px-2 py-1" @click="BrowserOpenURL('https://github.com/putyy/res-downloader/releases')">{{ t('footer.update_log') }}</span>
|
||||
</div>
|
||||
<Preview v-model:showModal="showPreviewRow" :previewRow="previewRow"/>
|
||||
<ShowLoading :loadingText="loadingText" :isLoading="loading"/>
|
||||
<ImportJson v-model:showModal="showImport" @submit="handleImport"/>
|
||||
@@ -47,34 +135,64 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {NButton, NImage, NTooltip} from "naive-ui"
|
||||
import {computed, h, onMounted, ref, reactive, watch} from "vue"
|
||||
import {NButton, NIcon, NImage, NInput, NSpace, NTooltip, NPopover, NGradientText} from "naive-ui"
|
||||
import {computed, h, onMounted, ref, watch} from "vue"
|
||||
import type {appType} from "@/types/app"
|
||||
|
||||
import type {DataTableRowKey, ImageRenderToolbarProps} from "naive-ui"
|
||||
import type {DataTableRowKey, ImageRenderToolbarProps, DataTableFilterState, DataTableBaseColumn} from "naive-ui"
|
||||
import Preview from "@/components/Preview.vue"
|
||||
import ShowLoading from "@/components/ShowLoading.vue"
|
||||
// @ts-ignore
|
||||
import {getDecryptionArray} from '@/assets/js/decrypt.js'
|
||||
import {useIndexStore} from "@/stores"
|
||||
import appApi from "@/api/app"
|
||||
import ResAction from "@/components/ResAction.vue"
|
||||
import Action from "@/components/Action.vue"
|
||||
import ActionDesc from "@/components/ActionDesc.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'
|
||||
import {
|
||||
DownloadOutline,
|
||||
ArrowRedoCircleOutline,
|
||||
ServerOutline,
|
||||
SearchOutline,
|
||||
Apps,
|
||||
TrashOutline, CloseOutline
|
||||
} from "@vicons/ionicons5"
|
||||
import {useDialog} from 'naive-ui'
|
||||
import * as bind from "../../wailsjs/go/core/Bind"
|
||||
import {Quit} from "../../wailsjs/runtime"
|
||||
import {DialogOptions} from "naive-ui/es/dialog/src/DialogProvider"
|
||||
import {formatSize} from "@/func"
|
||||
|
||||
const {t} = useI18n()
|
||||
const eventStore = useEventStore()
|
||||
const dialog = useDialog()
|
||||
const isProxy = computed(() => {
|
||||
return store.isProxy
|
||||
})
|
||||
const data = ref<any[]>([])
|
||||
const store = useIndexStore()
|
||||
const tableHeight = computed(() => {
|
||||
return store.globalConfig.Locale === "zh" ? store.tableHeight - 130 : store.tableHeight - 151
|
||||
const certUrl = computed(() => {
|
||||
return store.baseUrl + "/api/cert"
|
||||
})
|
||||
const data = ref<any[]>([])
|
||||
const filterClassify = ref<string[]>([])
|
||||
const filteredData = computed(() => {
|
||||
let result = data.value
|
||||
|
||||
if (filterClassify.value.length > 0) {
|
||||
result = result.filter(item => filterClassify.value.includes(item.Classify))
|
||||
}
|
||||
|
||||
if (descriptionSearchValue.value) {
|
||||
result = result.filter(item => item.Description?.toLowerCase().includes(descriptionSearchValue.value.toLowerCase()))
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const store = useIndexStore()
|
||||
const tableHeight = ref(800)
|
||||
const resourcesType = ref<string[]>(["all"])
|
||||
|
||||
const classifyAlias: { [key: string]: any } = {
|
||||
@@ -84,21 +202,26 @@ const classifyAlias: { [key: string]: any } = {
|
||||
m3u8: computed(() => t("index.m3u8")),
|
||||
live: computed(() => t("index.live")),
|
||||
xls: computed(() => t("index.xls")),
|
||||
doc: computed(() => t("index.pdf")),
|
||||
doc: computed(() => t("index.doc")),
|
||||
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")
|
||||
ready: t("index.ready"),
|
||||
pending: t("index.pending"),
|
||||
running: t("index.running"),
|
||||
error: t("index.error"),
|
||||
done: t("index.done"),
|
||||
handle: t("index.handle")
|
||||
}
|
||||
})
|
||||
|
||||
const maxConcurrentDownloads = computed(() => {
|
||||
return store.globalConfig.DownNumber
|
||||
})
|
||||
|
||||
const classify = ref([
|
||||
{
|
||||
value: "all",
|
||||
@@ -106,43 +229,49 @@ const classify = ref([
|
||||
},
|
||||
])
|
||||
|
||||
const descriptionSearchValue = ref("")
|
||||
const rememberChoice = ref(false)
|
||||
const rememberChoiceTmp = ref(false)
|
||||
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
type: "selection",
|
||||
},
|
||||
{
|
||||
title: computed(() => t("index.domain")),
|
||||
title: computed(() => {
|
||||
return checkedRowKeysValue.value.length > 0 ? h(NGradientText, {type: "success"}, t("index.choice") + `(${checkedRowKeysValue.value.length})`) : t("index.domain")
|
||||
}),
|
||||
key: "Domain",
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: computed(() => t("index.type")),
|
||||
key: "Classify",
|
||||
width: 80,
|
||||
filterOptions: computed(() => Array.from(classify.value).slice(1)),
|
||||
filterMultiple: true,
|
||||
filter: (value: string, row: appType.MediaInfo) => {
|
||||
return !!~row.Classify.indexOf(String(value))
|
||||
},
|
||||
render: (row: appType.MediaInfo) => {
|
||||
for (const key in classify.value) {
|
||||
if (classify.value[key].value === row.Classify) {
|
||||
return classify.value[key].label;
|
||||
}
|
||||
}
|
||||
return row.Classify;
|
||||
const item = classify.value.find(item => item.value === row.Classify)
|
||||
return item ? item.label : row.Classify
|
||||
}
|
||||
},
|
||||
{
|
||||
title: computed(() => t("index.preview")),
|
||||
key: "Url",
|
||||
width: 120,
|
||||
width: 80,
|
||||
render: (row: appType.MediaInfo) => {
|
||||
if (row.Classify === "image") {
|
||||
return h(NImage, {
|
||||
maxWidth: "80px",
|
||||
return h("div", {
|
||||
style: "width: 100%;max-height:80px;overflow:hidden;"
|
||||
}, h(NImage, {
|
||||
objectFit: "contain",
|
||||
lazy: true,
|
||||
"render-toolbar": renderToolbar,
|
||||
src: row.Url
|
||||
})
|
||||
}))
|
||||
}
|
||||
return [
|
||||
h(
|
||||
@@ -177,30 +306,84 @@ const columns = ref<any[]>([
|
||||
{
|
||||
title: computed(() => t("index.status")),
|
||||
key: "Status",
|
||||
render: (row: appType.MediaInfo) => {
|
||||
return dwStatus[row.Status as keyof typeof dwStatus]
|
||||
width: 80,
|
||||
render: (row: appType.MediaInfo, index: number) => {
|
||||
let status = "info"
|
||||
if (row.Status === "done" || row.Status === "running") {
|
||||
status = "success"
|
||||
} else if (row.Status === "pending") {
|
||||
status = "warning"
|
||||
}
|
||||
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
type: status as any,
|
||||
size: "small",
|
||||
style: {
|
||||
margin: "2px"
|
||||
},
|
||||
onClick: () => {
|
||||
if (row.SavePath && row.Status === "done") {
|
||||
appApi.openFolder({filePath: row.SavePath})
|
||||
} else if (row.Status === "ready") {
|
||||
download(row, index)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return row.Status === "running" ? row.SavePath : dwStatus.value[row.Status as keyof typeof dwStatus]
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: computed(() => t("index.description")),
|
||||
title: () => h('div', {class: 'flex items-center'}, [
|
||||
t('index.description'),
|
||||
h(NPopover, {
|
||||
style: "--wails-draggable:no-drag",
|
||||
trigger: 'click',
|
||||
placement: 'bottom',
|
||||
showArrow: true,
|
||||
}, {
|
||||
trigger: () => h(NIcon, {
|
||||
size: "18",
|
||||
class: "ml-1 text-gray-500 cursor-pointer",
|
||||
onClick: (e: MouseEvent) => e.stopPropagation()
|
||||
}, h(SearchOutline)),
|
||||
default: () => h('div', {class: 'p-2 w-64'}, [
|
||||
h(NInput, {
|
||||
value: descriptionSearchValue.value,
|
||||
'onUpdate:value': (val: string) => descriptionSearchValue.value = val,
|
||||
placeholder: t('index.search_description'),
|
||||
clearable: true
|
||||
}, {
|
||||
prefix: () => h(NIcon, {component: SearchOutline})
|
||||
})
|
||||
])
|
||||
})
|
||||
]),
|
||||
key: "Description",
|
||||
width: 150,
|
||||
render: (row: appType.MediaInfo, index: number) => {
|
||||
const d = h("div", {class: "ellipsis-2",}, row.Description)
|
||||
return h(NTooltip, {trigger: 'hover', placement: 'top'}, {
|
||||
trigger: () => h("div", {}, row.Description.length > 16 ? row.Description.substring(0, 16) + "..." : row.Description),
|
||||
default: () => h("div", {
|
||||
style: {
|
||||
"max-width": " 400px",
|
||||
"white-space": "normal",
|
||||
"word-wrap": "break-word"
|
||||
}
|
||||
}, row.Description)
|
||||
});
|
||||
trigger: () => d,
|
||||
default: () => d
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: computed(() => t("index.resource_size")),
|
||||
key: "Size"
|
||||
key: "Size",
|
||||
width: 120,
|
||||
sorter: (row1: appType.MediaInfo, row2: appType.MediaInfo) => row1.Size - row2.Size,
|
||||
render(row: appType.MediaInfo, index: number) {
|
||||
return formatSize(row.Size)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: computed(() => t("index.save_path")),
|
||||
@@ -209,6 +392,7 @@ const columns = ref<any[]>([
|
||||
return h("a",
|
||||
{
|
||||
href: "javascript:;",
|
||||
class: "ellipsis-2",
|
||||
style: {
|
||||
color: "#5a95d0"
|
||||
},
|
||||
@@ -218,19 +402,22 @@ const columns = ref<any[]>([
|
||||
}
|
||||
}
|
||||
},
|
||||
row.SavePath
|
||||
row.Status === "running" ? "" : row.SavePath
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: computed(() => t("index.operation")),
|
||||
key: "actions",
|
||||
width: 130,
|
||||
render(row: appType.MediaInfo, index: number) {
|
||||
return h(ResAction, {key: index, row: row, index: index, onAction: dataAction})
|
||||
return h(Action, {key: index, row: row, index: index, onAction: dataAction})
|
||||
},
|
||||
title() {
|
||||
return h(ActionDesc)
|
||||
}
|
||||
}
|
||||
])
|
||||
const downIndex = ref(0)
|
||||
|
||||
const checkedRowKeysValue = ref<DataTableRowKey[]>([])
|
||||
const showPreviewRow = ref(false)
|
||||
const previewRow = ref<appType.MediaInfo>()
|
||||
@@ -238,8 +425,32 @@ const loading = ref(false)
|
||||
const loadingText = ref("")
|
||||
const showImport = ref(false)
|
||||
const showPassword = ref(false)
|
||||
const downloadQueue = ref<appType.MediaInfo[]>([])
|
||||
let activeDownloads = 0
|
||||
let isOpenProxy = false
|
||||
let isInstall = false
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
window.addEventListener("resize", () => {
|
||||
resetTableHeight()
|
||||
})
|
||||
loading.value = true
|
||||
handleInstall().then((is: boolean) => {
|
||||
isInstall = true
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
checkLoading()
|
||||
watch(showPassword, () => {
|
||||
if (!showPassword.value) {
|
||||
checkLoading()
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
window.$message?.error(JSON.stringify(e), {duration: 5000})
|
||||
}
|
||||
|
||||
buildClassify()
|
||||
|
||||
const temp = localStorage.getItem("resources-type")
|
||||
@@ -254,11 +465,30 @@ onMounted(() => {
|
||||
data.value = JSON.parse(cache)
|
||||
}
|
||||
|
||||
const choiceCache = localStorage.getItem("remember-clear-choice")
|
||||
if (choiceCache === "1") {
|
||||
rememberChoice.value = true
|
||||
}
|
||||
|
||||
watch(rememberChoice, (n, o) => {
|
||||
if (rememberChoice.value) {
|
||||
localStorage.setItem("remember-clear-choice", "1")
|
||||
} else {
|
||||
localStorage.removeItem("remember-clear-choice")
|
||||
}
|
||||
})
|
||||
|
||||
resetTableHeight()
|
||||
|
||||
eventStore.addHandle({
|
||||
type: "newResources",
|
||||
event: (res: appType.MediaInfo) => {
|
||||
data.value.push(res)
|
||||
localStorage.setItem("resources-data", JSON.stringify(data.value))
|
||||
if (store.globalConfig.InsertTail) {
|
||||
data.value.push(res)
|
||||
} else {
|
||||
data.value.unshift(res)
|
||||
}
|
||||
cacheData()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -267,30 +497,33 @@ onMounted(() => {
|
||||
event: (res: { Id: string, SavePath: string, Status: string, Message: string }) => {
|
||||
switch (res.Status) {
|
||||
case "running":
|
||||
loading.value = true
|
||||
loadingText.value = res.Message
|
||||
break;
|
||||
updateItem(res.Id, item => {
|
||||
item.SavePath = res.Message
|
||||
item.Status = 'running'
|
||||
})
|
||||
break
|
||||
case "done":
|
||||
loading.value = false
|
||||
if (data.value[downIndex.value]?.Id === res.Id) {
|
||||
data.value[downIndex.value].SavePath = res.SavePath
|
||||
data.value[downIndex.value].Status = "done"
|
||||
} else {
|
||||
for (const i in data.value) {
|
||||
if (data.value[i].Id === res.Id) {
|
||||
data.value[i].SavePath = res.SavePath
|
||||
data.value[i].Status = "done"
|
||||
break
|
||||
}
|
||||
}
|
||||
updateItem(res.Id, item => {
|
||||
item.SavePath = res.SavePath
|
||||
item.Status = 'done'
|
||||
})
|
||||
if (activeDownloads > 0) {
|
||||
activeDownloads--
|
||||
}
|
||||
localStorage.setItem("resources-data", JSON.stringify(data.value))
|
||||
window?.$message?.success(t("index.download_success"))
|
||||
break;
|
||||
cacheData()
|
||||
checkQueue()
|
||||
break
|
||||
case "error":
|
||||
loading.value = false
|
||||
window?.$message?.error(res.Message)
|
||||
break;
|
||||
updateItem(res.Id, item => {
|
||||
item.SavePath = res.Message
|
||||
item.Status = 'error'
|
||||
})
|
||||
if (activeDownloads > 0) {
|
||||
activeDownloads--
|
||||
}
|
||||
cacheData()
|
||||
checkQueue()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -307,6 +540,28 @@ watch(resourcesType, (n, o) => {
|
||||
appApi.setType(resourcesType.value)
|
||||
})
|
||||
|
||||
const updateItem = (id: string, updater: (item: any) => void) => {
|
||||
const item = data.value.find(i => i.Id === id)
|
||||
if (item) updater(item)
|
||||
}
|
||||
|
||||
function cacheData() {
|
||||
localStorage.setItem("resources-data", JSON.stringify(data.value))
|
||||
}
|
||||
|
||||
const resetTableHeight = () => {
|
||||
try {
|
||||
const headerHeight = document.getElementById("header")?.offsetHeight || 0
|
||||
const bottomHeight = document.getElementById("bottom")?.offsetHeight || 0
|
||||
// @ts-ignore
|
||||
const theadHeight = document.getElementsByClassName("n-data-table-thead")[0]?.offsetHeight || 0
|
||||
const height = document.documentElement.clientHeight || window.innerHeight
|
||||
tableHeight.value = height - headerHeight - bottomHeight - theadHeight - 20
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
const buildClassify = () => {
|
||||
const mimeMap = store.globalConfig.MimeMap ?? {}
|
||||
const seen = new Set()
|
||||
@@ -314,9 +569,9 @@ const buildClassify = () => {
|
||||
{value: "all", label: computed(() => t("index.all"))},
|
||||
...Object.values(mimeMap)
|
||||
.filter(({Type}) => {
|
||||
if (seen.has(Type)) return false;
|
||||
seen.add(Type);
|
||||
return true;
|
||||
if (seen.has(Type)) return false
|
||||
seen.add(Type)
|
||||
return true
|
||||
})
|
||||
.map(({Type}) => ({
|
||||
value: Type,
|
||||
@@ -328,8 +583,27 @@ const buildClassify = () => {
|
||||
const dataAction = (row: appType.MediaInfo, index: number, type: string) => {
|
||||
switch (type) {
|
||||
case "down":
|
||||
download(row, index);
|
||||
break;
|
||||
download(row, index)
|
||||
break
|
||||
case "cancel":
|
||||
if (row.Status === "running") {
|
||||
appApi.cancel({id: row.Id}).then((res) => {
|
||||
updateItem(row.Id, item => {
|
||||
item.Status = 'ready'
|
||||
item.SavePath = ''
|
||||
})
|
||||
if (activeDownloads > 0) {
|
||||
activeDownloads--
|
||||
}
|
||||
cacheData()
|
||||
checkQueue()
|
||||
if (res.code === 0) {
|
||||
window?.$message?.error(res.message)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
break
|
||||
case "copy":
|
||||
ClipboardSetText(row.Url).then((is: boolean) => {
|
||||
if (is) {
|
||||
@@ -350,16 +624,14 @@ const dataAction = (row: appType.MediaInfo, index: number, type: string) => {
|
||||
break
|
||||
case "open":
|
||||
BrowserOpenURL(row.Url)
|
||||
break;
|
||||
break
|
||||
case "decode":
|
||||
decodeWxFile(row, index)
|
||||
break;
|
||||
break
|
||||
case "delete":
|
||||
appApi.delete({sign: row.UrlSign}).then(() => {
|
||||
let arr = data.value
|
||||
arr.splice(index, 1);
|
||||
data.value = arr
|
||||
localStorage.setItem("resources-data", JSON.stringify(data.value))
|
||||
data.value.splice(index, 1)
|
||||
cacheData()
|
||||
})
|
||||
break
|
||||
}
|
||||
@@ -384,38 +656,78 @@ const handleCheck = (rowKeys: DataTableRowKey[]) => {
|
||||
checkedRowKeysValue.value = rowKeys
|
||||
}
|
||||
|
||||
const batchDown = async () => {
|
||||
if (checkedRowKeysValue.value.length <= 0) {
|
||||
return
|
||||
}
|
||||
if (!store.globalConfig.SaveDirectory) {
|
||||
window?.$message?.error(t("index.save_path_empty"))
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < data.value.length; i++) {
|
||||
if (checkedRowKeysValue.value.includes(data.value[i].Id) && data.value[i].Classify != "live" && data.value[i].Classify != "m3u8") {
|
||||
download(data.value[i], i)
|
||||
await checkVariable()
|
||||
}
|
||||
}
|
||||
const updateFilters = (filters: DataTableFilterState, initiatorColumn: DataTableBaseColumn) => {
|
||||
filterClassify.value = filters.Classify as string[]
|
||||
}
|
||||
|
||||
const batchImport = () => {
|
||||
const batchDown = async () => {
|
||||
if (checkedRowKeysValue.value.length <= 0) {
|
||||
window?.$message?.error(t("index.use_data"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!store.globalConfig.SaveDirectory) {
|
||||
window?.$message?.error(t("index.save_path_empty"))
|
||||
return
|
||||
}
|
||||
|
||||
data.value.forEach((item, index) => {
|
||||
if (checkedRowKeysValue.value.includes(item.Id) && item.Classify !== 'live' && item.Classify !== 'm3u8') {
|
||||
download(item, index)
|
||||
}
|
||||
})
|
||||
|
||||
checkedRowKeysValue.value = []
|
||||
}
|
||||
|
||||
const batchCancel = async () => {
|
||||
if (checkedRowKeysValue.value.length <= 0) {
|
||||
window?.$message?.error(t("index.use_data"))
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
const cancelTasks: Promise<any>[] = []
|
||||
data.value.forEach((item, index) => {
|
||||
if (checkedRowKeysValue.value.includes(item.Id) && item.Status === "running") {
|
||||
if (activeDownloads > 0) {
|
||||
activeDownloads--
|
||||
}
|
||||
cancelTasks.push(appApi.cancel({id: item.Id}).then(() => {
|
||||
item.Status = 'ready'
|
||||
item.SavePath = ''
|
||||
checkQueue()
|
||||
}))
|
||||
}
|
||||
})
|
||||
await Promise.allSettled(cancelTasks)
|
||||
loading.value = false
|
||||
checkedRowKeysValue.value = []
|
||||
cacheData()
|
||||
}
|
||||
|
||||
const batchExport = (type?: string) => {
|
||||
if (checkedRowKeysValue.value.length <= 0) {
|
||||
window?.$message?.error(t("index.use_data"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!store.globalConfig.SaveDirectory) {
|
||||
window?.$message?.error(t("index.save_path_empty"))
|
||||
return
|
||||
}
|
||||
|
||||
loadingText.value = t("common.loading")
|
||||
loading.value = true
|
||||
let jsonData = []
|
||||
for (let i = 0; i < data.value.length; i++) {
|
||||
jsonData.push(encodeURIComponent(JSON.stringify(data.value[i])))
|
||||
|
||||
let jsonData = data.value.filter(item => checkedRowKeysValue.value.includes(item.Id))
|
||||
|
||||
if (type === "url") {
|
||||
jsonData = jsonData.map(item => item.Url)
|
||||
} else {
|
||||
jsonData = jsonData.map(item => encodeURIComponent(JSON.stringify(item)))
|
||||
}
|
||||
appApi.batchImport({content: jsonData.join("\n")}).then((res: appType.Res) => {
|
||||
|
||||
appApi.batchExport({content: jsonData.join("\n")}).then((res: appType.Res) => {
|
||||
loading.value = false
|
||||
if (res.code === 0) {
|
||||
window?.$message?.error(res.message)
|
||||
@@ -426,27 +738,10 @@ const batchImport = () => {
|
||||
duration: 5000
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
const uint8ArrayToBase64 = (bytes: any) => {
|
||||
let binary = '';
|
||||
const len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
async function checkVariable() {
|
||||
return new Promise((resolve) => {
|
||||
const interval = setInterval(() => {
|
||||
if (!loading.value) {
|
||||
clearInterval(interval)
|
||||
resolve(true)
|
||||
}
|
||||
}, 600);
|
||||
});
|
||||
return window.btoa(Array.from(bytes, (byte: any) => String.fromCharCode(byte)).join(''))
|
||||
}
|
||||
|
||||
const download = (row: appType.MediaInfo, index: number) => {
|
||||
@@ -454,36 +749,62 @@ const download = (row: appType.MediaInfo, index: number) => {
|
||||
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) => {
|
||||
if (res.code === 0) {
|
||||
loading.value = false
|
||||
window?.$message?.error(res.message)
|
||||
|
||||
if (data.value.some(item => item.Id === row.Id && item.Status === "running")) {
|
||||
return
|
||||
}
|
||||
|
||||
if (downloadQueue.value.some(item => item.Id === row.Id || item.Url === row.Url)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (activeDownloads >= maxConcurrentDownloads.value) {
|
||||
row.Status = "pending"
|
||||
downloadQueue.value.push(row)
|
||||
window?.$message?.info(t("index.download_queued", {count: downloadQueue.value.length}))
|
||||
return
|
||||
}
|
||||
|
||||
startDownload(row, index)
|
||||
}
|
||||
|
||||
const startDownload = (row: appType.MediaInfo, index: number) => {
|
||||
activeDownloads++
|
||||
|
||||
const decodeStr = row.DecodeKey
|
||||
? uint8ArrayToBase64(getDecryptionArray(row.DecodeKey))
|
||||
: ""
|
||||
|
||||
appApi.download({...row, decodeStr}).then((res: appType.Res) => {
|
||||
if (res.code === 0) {
|
||||
window?.$message?.error(res.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const checkQueue = () => {
|
||||
if (downloadQueue.value.length > 0 && activeDownloads < maxConcurrentDownloads.value) {
|
||||
const nextItem = downloadQueue.value.shift()
|
||||
if (nextItem) {
|
||||
const index = data.value.findIndex(item => item.Id === nextItem.Id)
|
||||
if (index !== -1) {
|
||||
startDownload(nextItem, index)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
appApi.download({...row, decodeStr: ""}).then((res: appType.Res) => {
|
||||
if (res.code === 0) {
|
||||
loading.value = false
|
||||
window?.$message?.error(res.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
isOpenProxy = true
|
||||
store.openProxy().then((res: appType.Res) => {
|
||||
if (res.code === 1) {
|
||||
return
|
||||
}
|
||||
if (store.envInfo.platform === "darwin" || store.envInfo.platform === "linux") {
|
||||
|
||||
if (["darwin", "linux"].includes(store.envInfo.platform)) {
|
||||
showPassword.value = true
|
||||
} else {
|
||||
window.$message?.error(res.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -493,6 +814,21 @@ const close = () => {
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
if (checkedRowKeysValue.value.length > 0) {
|
||||
let newData = [] as any[]
|
||||
data.value.forEach((item, index) => {
|
||||
if (checkedRowKeysValue.value.includes(item.Id)) {
|
||||
appApi.delete({sign: item.UrlSign})
|
||||
} else {
|
||||
newData.push(item)
|
||||
}
|
||||
})
|
||||
data.value = newData
|
||||
checkedRowKeysValue.value = []
|
||||
cacheData()
|
||||
return
|
||||
}
|
||||
|
||||
data.value = []
|
||||
localStorage.setItem("resources-data", "")
|
||||
appApi.clear()
|
||||
@@ -523,7 +859,7 @@ 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))
|
||||
cacheData()
|
||||
window?.$message?.success(t("index.video_decode_success"))
|
||||
})
|
||||
}
|
||||
@@ -532,9 +868,10 @@ const decodeWxFile = (row: appType.MediaInfo, index: number) => {
|
||||
|
||||
const handleImport = (content: string) => {
|
||||
if (!content) {
|
||||
// window?.$message?.error(t("view_index.import_empty"))
|
||||
window?.$message?.error(t("view.import_empty"))
|
||||
return
|
||||
}
|
||||
let newItems = [] as any[]
|
||||
content.split("\n").forEach((line, index) => {
|
||||
try {
|
||||
let res = JSON.parse(decodeURIComponent(line))
|
||||
@@ -542,24 +879,86 @@ const handleImport = (content: string) => {
|
||||
res.Id = res.Id + Math.floor(Math.random() * 100000)
|
||||
res.SavePath = ""
|
||||
res.Status = "ready"
|
||||
data.value.unshift(res)
|
||||
newItems.push(res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
});
|
||||
localStorage.setItem("resources-data", JSON.stringify(data.value))
|
||||
})
|
||||
if (newItems.length > 0) {
|
||||
data.value = [...newItems, ...data.value]
|
||||
cacheData()
|
||||
}
|
||||
showImport.value = false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
const handlePassword = async (password: string, isCache: boolean) => {
|
||||
const res = await appApi.setSystemPassword({password, isCache})
|
||||
if (res.code === 0) {
|
||||
window.$message?.error(res.message)
|
||||
return
|
||||
}
|
||||
|
||||
if (isOpenProxy) {
|
||||
showPassword.value = false
|
||||
store.openProxy()
|
||||
return
|
||||
}
|
||||
|
||||
handleInstall().then((is: boolean) => {
|
||||
if (is) {
|
||||
showPassword.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
const handleInstall = async () => {
|
||||
isOpenProxy = false
|
||||
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(t("index.win_install_tip"))
|
||||
} else if (["darwin", "linux"].includes(store.envInfo.platform)) {
|
||||
showPassword.value = true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const checkLoading = () => {
|
||||
setTimeout(() => {
|
||||
if (loading.value && !isInstall && !showPassword.value) {
|
||||
dialog.warning({
|
||||
title: t("index.start_err_tip"),
|
||||
content: t("index.start_err_content"),
|
||||
positiveText: t("index.start_err_positiveText"),
|
||||
negativeText: t("index.start_err_negativeText"),
|
||||
draggable: false,
|
||||
closeOnEsc: false,
|
||||
closable: false,
|
||||
maskClosable: false,
|
||||
onPositiveClick: () => {
|
||||
bind.ResetApp()
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
Quit()
|
||||
}
|
||||
} as DialogOptions)
|
||||
}
|
||||
}, 6000)
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.ellipsis-2 {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
</style>
|
||||
@@ -1,176 +1,221 @@
|
||||
<template>
|
||||
<div class="h-full relative p-5 overflow-y-auto [&::-webkit-scrollbar]:hidden" :key="renderKey">
|
||||
<NForm
|
||||
:model="formValue"
|
||||
size="medium"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
require-mark-placement="right-hanging"
|
||||
style="--wails-draggable:no-drag"
|
||||
class="w-[700px]"
|
||||
>
|
||||
<NFormItem label="Host" path="Host">
|
||||
<NInput v-model:value="formValue.Host" placeholder="127.0.0.1"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.restart_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
<NTabs type="line" animated>
|
||||
<NTabPane name="basic" :tab="t('setting.basic_setting')">
|
||||
<NForm
|
||||
:model="formValue"
|
||||
size="medium"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
require-mark-placement="right-hanging"
|
||||
style="--wails-draggable:no-drag"
|
||||
class="w-[700px]"
|
||||
>
|
||||
<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="Port" path="Port">
|
||||
<NInput v-model:value="formValue.Port" placeholder="8899"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.restart_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
<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">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.filename_rules_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<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>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.upstream_proxy_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
<NFormItem :label="t('setting.quality')" path="Quality">
|
||||
<NSelect v-model:value="formValue.Quality" :options="options"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.quality_tip") }}
|
||||
</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="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>
|
||||
|
||||
<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">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.filename_rules_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
<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>
|
||||
|
||||
<NFormItem :label="t('setting.quality')" path="Quality">
|
||||
<NSelect v-model:value="formValue.Quality" :options="options"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.quality_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
</div>
|
||||
<NFormItem :label="t('setting.insert_tail')" path="InsertTail">
|
||||
<NSwitch v-model:value="formValue.InsertTail"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.insert_tail_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<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 >
|
||||
<n-popconfirm @positive-click="resetHandle">
|
||||
<template #trigger>
|
||||
<NButton tertiary type="error" style="--wails-draggable:no-drag">
|
||||
{{ t("index.start_err_positiveText") }}
|
||||
</NButton>
|
||||
</template>
|
||||
{{t("index.reset_app_tip")}}
|
||||
</n-popconfirm>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NTabPane>
|
||||
|
||||
<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>
|
||||
<NTabPane name="advanced" :tab="t('setting.advanced_setting')">
|
||||
<NForm
|
||||
:model="formValue"
|
||||
size="medium"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
require-mark-placement="right-hanging"
|
||||
style="--wails-draggable:no-drag"
|
||||
class="w-[700px]"
|
||||
>
|
||||
<NFormItem label="Host" path="Host" :validation-status="hostValidationFeedback==='' ? undefined : 'error'" :feedback="hostValidationFeedback">
|
||||
<NInput v-model:value="formValue.Host" placeholder="127.0.0.1"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.restart_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<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="Port" path="Port" :validation-status="portValidationFeedback==='' ? undefined : 'error'" :feedback="portValidationFeedback">
|
||||
<NInput v-model:value="formValue.Port" placeholder="8899"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.restart_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>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.connections_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
</div>
|
||||
<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>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.upstream_proxy_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="UserAgent" path="UserAgent">
|
||||
<NInput v-model:value="formValue.UserAgent" placeholder="默认UserAgent"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.user_agent_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
<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="Headers" path="Headers">
|
||||
<NInput v-model:value="formValue.UseHeaders" placeholder="User-Agent,Referer,Authorization,Cookie"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.use_headers_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>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.connections_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="t('setting.mime_map')" path="MimeMap">
|
||||
<NInput
|
||||
v-model:value="MimeMap"
|
||||
type="textarea"
|
||||
rows="11"
|
||||
placeholder='{"video/mp4": { "Type": "video","Suffix": ".mp4"}}'
|
||||
/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.mime_map_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<NFormItem :label="t('setting.down_number')" path="DownNumber">
|
||||
<NInputNumber v-model:value="formValue.DownNumber" :min="1" :max="10"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.down_number_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="UserAgent" path="UserAgent">
|
||||
<NInput v-model:value="formValue.UserAgent" placeholder="UserAgent"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.user_agent_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="Headers" path="Headers">
|
||||
<NInput v-model:value="formValue.UseHeaders" placeholder="User-Agent,Referer,Authorization,Cookie"/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.use_headers_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="t('setting.mime_map')" path="MimeMap">
|
||||
<NInput
|
||||
v-model:value="MimeMap"
|
||||
type="textarea"
|
||||
rows="11"
|
||||
placeholder='{"video/mp4": { "Type": "video","Suffix": ".mp4"}}'
|
||||
/>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NIcon size="18" class="ml-1 text-gray-500">
|
||||
<HelpCircleOutline/>
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t("setting.mime_map_tip") }}
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -182,6 +227,9 @@ import type {appType} from "@/types/app"
|
||||
import appApi from "@/api/app"
|
||||
import {computed} from "vue"
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {isValidHost, isValidPort} from '@/func'
|
||||
import {NButton, NIcon} from "naive-ui"
|
||||
import * as bind from "../../wailsjs/go/core/Bind"
|
||||
|
||||
const {t} = useI18n()
|
||||
const store = useIndexStore()
|
||||
@@ -197,7 +245,26 @@ 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)
|
||||
|
||||
const hostValidationFeedback = ref("")
|
||||
const portValidationFeedback = ref("")
|
||||
|
||||
watch(formValue.value, () => {
|
||||
formValue.value.Port = formValue.value.Port.trim()
|
||||
formValue.value.Host = formValue.value.Host.trim()
|
||||
|
||||
if (!isValidHost(formValue.value.Host)) {
|
||||
hostValidationFeedback.value = t("setting.host_format_error")
|
||||
return
|
||||
} else {
|
||||
hostValidationFeedback.value = ''
|
||||
}
|
||||
|
||||
if (!isValidPort(parseInt(formValue.value.Port))) {
|
||||
portValidationFeedback.value = t("setting.port_format_error")
|
||||
return
|
||||
} else {
|
||||
portValidationFeedback.value = ''
|
||||
}
|
||||
store.setConfig(formValue.value)
|
||||
}, {deep: true})
|
||||
|
||||
@@ -225,6 +292,17 @@ const selectDir = () => {
|
||||
}
|
||||
}).catch((err: any) => {
|
||||
window?.$message?.error(err)
|
||||
});
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
const resetHandle = ()=>{
|
||||
localStorage.clear()
|
||||
bind.ResetApp()
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.n-tabs-nav--top{
|
||||
@apply sticky top-0 z-10;
|
||||
background-color: var(--n-color);
|
||||
}
|
||||
</style>
|
||||
2
frontend/wailsjs/go/core/Bind.d.ts
vendored
2
frontend/wailsjs/go/core/Bind.d.ts
vendored
@@ -5,3 +5,5 @@ import {core} from '../models';
|
||||
export function AppInfo():Promise<core.ResponseData>;
|
||||
|
||||
export function Config():Promise<core.ResponseData>;
|
||||
|
||||
export function ResetApp():Promise<void>;
|
||||
|
||||
@@ -9,3 +9,7 @@ export function AppInfo() {
|
||||
export function Config() {
|
||||
return window['go']['core']['Bind']['Config']();
|
||||
}
|
||||
|
||||
export function ResetApp() {
|
||||
return window['go']['core']['Bind']['ResetApp']();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"info": {
|
||||
"companyName": "res-downloader",
|
||||
"productName": "res-downloader",
|
||||
"productVersion": "3.0.6",
|
||||
"productVersion": "3.1.2",
|
||||
"copyright": "Copyright © 2023",
|
||||
"comments": "This is a high-value high-performance and diverse resource downloader called res-downloader."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user