diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 08136fa8e..f2c0d80d6 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -2,13 +2,15 @@ package controller import ( "github.com/GopeedLab/gopeed/pkg/base" + "net/http" + "net/url" "os" "path/filepath" ) type Controller struct { - GetConfig func(v any) - ProxyConfig *base.DownloaderProxyConfig + GetConfig func(v any) + GetProxy func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) FileController //ContextDialer() (proxy.Dialer, error) } @@ -23,6 +25,7 @@ type DefaultFileController struct { func NewController() *Controller { return &Controller{ GetConfig: func(v any) {}, + GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil }, FileController: &DefaultFileController{}, } } diff --git a/internal/protocol/bt/fetcher.go b/internal/protocol/bt/fetcher.go index 1b2796c72..a090445b6 100644 --- a/internal/protocol/bt/fetcher.go +++ b/internal/protocol/bt/fetcher.go @@ -73,7 +73,7 @@ func (f *Fetcher) initClient() (err error) { cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20()) cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version) cfg.ListenPort = f.config.ListenPort - cfg.HTTPProxy = f.ctl.ProxyConfig.ToHandler() + cfg.HTTPProxy = f.ctl.GetProxy(f.meta.Req.Proxy) cfg.DefaultStorage = newFileOpts(newFileClientOpts{ ClientBaseDir: cfg.DataDir, HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) { @@ -98,11 +98,14 @@ func (f *Fetcher) initClient() (err error) { } func (f *Fetcher) Resolve(req *base.Request) error { + if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil { + return err + } + f.meta.Req = req if err := f.addTorrent(req, false); err != nil { return err } f.updateRes() - f.meta.Req = req return nil } @@ -339,9 +342,6 @@ func (f *Fetcher) WaitUpload() (err error) { } func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) { - if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil { - return - } if err = f.initClient(); err != nil { return } diff --git a/internal/protocol/bt/fetcher_test.go b/internal/protocol/bt/fetcher_test.go index be20321af..012c4134a 100644 --- a/internal/protocol/bt/fetcher_test.go +++ b/internal/protocol/bt/fetcher_test.go @@ -8,6 +8,8 @@ import ( "github.com/GopeedLab/gopeed/internal/test" "github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/protocol/bt" + gohttp "net/http" + "net/url" "os" "reflect" "testing" @@ -210,7 +212,9 @@ func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher newController.GetConfig = func(v any) { json.Unmarshal([]byte(test.ToJson(mockCfg)), v) } - newController.ProxyConfig = proxyConfig + newController.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) { + return proxyConfig.ToHandler() + } fetcher.Setup(newController) return fetcher } diff --git a/internal/protocol/http/fetcher.go b/internal/protocol/http/fetcher.go index 3647c2c4b..399ea01c2 100644 --- a/internal/protocol/http/fetcher.go +++ b/internal/protocol/http/fetcher.go @@ -79,6 +79,7 @@ func (f *Fetcher) Resolve(req *base.Request) error { if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil { return err } + f.meta.Req = req httpReq, err := f.buildRequest(nil, req) if err != nil { return err @@ -153,7 +154,6 @@ func (f *Fetcher) Resolve(req *base.Request) error { file.Name = httpReq.URL.Hostname() } res.Files = append(res.Files, file) - f.meta.Req = req f.meta.Res = res return nil } @@ -444,7 +444,7 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) { func (f *Fetcher) buildClient() *http.Client { transport := &http.Transport{ - Proxy: f.ctl.ProxyConfig.ToHandler(), + Proxy: f.ctl.GetProxy(f.meta.Req.Proxy), } // Cookie handle jar, _ := cookiejar.New(nil) diff --git a/internal/protocol/http/fetcher_test.go b/internal/protocol/http/fetcher_test.go index d1507adfe..8109cbb3d 100644 --- a/internal/protocol/http/fetcher_test.go +++ b/internal/protocol/http/fetcher_test.go @@ -9,6 +9,8 @@ import ( "github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/protocol/http" "net" + gohttp "net/http" + "net/url" "testing" "time" ) @@ -385,10 +387,12 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) { func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) { fetcher := downloadReady(httpListener, 4, t) ctl := controller.NewController() - ctl.ProxyConfig = &base.DownloaderProxyConfig{ - Enable: true, - Scheme: "socks5", - Host: proxyListener.Addr().String(), + ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) { + return (&base.DownloaderProxyConfig{ + Enable: true, + Scheme: "socks5", + Host: proxyListener.Addr().String(), + }).ToHandler() } fetcher.Setup(ctl) err := fetcher.Start() diff --git a/pkg/base/model.go b/pkg/base/model.go index 72f68c83e..8f79f4a7c 100644 --- a/pkg/base/model.go +++ b/pkg/base/model.go @@ -16,6 +16,8 @@ type Request struct { Extra any `json:"extra"` // Labels is used to mark the download task Labels map[string]string `json:"labels"` + // Proxy is special proxy config for request + Proxy *RequestProxy `json:"proxy"` } func (r *Request) Validate() error { @@ -25,6 +27,37 @@ func (r *Request) Validate() error { return nil } +type RequestProxyMode string + +const ( + // RequestProxyModeFollow follow setting proxy + RequestProxyModeFollow RequestProxyMode = "follow" + // RequestProxyModeNone not use proxy + RequestProxyModeNone RequestProxyMode = "none" + // RequestProxyModeCustom custom proxy + RequestProxyModeCustom RequestProxyMode = "custom" +) + +type RequestProxy struct { + Mode RequestProxyMode `json:"mode"` + Scheme string `json:"scheme"` + Host string `json:"host"` + Usr string `json:"usr"` + Pwd string `json:"pwd"` +} + +func (p *RequestProxy) ToHandler() func(r *http.Request) (*url.URL, error) { + if p == nil || p.Mode != RequestProxyModeCustom { + return nil + } + + if p.Scheme == "" || p.Host == "" { + return nil + } + + return http.ProxyURL(util.BuildProxyUrl(p.Scheme, p.Host, p.Usr, p.Pwd)) +} + // Resource download resource type Resource struct { // if name is not empty, the resource is a folder and the name is the folder name @@ -94,15 +127,7 @@ func (o *Options) InitSelectFiles(fileSize int) { } func (o *Options) Clone() *Options { - if o == nil { - return nil - } - return &Options{ - Name: o.Name, - Path: o.Path, - SelectFiles: o.SelectFiles, - Extra: o.Extra, - } + return util.DeepClone(o) } func ParseReqExtra[E any](req *Request) error { diff --git a/pkg/download/downloader.go b/pkg/download/downloader.go index 8bb2c7f49..c8eef3f84 100644 --- a/pkg/download/downloader.go +++ b/pkg/download/downloader.go @@ -12,6 +12,8 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/pkgerrors" "github.com/virtuald/go-paniclog" + gohttp "net/http" + "net/url" "os" "path/filepath" "sort" @@ -253,7 +255,20 @@ func (d *Downloader) setupFetcher(fm fetcher.FetcherManager, fetcher fetcher.Fet ctl.GetConfig = func(v any) { d.getProtocolConfig(fm.Name(), v) } - ctl.ProxyConfig = d.cfg.Proxy + // Get proxy config, task request proxy config has higher priority, then use global proxy config + ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) { + if requestProxy == nil { + return d.cfg.Proxy.ToHandler() + } + switch requestProxy.Mode { + case base.RequestProxyModeNone: + return nil + case base.RequestProxyModeCustom: + return requestProxy.ToHandler() + default: + return d.cfg.Proxy.ToHandler() + } + } fetcher.Setup(ctl) } diff --git a/pkg/download/downloader_test.go b/pkg/download/downloader_test.go index 2e8976d8a..3556f6905 100644 --- a/pkg/download/downloader_test.go +++ b/pkg/download/downloader_test.go @@ -159,21 +159,21 @@ func TestDownloader_CreateDirectBatch(t *testing.T) { func TestDownloader_CreateWithProxy(t *testing.T) { // No proxy - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { return nil }, nil) // Disable proxy - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { proxyCfg.Enable = false return proxyCfg }, nil) // Enable system proxy but not set proxy environment variable - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { proxyCfg.System = true return proxyCfg }, nil) // Enable proxy but error proxy environment variable - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234") os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234") proxyCfg.System = true @@ -184,33 +184,50 @@ func TestDownloader_CreateWithProxy(t *testing.T) { } }) // Enable system proxy and set proxy environment variable - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String()) os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String()) proxyCfg.System = true return proxyCfg }, nil) // Invalid proxy scheme - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { proxyCfg.Scheme = "" return proxyCfg }, nil) // Invalid proxy host - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { proxyCfg.Host = "" return proxyCfg }, nil) // Use proxy without auth - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { return proxyCfg }, nil) // Use proxy with auth - doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, true, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { return proxyCfg }, nil) + + // Request proxy mode follow + doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy { + reqProxy.Mode = base.RequestProxyModeFollow + return reqProxy + }, nil, nil) + + // Request proxy mode none + doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy { + reqProxy.Mode = base.RequestProxyModeNone + return reqProxy + }, nil, nil) + + // Request proxy mode custom + doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy { + return reqProxy + }, nil, nil) } -func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) { +func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildReqProxy func(reqProxy *base.RequestProxy) *base.RequestProxy, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) { usr, pwd := "", "" if auth { usr, pwd = "admin", "123" @@ -223,17 +240,29 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig f t.Fatal(err) } defer downloader.Clear() - downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&base.DownloaderProxyConfig{ + globalProxyCfg := &base.DownloaderProxyConfig{ Enable: true, Scheme: "socks5", Host: proxyListener.Addr().String(), Usr: usr, Pwd: pwd, - }) + } + if buildProxyConfig != nil { + globalProxyCfg = buildProxyConfig(globalProxyCfg) + } + downloader.cfg.DownloaderStoreConfig.Proxy = globalProxyCfg req := &base.Request{ URL: test.ExternalDownloadUrl, } + if buildReqProxy != nil { + req.Proxy = buildReqProxy(&base.RequestProxy{ + Scheme: "socks5", + Host: proxyListener.Addr().String(), + Usr: usr, + Pwd: pwd, + }) + } rr, err := downloader.Resolve(req) if err != nil { if errHandler == nil { diff --git a/pkg/download/engine/engine.go b/pkg/download/engine/engine.go index fa1628971..93eaa60c3 100644 --- a/pkg/download/engine/engine.go +++ b/pkg/download/engine/engine.go @@ -106,7 +106,7 @@ func (e *Engine) await(value any) { } func (e *Engine) Close() { - e.loop.Stop() + e.loop.StopNoWait() } type Config struct { diff --git a/pkg/download/extension.go b/pkg/download/extension.go index 4c703f69e..642f37b45 100644 --- a/pkg/download/extension.go +++ b/pkg/download/extension.go @@ -694,8 +694,12 @@ type ExtensionTask struct { } func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask { + newTask := task.clone() + // Assign the pointer of the properties that the extension supports modification + newTask.Meta = task.Meta + newTask.Status = task.Status return &ExtensionTask{ - Task: task.clone(), + Task: newTask, download: download, } } diff --git a/pkg/download/model.go b/pkg/download/model.go index 9edf955cd..7c12790a2 100644 --- a/pkg/download/model.go +++ b/pkg/download/model.go @@ -97,16 +97,7 @@ func (t *Task) updateStatus(status base.Status) { } func (t *Task) clone() *Task { - return &Task{ - ID: t.ID, - Protocol: t.Protocol, - Meta: t.Meta, - Status: t.Status, - Uploading: t.Uploading, - Progress: t.Progress, - CreatedAt: t.CreatedAt, - UpdatedAt: t.UpdatedAt, - } + return util.DeepClone(t) } func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 { diff --git a/pkg/util/json.go b/pkg/util/json.go index 081feecc5..b4b7dc88f 100644 --- a/pkg/util/json.go +++ b/pkg/util/json.go @@ -12,3 +12,17 @@ func MapToStruct(s any, v any) error { } return json.Unmarshal(b, v) } + +func DeepClone[T any](v *T) *T { + if v == nil { + return nil + } + + var t T + b, err := json.Marshal(v) + if err != nil { + return &t + } + json.Unmarshal(b, &t) + return &t +} diff --git a/pkg/util/json_test.go b/pkg/util/json_test.go new file mode 100644 index 000000000..83b335453 --- /dev/null +++ b/pkg/util/json_test.go @@ -0,0 +1,60 @@ +package util + +import ( + "reflect" + "testing" +) + +func TestDeepClone(t *testing.T) { + type user struct { + Name string `json:"name"` + Age int `json:"age"` + + v int + } + + type args[T any] struct { + v *T + } + type testCase[T any] struct { + name string + args args[T] + want *T + } + tests := []testCase[user]{ + { + name: "case 1", + args: args[user]{ + v: &user{ + Name: "test", + Age: 10, + }, + }, + want: &user{ + Name: "test", + Age: 10, + }, + }, + { + name: "case 2", + args: args[user]{ + v: &user{ + Name: "test", + Age: 10, + v: 1, + }, + }, + want: &user{ + Name: "test", + Age: 10, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := DeepClone(tt.args.v); !reflect.DeepEqual(got, tt.want) { + t.Errorf("DeepClone() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/ui/flutter/lib/api/model/request.dart b/ui/flutter/lib/api/model/request.dart index 5e39b1f50..96dfdd2dc 100644 --- a/ui/flutter/lib/api/model/request.dart +++ b/ui/flutter/lib/api/model/request.dart @@ -7,11 +7,13 @@ class Request { String url; Object? extra; Map? labels = {}; + RequestProxy? proxy; Request({ required this.url, this.extra, this.labels, + this.proxy, }); factory Request.fromJson(Map json) => @@ -45,3 +47,24 @@ class ReqExtraBt { Map toJson() => _$ReqExtraBtToJson(this); } + +enum RequestProxyMode { + follow, + none, + custom, +} + +@JsonSerializable() +class RequestProxy { + RequestProxyMode mode = RequestProxyMode.follow; + String scheme = 'http'; + String host = ''; + String usr = ''; + String pwd = ''; + + RequestProxy(); + + factory RequestProxy.fromJson(Map json) => + _$RequestProxyFromJson(json); + Map toJson() => _$RequestProxyToJson(this); +} diff --git a/ui/flutter/lib/api/model/request.g.dart b/ui/flutter/lib/api/model/request.g.dart index 3ddd44ad2..20a383f00 100644 --- a/ui/flutter/lib/api/model/request.g.dart +++ b/ui/flutter/lib/api/model/request.g.dart @@ -12,6 +12,9 @@ Request _$RequestFromJson(Map json) => Request( labels: (json['labels'] as Map?)?.map( (k, e) => MapEntry(k, e as String), ), + proxy: json['proxy'] == null + ? null + : RequestProxy.fromJson(json['proxy'] as Map), ); Map _$RequestToJson(Request instance) { @@ -27,6 +30,7 @@ Map _$RequestToJson(Request instance) { writeNotNull('extra', instance.extra); writeNotNull('labels', instance.labels); + writeNotNull('proxy', instance.proxy?.toJson()); return val; } @@ -50,3 +54,25 @@ Map _$ReqExtraBtToJson(ReqExtraBt instance) => { 'trackers': instance.trackers, }; + +RequestProxy _$RequestProxyFromJson(Map json) => RequestProxy() + ..mode = $enumDecode(_$RequestProxyModeEnumMap, json['mode']) + ..scheme = json['scheme'] as String + ..host = json['host'] as String + ..usr = json['usr'] as String + ..pwd = json['pwd'] as String; + +Map _$RequestProxyToJson(RequestProxy instance) => + { + 'mode': _$RequestProxyModeEnumMap[instance.mode]!, + 'scheme': instance.scheme, + 'host': instance.host, + 'usr': instance.usr, + 'pwd': instance.pwd, + }; + +const _$RequestProxyModeEnumMap = { + RequestProxyMode.follow: 'follow', + RequestProxyMode.none: 'none', + RequestProxyMode.custom: 'custom', +}; diff --git a/ui/flutter/lib/api/model/resource.g.dart b/ui/flutter/lib/api/model/resource.g.dart index 2feddd86d..0a5e8fc8a 100644 --- a/ui/flutter/lib/api/model/resource.g.dart +++ b/ui/flutter/lib/api/model/resource.g.dart @@ -8,7 +8,7 @@ part of 'resource.dart'; Resource _$ResourceFromJson(Map json) => Resource( name: json['name'] as String? ?? "", - size: json['size'] as int? ?? 0, + size: (json['size'] as num?)?.toInt() ?? 0, range: json['range'] as bool? ?? false, files: (json['files'] as List) .map((e) => FileInfo.fromJson(e as Map)) @@ -27,7 +27,7 @@ Map _$ResourceToJson(Resource instance) => { FileInfo _$FileInfoFromJson(Map json) => FileInfo( path: json['path'] as String? ?? "", name: json['name'] as String, - size: json['size'] as int? ?? 0, + size: (json['size'] as num?)?.toInt() ?? 0, req: json['req'] == null ? null : Request.fromJson(json['req'] as Map), diff --git a/ui/flutter/lib/app/modules/create/controllers/create_controller.dart b/ui/flutter/lib/app/modules/create/controllers/create_controller.dart index f3594de43..6884c0ebe 100644 --- a/ui/flutter/lib/app/modules/create/controllers/create_controller.dart +++ b/ui/flutter/lib/app/modules/create/controllers/create_controller.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:gopeed/api/model/request.dart'; class CreateController extends GetxController with GetSingleTickerProviderStateMixin { @@ -13,6 +14,7 @@ class CreateController extends GetxController final isConfirming = false.obs; final showAdvanced = false.obs; final directDownload = false.obs; + final proxyConfig = Rx(null); late TabController advancedTabController; final oldUrl = "".obs; final fileDataUri = "".obs; diff --git a/ui/flutter/lib/app/modules/create/views/create_view.dart b/ui/flutter/lib/app/modules/create/views/create_view.dart index 394350d66..2d5330835 100644 --- a/ui/flutter/lib/app/modules/create/views/create_view.dart +++ b/ui/flutter/lib/app/modules/create/views/create_view.dart @@ -4,6 +4,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:gopeed/api/model/downloader_config.dart'; import 'package:path/path.dart' as path; import 'package:rounded_loading_button_plus/rounded_loading_button.dart'; @@ -32,6 +33,10 @@ class CreateView extends GetView { final _connectionsController = TextEditingController(); final _pathController = TextEditingController(); final _confirmController = RoundedLoadingButtonController(); + final _proxyIpController = TextEditingController(); + final _proxyPortController = TextEditingController(); + final _proxyUsrController = TextEditingController(); + final _proxyPwdController = TextEditingController(); final _httpUaController = TextEditingController(); final _httpCookieController = TextEditingController(); final _httpRefererController = TextEditingController(); @@ -261,7 +266,164 @@ class CreateView extends GetView { () => Visibility( visible: controller.showAdvanced.value, child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Transform.translate( + offset: const Offset(-40, 0), + child: Row( + children: [ + const Icon( + Icons.wifi_2_bar, + color: Colors.grey, + ), + const SizedBox( + width: 15, + ), + SizedBox( + width: 150, + child: DropdownButton< + RequestProxyMode>( + hint: Text('proxy'.tr), + isExpanded: true, + value: controller + .proxyConfig.value?.mode, + onChanged: (value) async { + if (value != null) { + controller.proxyConfig + .value = RequestProxy() + ..mode = value; + } + }, + items: [ + DropdownMenuItem< + RequestProxyMode>( + value: + RequestProxyMode.follow, + child: Text( + 'followSettings'.tr), + ), + DropdownMenuItem< + RequestProxyMode>( + value: + RequestProxyMode.none, + child: Text('noProxy'.tr), + ), + DropdownMenuItem< + RequestProxyMode>( + value: + RequestProxyMode.custom, + child: + Text('customProxy'.tr), + ), + ], + )) + ], + ), + ), + ...(controller.proxyConfig.value?.mode == + RequestProxyMode.custom + ? [ + SizedBox( + width: 150, + child: DropdownButtonFormField< + String>( + value: controller + .proxyConfig.value?.scheme, + onChanged: (value) async { + if (value != null) {} + }, + items: const [ + DropdownMenuItem( + value: 'http', + child: Text('HTTP'), + ), + DropdownMenuItem( + value: 'https', + child: Text('HTTPS'), + ), + DropdownMenuItem( + value: 'socks5', + child: Text('SOCKS5'), + ), + ], + ), + ), + Row(children: [ + Flexible( + child: TextFormField( + controller: + _proxyIpController, + decoration: InputDecoration( + labelText: 'server'.tr, + contentPadding: + const EdgeInsets.all( + 0.0), + ), + ), + ), + const Padding( + padding: EdgeInsets.only( + left: 10)), + Flexible( + child: TextFormField( + controller: + _proxyPortController, + decoration: InputDecoration( + labelText: 'port'.tr, + contentPadding: + const EdgeInsets.all( + 0.0), + ), + keyboardType: + TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter + .digitsOnly, + NumericalRangeFormatter( + min: 0, max: 65535), + ], + ), + ), + ]), + Row(children: [ + Flexible( + child: TextFormField( + controller: + _proxyUsrController, + decoration: InputDecoration( + labelText: 'username'.tr, + contentPadding: + const EdgeInsets.all( + 0.0), + ), + ), + ), + const Padding( + padding: EdgeInsets.only( + left: 10)), + Flexible( + child: TextFormField( + controller: + _proxyPwdController, + decoration: InputDecoration( + labelText: 'password'.tr, + contentPadding: + const EdgeInsets.all( + 0.0), + ), + ), + ), + ]) + ] + : const []), + ], + ), + const Divider(), TabBar( controller: controller.advancedTabController, tabs: const [ @@ -416,18 +578,21 @@ class CreateView extends GetView { if (isDirect) { await Future.wait(urls.map((url) { return createTask(CreateTask( - req: Request(url: url, extra: parseReqExtra(url)), + req: Request( + url: url, extra: parseReqExtra(url), proxy: parseProxy()), opt: Options( - name: isMultiLine ? "" : _renameController.text, - path: _pathController.text, - selectFiles: [], - extra: parseReqOptsExtra()))); + name: isMultiLine ? "" : _renameController.text, + path: _pathController.text, + selectFiles: [], + extra: parseReqOptsExtra(), + ))); })); Get.rootDelegate.offNamed(Routes.TASK); } else { final rr = await resolve(Request( url: submitUrl, extra: parseReqExtra(_urlController.text), + proxy: parseProxy(), )); await _showResolveDialog(rr); } @@ -440,6 +605,18 @@ class CreateView extends GetView { } } + RequestProxy? parseProxy() { + if (controller.proxyConfig.value?.mode == RequestProxyMode.custom) { + return RequestProxy() + ..mode = RequestProxyMode.custom + ..scheme = _proxyIpController.text + ..host = "${_proxyIpController.text}:${_proxyPortController.text}" + ..usr = _proxyUsrController.text + ..pwd = _proxyPwdController.text; + } + return controller.proxyConfig.value; + } + Object? parseReqExtra(String url) { Object? reqExtra; if (controller.showAdvanced.value) { @@ -543,7 +720,7 @@ class CreateView extends GetView { controller.selectedIndexes.map((index) { final file = rr.res.files[index]; return createTask(CreateTask( - req: file.req!, + req: file.req!..proxy = parseProxy(), opt: Options( name: file.name, path: path.join(_pathController.text, diff --git a/ui/flutter/lib/app/modules/setting/views/setting_view.dart b/ui/flutter/lib/app/modules/setting/views/setting_view.dart index 22c7c4760..02b6a29dd 100644 --- a/ui/flutter/lib/app/modules/setting/views/setting_view.dart +++ b/ui/flutter/lib/app/modules/setting/views/setting_view.dart @@ -847,7 +847,7 @@ class SettingView extends GetView { // advanced config log items start buildLogsDir() { return ListTile( - title: Text("日志目录"), + title: Text("logDirectory".tr), subtitle: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -981,7 +981,7 @@ class SettingView extends GetView { : null, ]), )), - const Text('开发者'), + Text('developer'.tr), Card( child: Column( children: _addDivider([ diff --git a/ui/flutter/lib/i18n/langs/en_us.dart b/ui/flutter/lib/i18n/langs/en_us.dart index f5bdcfea1..32560eb05 100644 --- a/ui/flutter/lib/i18n/langs/en_us.dart +++ b/ui/flutter/lib/i18n/langs/en_us.dart @@ -17,6 +17,7 @@ const enUS = { 'create': 'Create Task', 'directDownload': 'Direct Download', 'advancedOptions': 'Advanced Options', + 'followSettings': 'Follow Settings', 'downloadLink': 'Download Link', 'downloadLinkValid': 'Please enter the download link', 'downloadLinkHit': @@ -60,6 +61,8 @@ const enUS = { 'set': 'SET', 'portInUse': 'Port [@port] is in use, please change the port', 'effectAfterRestart': 'Effect after restart', + 'developer': 'Developer', + 'logDirectory': 'Log Directory', 'show': 'Show', 'startAll': 'Start All', 'pauseAll': 'Pause All', @@ -96,7 +99,7 @@ const enUS = { 'browserExtension': 'Browser Extension', 'launchAtStartup': 'Launch at Startup', 'seedConfig': 'Seed Config', - 'seedKeep': 'Keep seeding', + 'seedKeep': 'Keep seeding until manually stopped', 'seedRatio': 'Seed ratio', 'seedTime': 'Seed time (minutes)', 'taskDetail': 'Task Detail', diff --git a/ui/flutter/lib/i18n/langs/zh_cn.dart b/ui/flutter/lib/i18n/langs/zh_cn.dart index 549637ae9..ebdb53267 100644 --- a/ui/flutter/lib/i18n/langs/zh_cn.dart +++ b/ui/flutter/lib/i18n/langs/zh_cn.dart @@ -17,6 +17,7 @@ const zhCN = { 'create': '创建任务', 'directDownload': '直接下载', 'advancedOptions': '高级选项', + 'followSettings': '跟随设置', 'downloadLink': '下载链接', 'downloadLinkValid': '请输入下载链接', 'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append', @@ -58,6 +59,8 @@ const zhCN = { 'set': '已设置', 'portInUse': '端口[@port]已被占用,请更换端口', 'effectAfterRestart': '此配置项将在重启应用后生效', + 'developer': '开发者', + 'logDirectory': '日志目录', 'show': '显示', 'startAll': '全部开始', 'pauseAll': '全部暂停', @@ -93,7 +96,7 @@ const zhCN = { 'browserExtension': '浏览器扩展', 'launchAtStartup': '开机自动运行', 'seedConfig': '做种设置', - 'seedKeep': '持续做种', + 'seedKeep': '持续做种直到手动停止', 'seedRatio': '做种分享率', 'seedTime': '做种时间(分钟)', 'taskDetail': '任务详情', diff --git a/ui/flutter/lib/i18n/langs/zh_tw.dart b/ui/flutter/lib/i18n/langs/zh_tw.dart index 239cd3d14..f56ea5d94 100644 --- a/ui/flutter/lib/i18n/langs/zh_tw.dart +++ b/ui/flutter/lib/i18n/langs/zh_tw.dart @@ -17,6 +17,7 @@ const zhTW = { 'create': '建立任務', 'directDownload': '直接下載', 'advancedOptions': '進階選項', + 'followSettings': '跟隨設定', 'downloadLink': '下載連結', 'downloadLinkValid': '請輸入下載連結', 'downloadLinkHit': '請輸入下載連結,支援 HTTP/HTTPS/MAGNET@append', @@ -58,6 +59,8 @@ const zhTW = { 'set': '已設定', 'portInUse': '連接埠 [@port] 已被使用,請更改連接埠', 'effectAfterRestart': '重新啟動後生效', + 'developer': '開發者', + 'logDirectory': '日誌目錄', 'show': '顯示', 'startAll': '全部開始', 'pauseAll': '全部暫停', @@ -93,15 +96,9 @@ const zhTW = { 'browserExtension': '瀏覽器擴充功能', 'launchAtStartup': '開機自動執行', 'seedConfig': '做種設定', - 'seedKeep': '持續做種', + 'seedKeep': '持續做種直到手動停止', 'seedRatio': '做種分享率', 'seedTime': '做種時間(分鐘)', - /* - 'taskDetail': '任务详情', - 'taskName': '任务名称', - 'taskUrl': '任务链接', - 'downloadPath': '下载路径', - */ 'taskDetail': '任務詳情', 'taskName': '任務名稱', 'taskUrl': '任務連結',