mirror of
https://github.com/putyy/res-downloader.git
synced 2026-01-12 14:14:55 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bca2e110de | ||
|
|
8d55a86c06 | ||
|
|
f61199bed6 | ||
|
|
2d75bbb5c3 | ||
|
|
55d3f06cb6 | ||
|
|
1809847b8a | ||
|
|
da8e8d9641 | ||
|
|
ead622d95e | ||
|
|
c47fcba36b | ||
|
|
54c0da081c |
@@ -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
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"res-downloader/core/shared"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -47,9 +49,12 @@ type FileDownloader struct {
|
||||
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,
|
||||
@@ -59,6 +64,8 @@ func NewFileDownloader(url, filename string, totalTasks int, headers map[string]
|
||||
TotalSize: 0,
|
||||
Headers: headers,
|
||||
DownloadTaskList: make([]*DownloadTask, 0),
|
||||
ctx: ctx,
|
||||
cancelFunc: cancelFunc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +79,6 @@ func (fd *FileDownloader) buildClient() *http.Client {
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +149,9 @@ 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)
|
||||
@@ -268,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):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,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)
|
||||
}
|
||||
@@ -307,6 +332,12 @@ 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 {
|
||||
writeSize := int64(n)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
18
core/http.go
18
core/http.go
@@ -345,6 +345,24 @@ 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 {
|
||||
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
|
||||
|
||||
@@ -56,6 +56,8 @@ 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-export":
|
||||
|
||||
@@ -53,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
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
@@ -22,6 +23,7 @@ type WxFileDecodeResult struct {
|
||||
|
||||
type Resource struct {
|
||||
mediaMark sync.Map
|
||||
tasks sync.Map
|
||||
resType map[string]bool
|
||||
resTypeMux sync.RWMutex
|
||||
}
|
||||
@@ -86,6 +88,15 @@ func (r *Resource) delete(sign string) {
|
||||
r.mediaMark.Delete(sign)
|
||||
}
|
||||
|
||||
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 MediaInfo, decodeStr string) {
|
||||
if globalConfig.SaveDirectory == "" {
|
||||
return
|
||||
@@ -93,6 +104,11 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
|
||||
go func(mediaInfo 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
|
||||
@@ -107,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") {
|
||||
@@ -140,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 != "" {
|
||||
|
||||
@@ -9,7 +9,10 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
sysRuntime "runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -61,6 +64,14 @@ func IsDevelopment() bool {
|
||||
return os.Getenv("APP_ENV") == "development"
|
||||
}
|
||||
|
||||
func GetFileNameFromURL(rawUrl string) string {
|
||||
parsedURL, err := url.Parse(rawUrl)
|
||||
if err == nil {
|
||||
return path.Base(parsedURL.Path)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func GetCurrentDateTimeFormatted() string {
|
||||
now := time.Now()
|
||||
return fmt.Sprintf("%04d%02d%02d%02d%02d%02d",
|
||||
@@ -72,6 +83,24 @@ func GetCurrentDateTimeFormatted() string {
|
||||
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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -23,6 +23,16 @@
|
||||
</NIcon>
|
||||
</template>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center justify-start p-1.5 cursor-pointer" @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"
|
||||
@@ -76,6 +86,7 @@ import {
|
||||
LockOpenSharp,
|
||||
LinkOutline,
|
||||
GridSharp,
|
||||
CloseOutline,
|
||||
TrashOutline
|
||||
} from "@vicons/ionicons5"
|
||||
|
||||
|
||||
@@ -18,6 +18,14 @@
|
||||
<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"
|
||||
@@ -91,6 +99,7 @@ import {
|
||||
LinkOutline,
|
||||
LockOpenSharp,
|
||||
GridSharp,
|
||||
CloseOutline,
|
||||
TrashOutline
|
||||
} from "@vicons/ionicons5"
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"batch_download": "Batch Download",
|
||||
"batch_export": "Batch Export",
|
||||
"batch_import": "Batch Import",
|
||||
"export_url": "Export Url",
|
||||
"import_success": "Export Success",
|
||||
"all": "All",
|
||||
"image": "Image",
|
||||
@@ -71,6 +72,7 @@
|
||||
"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",
|
||||
@@ -82,7 +84,11 @@
|
||||
"win_install_tip": "For the first time using this software, please right-click and select 'Run as administrator'",
|
||||
"download_queued": "Download has been added to the queue, current queue length:{count}",
|
||||
"search": "Search",
|
||||
"search_description": "Keyword 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"
|
||||
},
|
||||
"setting": {
|
||||
"restart_tip": "Keep default if unsure, please restart software after modification",
|
||||
@@ -96,7 +102,7 @@
|
||||
"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": "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",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"batch_download": "批量下载",
|
||||
"batch_export": "批量导出",
|
||||
"batch_import": "批量导入",
|
||||
"export_url": "导出链接",
|
||||
"import_success": "导出成功",
|
||||
"all": "全部",
|
||||
"image": "图片",
|
||||
@@ -71,6 +72,7 @@
|
||||
"open_link": "打开链接",
|
||||
"open_file": "打开文件",
|
||||
"delete_row": "删除记录",
|
||||
"cancel_down": "取消下载",
|
||||
"more_operation": "更多操作",
|
||||
"video_decode": "视频解密",
|
||||
"video_decode_loading": "解密中",
|
||||
@@ -82,7 +84,11 @@
|
||||
"win_install_tip": "首次启用本软件,请使用鼠标右键选择以管理员身份运行",
|
||||
"download_queued": "下载已加入队列,当前队列长度:{count}",
|
||||
"search": "搜索",
|
||||
"search_description": "关键字搜索..."
|
||||
"search_description": "关键字搜索...",
|
||||
"start_err_tip": "错误提示",
|
||||
"start_err_content": "当前启动过程遇到了问题,是否重置应用?",
|
||||
"start_err_positiveText": "清理缓存并重启",
|
||||
"start_err_negativeText": "关闭软件"
|
||||
},
|
||||
"setting": {
|
||||
"restart_tip": "如果不清楚保持默认就行,修改后请重启软件",
|
||||
|
||||
@@ -10,22 +10,23 @@
|
||||
</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>
|
||||
<n-popconfirm
|
||||
@positive-click="clear"
|
||||
>
|
||||
<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>
|
||||
{{ t("index.clear_list_tip") }}
|
||||
</n-popconfirm>
|
||||
<NButtonGroup style="--wails-draggable:no-drag">
|
||||
<n-popconfirm
|
||||
@positive-click="clear"
|
||||
>
|
||||
<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>
|
||||
{{ t("index.clear_list_tip") }}
|
||||
</n-popconfirm>
|
||||
|
||||
<NButton tertiary type="primary" @click.stop="batchDown">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
@@ -34,21 +35,48 @@
|
||||
</template>
|
||||
{{ t('index.batch_download') }}
|
||||
</NButton>
|
||||
<NButton tertiary type="warning" @click.stop="batchExport">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowRedoCircleOutline/>
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('index.batch_export') }}
|
||||
</NButton>
|
||||
<NButton tertiary type="info" @click.stop="showImport=true">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ServerOutline/>
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('index.batch_import') }}
|
||||
<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>
|
||||
@@ -65,6 +93,7 @@
|
||||
:height-for-row="()=> 48"
|
||||
:checked-row-keys="checkedRowKeysValue"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
@update:filters="updateFilters"
|
||||
style="--wails-draggable:no-drag"
|
||||
/>
|
||||
</div>
|
||||
@@ -85,7 +114,7 @@
|
||||
import {NButton, NIcon, NImage, NInput, NSpace, NTooltip, NPopover} 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
|
||||
@@ -104,11 +133,17 @@ import {
|
||||
ArrowRedoCircleOutline,
|
||||
ServerOutline,
|
||||
SearchOutline,
|
||||
TrashOutline
|
||||
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"
|
||||
|
||||
const {t} = useI18n()
|
||||
const eventStore = useEventStore()
|
||||
const dialog = useDialog()
|
||||
const isProxy = computed(() => {
|
||||
return store.isProxy
|
||||
})
|
||||
@@ -116,12 +151,12 @@ const certUrl = computed(() => {
|
||||
return store.baseUrl + "/api/cert"
|
||||
})
|
||||
const data = ref<any[]>([])
|
||||
let filterClassify: string[] = []
|
||||
const filterClassify = ref<string[]>([])
|
||||
const filteredData = computed(() => {
|
||||
let result = data.value
|
||||
|
||||
if (filterClassify.length > 0) {
|
||||
result = result.filter(item => filterClassify.includes(item.Classify))
|
||||
if (filterClassify.value.length > 0) {
|
||||
result = result.filter(item => filterClassify.value.includes(item.Classify))
|
||||
}
|
||||
|
||||
if (descriptionSearchValue.value) {
|
||||
@@ -186,7 +221,6 @@ const columns = ref<any[]>([
|
||||
filterOptions: computed(() => Array.from(classify.value).slice(1)),
|
||||
filterMultiple: true,
|
||||
filter: (value: string, row: appType.MediaInfo) => {
|
||||
if (!filterClassify.includes(value)) filterClassify.push(value)
|
||||
return !!~row.Classify.indexOf(String(value))
|
||||
},
|
||||
render: (row: appType.MediaInfo) => {
|
||||
@@ -273,6 +307,7 @@ const columns = ref<any[]>([
|
||||
title: () => h('div', {class: 'flex items-center'}, [
|
||||
t('index.description'),
|
||||
h(NPopover, {
|
||||
style:"--wails-draggable:no-drag",
|
||||
trigger: 'click',
|
||||
placement: 'bottom',
|
||||
showArrow: true,
|
||||
@@ -352,6 +387,7 @@ const showPassword = ref(false)
|
||||
const downloadQueue = ref<appType.MediaInfo[]>([])
|
||||
let activeDownloads = 0
|
||||
let isOpenProxy = false
|
||||
let isInstall = false
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
@@ -360,8 +396,16 @@ onMounted(() => {
|
||||
})
|
||||
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})
|
||||
}
|
||||
@@ -479,6 +523,22 @@ const dataAction = (row: appType.MediaInfo, index: number, type: string) => {
|
||||
case "down":
|
||||
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 = ''
|
||||
})
|
||||
cacheData()
|
||||
checkQueue()
|
||||
if (res.code === 0) {
|
||||
window?.$message?.error(res.message)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
break
|
||||
case "copy":
|
||||
ClipboardSetText(row.Url).then((is: boolean) => {
|
||||
if (is) {
|
||||
@@ -531,6 +591,10 @@ const handleCheck = (rowKeys: DataTableRowKey[]) => {
|
||||
checkedRowKeysValue.value = rowKeys
|
||||
}
|
||||
|
||||
const updateFilters = (filters: DataTableFilterState, initiatorColumn: DataTableBaseColumn)=>{
|
||||
filterClassify.value = filters.Classify as string[]
|
||||
}
|
||||
|
||||
const batchDown = async () => {
|
||||
if (checkedRowKeysValue.value.length <= 0) {
|
||||
window?.$message?.error(t("index.use_data"))
|
||||
@@ -551,7 +615,25 @@ const batchDown = async () => {
|
||||
checkedRowKeysValue.value = []
|
||||
}
|
||||
|
||||
const batchExport = () => {
|
||||
const batchCancel = () =>{
|
||||
if (checkedRowKeysValue.value.length <= 0) {
|
||||
window?.$message?.error(t("index.use_data"))
|
||||
return
|
||||
}
|
||||
|
||||
data.value.forEach(async (item, index) => {
|
||||
if (checkedRowKeysValue.value.includes(item.Id) && item.Status === "running") {
|
||||
appApi.cancel({id: item.Id})
|
||||
data.value[index].Status = 'ready'
|
||||
data.value[index].SavePath = ''
|
||||
}
|
||||
})
|
||||
checkedRowKeysValue.value = []
|
||||
cacheData()
|
||||
checkQueue()
|
||||
}
|
||||
|
||||
const batchExport = (type?: string) => {
|
||||
if (checkedRowKeysValue.value.length <= 0) {
|
||||
window?.$message?.error(t("index.use_data"))
|
||||
return
|
||||
@@ -565,9 +647,13 @@ const batchExport = () => {
|
||||
loadingText.value = t("common.loading")
|
||||
loading.value = true
|
||||
|
||||
const jsonData = data.value
|
||||
.filter(item => checkedRowKeysValue.value.includes(item.Id))
|
||||
.map(item => encodeURIComponent(JSON.stringify(item)))
|
||||
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.batchExport({content: jsonData.join("\n")}).then((res: appType.Res) => {
|
||||
loading.value = false
|
||||
@@ -732,8 +818,8 @@ const handleImport = (content: string) => {
|
||||
})
|
||||
if (newItems.length > 0) {
|
||||
data.value = [...newItems, ...data.value]
|
||||
cacheData()
|
||||
}
|
||||
cacheData()
|
||||
showImport.value = false
|
||||
}
|
||||
|
||||
@@ -774,6 +860,29 @@ const handleInstall = async () => {
|
||||
}
|
||||
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 {
|
||||
|
||||
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.1.0",
|
||||
"productVersion": "3.1.1",
|
||||
"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