mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 00:25:43 +00:00
feat: create task with advanced options (#157)
This commit is contained in:
22 files changed
+357
-136
No files matched your search
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
@@ -68,7 +69,11 @@ func (f *Fetcher) initClient() (err error) {
|
||||
}
|
||||
|
||||
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
if err := f.addTorrent(req.URL); err != nil {
|
||||
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.addTorrent(req); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
@@ -122,7 +127,7 @@ func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||
|
||||
func (f *Fetcher) Start() (err error) {
|
||||
if !f.torrentReady.Load() {
|
||||
if err = f.addTorrent(f.meta.Req.URL); err != nil {
|
||||
if err = f.addTorrent(f.meta.Req); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -211,15 +216,15 @@ func (f *Fetcher) Progress() fetcher.Progress {
|
||||
return f.progress
|
||||
}
|
||||
|
||||
func (f *Fetcher) addTorrent(url string) (err error) {
|
||||
func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
||||
if err = f.initClient(); err != nil {
|
||||
return
|
||||
}
|
||||
schema := util.ParseSchema(url)
|
||||
schema := util.ParseSchema(req.URL)
|
||||
if schema == "MAGNET" {
|
||||
f.torrent, err = client.AddMagnet(url)
|
||||
f.torrent, err = client.AddMagnet(req.URL)
|
||||
} else {
|
||||
f.torrent, err = client.AddTorrentFromFile(url)
|
||||
f.torrent, err = client.AddTorrentFromFile(req.URL)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
@@ -229,9 +234,25 @@ func (f *Fetcher) addTorrent(url string) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// use map to deduplicate
|
||||
trackers := make(map[string]bool)
|
||||
if req.Extra != nil {
|
||||
extra := req.Extra.(*bt.ReqExtra)
|
||||
if len(extra.Trackers) > 0 {
|
||||
for _, tracker := range extra.Trackers {
|
||||
trackers[tracker] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if exist && len(cfg.Trackers) > 0 {
|
||||
announceList := make([][]string, 0)
|
||||
for _, tracker := range cfg.Trackers {
|
||||
trackers[tracker] = true
|
||||
}
|
||||
}
|
||||
if len(trackers) > 0 {
|
||||
announceList := make([][]string, 0)
|
||||
for tracker := range trackers {
|
||||
announceList = append(announceList, []string{tracker})
|
||||
}
|
||||
f.torrent.AddTrackers(announceList)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
@@ -21,6 +22,12 @@ func TestFetcher_Config(t *testing.T) {
|
||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||
Extra: bt.ReqExtra{
|
||||
Trackers: []string{
|
||||
"udp://tracker.birkenwald.de:6969/announce",
|
||||
"udp://tracker.bitsearch.to:1337/announce",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package bt
|
||||
|
||||
type ReqExtra struct {
|
||||
Trackers []string `json:"trackers"`
|
||||
}
|
||||
@@ -26,6 +26,6 @@ subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
"donate": "Donate",
|
||||
"exit": "Exit",
|
||||
"create": "Create Task",
|
||||
"advancedOptions": "Advanced Options",
|
||||
"downloadLink": "Download Link",
|
||||
"downloadLinkValid": "Please enter the download link",
|
||||
"downloadLinkHit": "Please enter the download link, HTTP/HTTPS/MAGNET supported@append",
|
||||
@@ -20,6 +21,7 @@
|
||||
"noFileSelected": "Please select at least one file to continue.",
|
||||
"noStoragePermission": "Storage permission required",
|
||||
"selectFile": "Select File",
|
||||
"rename": "Rename",
|
||||
"basic": "Basic",
|
||||
"advanced": "Advanced",
|
||||
"general": "General",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"donate": "打赏",
|
||||
"exit": "退出",
|
||||
"create": "创建任务",
|
||||
"advancedOptions": "高级选项",
|
||||
"downloadLink": "下载链接",
|
||||
"downloadLinkValid": "请输入下载链接",
|
||||
"downloadLinkHit": "请输入下载链接,支持 HTTP/HTTPS/MAGNET@append",
|
||||
@@ -20,6 +21,7 @@
|
||||
"noFileSelected": "请至少选择一个文件下载",
|
||||
"noStoragePermission": "需要开启存储权限",
|
||||
"selectFile": "选择文件",
|
||||
"rename": "重命名",
|
||||
"basic": "基础",
|
||||
"advanced": "高级",
|
||||
"general": "通用",
|
||||
|
||||
@@ -2,17 +2,18 @@ import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'options.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true, genericArgumentFactories: true)
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class Options {
|
||||
String name;
|
||||
String path;
|
||||
List<int> selectFiles;
|
||||
Map<String, dynamic>? extra;
|
||||
Object? extra;
|
||||
|
||||
Options({
|
||||
required this.name,
|
||||
required this.path,
|
||||
required this.selectFiles,
|
||||
this.extra,
|
||||
});
|
||||
|
||||
factory Options.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -11,7 +11,8 @@ Options _$OptionsFromJson(Map<String, dynamic> json) => Options(
|
||||
path: json['path'] as String,
|
||||
selectFiles:
|
||||
(json['selectFiles'] as List<dynamic>).map((e) => e as int).toList(),
|
||||
)..extra = json['extra'] as Map<String, dynamic>?;
|
||||
extra: json['extra'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$OptionsToJson(Options instance) {
|
||||
final val = <String, dynamic>{
|
||||
|
||||
@@ -5,10 +5,11 @@ part 'request.g.dart';
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class Request {
|
||||
String url;
|
||||
Map<String, dynamic>? extra;
|
||||
Object? extra;
|
||||
|
||||
Request({
|
||||
required this.url,
|
||||
this.extra,
|
||||
});
|
||||
|
||||
factory Request.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -20,7 +21,7 @@ class Request {
|
||||
@JsonSerializable()
|
||||
class ReqExtraHttp {
|
||||
String method = 'GET';
|
||||
Map<String, String> headers = {};
|
||||
Map<String, String> header = {};
|
||||
String body = '';
|
||||
|
||||
ReqExtraHttp();
|
||||
@@ -30,3 +31,15 @@ class ReqExtraHttp {
|
||||
|
||||
Map<String, dynamic> toJson() => _$ReqExtraHttpToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ReqExtraBt {
|
||||
List<String> trackers = [];
|
||||
|
||||
ReqExtraBt();
|
||||
|
||||
factory ReqExtraBt.fromJson(Map<String, dynamic> json) =>
|
||||
_$ReqExtraBtFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ReqExtraBtToJson(this);
|
||||
}
|
||||
@@ -8,7 +8,8 @@ part of 'request.dart';
|
||||
|
||||
Request _$RequestFromJson(Map<String, dynamic> json) => Request(
|
||||
url: json['url'] as String,
|
||||
)..extra = json['extra'] as Map<String, dynamic>?;
|
||||
extra: json['extra'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RequestToJson(Request instance) {
|
||||
final val = <String, dynamic>{
|
||||
@@ -27,12 +28,21 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
|
||||
|
||||
ReqExtraHttp _$ReqExtraHttpFromJson(Map<String, dynamic> json) => ReqExtraHttp()
|
||||
..method = json['method'] as String
|
||||
..headers = Map<String, String>.from(json['headers'] as Map)
|
||||
..header = Map<String, String>.from(json['header'] as Map)
|
||||
..body = json['body'] as String;
|
||||
|
||||
Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) =>
|
||||
<String, dynamic>{
|
||||
'method': instance.method,
|
||||
'headers': instance.headers,
|
||||
'header': instance.header,
|
||||
'body': instance.body,
|
||||
};
|
||||
|
||||
ReqExtraBt _$ReqExtraBtFromJson(Map<String, dynamic> json) => ReqExtraBt()
|
||||
..trackers =
|
||||
(json['trackers'] as List<dynamic>).map((e) => e as String).toList();
|
||||
|
||||
Map<String, dynamic> _$ReqExtraBtToJson(ReqExtraBt instance) =>
|
||||
<String, dynamic>{
|
||||
'trackers': instance.trackers,
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -337,7 +338,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
extra.themeMode = ThemeMode.system.name;
|
||||
}
|
||||
if (extra.locale.isEmpty) {
|
||||
final systemLocale = getLocaleKey(ui.window.locale);
|
||||
final systemLocale = getLocaleKey(PlatformDispatcher.instance.locale);
|
||||
extra.locale = AppTranslation.translations.containsKey(systemLocale)
|
||||
? systemLocale
|
||||
: getLocaleKey(fallbackLocale);
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class CreateController extends GetxController {
|
||||
class CreateController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
// final files = [].obs;
|
||||
final RxList fileInfos = [].obs;
|
||||
final RxList selectedIndexes = [].obs;
|
||||
final RxList openedFolders = [].obs;
|
||||
final isResolving = false.obs;
|
||||
final showAdvanced = false.obs;
|
||||
late TabController advancedTabController;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
advancedTabController = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
advancedTabController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../api/model/resolve_result.dart';
|
||||
import 'package:rounded_loading_button/rounded_loading_button.dart';
|
||||
@@ -9,6 +11,7 @@ import '../../../../api/api.dart';
|
||||
import '../../../../api/model/create_task.dart';
|
||||
import '../../../../api/model/options.dart';
|
||||
import '../../../../api/model/request.dart';
|
||||
import '../../../../util/input_formatter.dart';
|
||||
import '../../../../util/message.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
@@ -22,6 +25,10 @@ class CreateView extends GetView<CreateController> {
|
||||
|
||||
final _urlController = TextEditingController();
|
||||
final _confirmController = RoundedLoadingButtonController();
|
||||
final _httpUaController = TextEditingController();
|
||||
final _httpCookieController = TextEditingController();
|
||||
final _httpRefererController = TextEditingController();
|
||||
final _btTrackerController = TextEditingController();
|
||||
|
||||
CreateView({Key? key}) : super(key: key);
|
||||
|
||||
@@ -48,68 +55,143 @@ class CreateView extends GetView<CreateController> {
|
||||
_urlController.text = details.files[0].path;
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Form(
|
||||
key: _resolveFormKey,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
autofocus: true,
|
||||
controller: _urlController,
|
||||
minLines: 1,
|
||||
maxLines: 30,
|
||||
decoration: InputDecoration(
|
||||
hintText: _hitText(),
|
||||
hintStyle: const TextStyle(fontSize: 12),
|
||||
labelText: 'downloadLink'.tr,
|
||||
icon: const Icon(Icons.link),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: _urlController.clear,
|
||||
icon: const Icon(Icons.clear),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Form(
|
||||
key: _resolveFormKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
autofocus: true,
|
||||
controller: _urlController,
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
hintText: _hitText(),
|
||||
hintStyle: const TextStyle(fontSize: 12),
|
||||
labelText: 'downloadLink'.tr,
|
||||
icon: const Icon(Icons.link),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: _urlController.clear,
|
||||
icon: const Icon(Icons.clear),
|
||||
),
|
||||
),
|
||||
validator: (v) {
|
||||
return v!.trim().isNotEmpty
|
||||
? null
|
||||
: 'downloadLinkValid'.tr;
|
||||
}),
|
||||
),
|
||||
Util.isWeb()
|
||||
? null
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () async {
|
||||
var pr = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ["torrent"]);
|
||||
if (pr != null) {
|
||||
_urlController.text = pr.files[0].path ?? "";
|
||||
}
|
||||
}),
|
||||
].where((e) => e != null).map((e) => e!).toList(),
|
||||
),
|
||||
Obx(() => Visibility(
|
||||
visible: controller.showAdvanced.value,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 40, top: 15),
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: controller.advancedTabController,
|
||||
tabs: const [
|
||||
Tab(
|
||||
text: 'HTTP',
|
||||
),
|
||||
Tab(
|
||||
text: 'BitTorrent',
|
||||
)
|
||||
],
|
||||
),
|
||||
AutoScaleTabBarView(
|
||||
controller: controller.advancedTabController,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _httpUaController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'User-Agent',
|
||||
)),
|
||||
TextFormField(
|
||||
controller: _httpCookieController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Cookie',
|
||||
)),
|
||||
TextFormField(
|
||||
controller: _httpRefererController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Referer',
|
||||
)),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _btTrackerController,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Trakers',
|
||||
hintText: 'addTrackerHit'.tr,
|
||||
)),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
))),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 15),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
controller.showAdvanced.value =
|
||||
!controller.showAdvanced.value;
|
||||
},
|
||||
child:
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Obx(() => Checkbox(
|
||||
value: controller.showAdvanced.value,
|
||||
onChanged: (bool? value) {
|
||||
controller.showAdvanced.value =
|
||||
value ?? false;
|
||||
},
|
||||
)),
|
||||
Text('advancedOptions'.tr),
|
||||
]),
|
||||
),
|
||||
validator: (v) {
|
||||
return v!.trim().isNotEmpty
|
||||
? null
|
||||
: 'downloadLinkValid'.tr;
|
||||
}),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () async {
|
||||
var pr = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ["torrent"]);
|
||||
if (pr != null) {
|
||||
_urlController.text = pr.files[0].path ?? "";
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
),
|
||||
SizedBox(
|
||||
width: 150,
|
||||
height: 40,
|
||||
child: RoundedLoadingButton(
|
||||
color: Get.theme.colorScheme.secondary,
|
||||
onPressed: _doResolve,
|
||||
controller: _confirmController,
|
||||
child: Text('confirm'.tr),
|
||||
),
|
||||
),
|
||||
child: RoundedLoadingButton(
|
||||
color: Get.theme.colorScheme.secondary,
|
||||
onPressed: _doResolve,
|
||||
controller: _confirmController,
|
||||
child: Text('confirm'.tr),
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
]))),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -122,8 +204,24 @@ class CreateView extends GetView<CreateController> {
|
||||
try {
|
||||
_confirmController.start();
|
||||
if (_resolveFormKey.currentState!.validate()) {
|
||||
Object? extra;
|
||||
if (controller.showAdvanced.value) {
|
||||
final u = Uri.parse(_urlController.text);
|
||||
if (u.scheme.startsWith("http")) {
|
||||
extra = ReqExtraHttp()
|
||||
..header = {
|
||||
"User-Agent": _httpUaController.text,
|
||||
"Cookie": _httpCookieController.text,
|
||||
"Referer": _httpRefererController.text,
|
||||
};
|
||||
} else {
|
||||
extra = ReqExtraBt()
|
||||
..trackers = Util.textToLines(_btTrackerController.text);
|
||||
}
|
||||
}
|
||||
final rr = await resolve(Request(
|
||||
url: _urlController.text,
|
||||
extra: extra,
|
||||
));
|
||||
await _showResolveDialog(rr);
|
||||
}
|
||||
@@ -146,6 +244,8 @@ class CreateView extends GetView<CreateController> {
|
||||
final appController = Get.find<AppController>();
|
||||
|
||||
final createFormKey = GlobalKey<FormState>();
|
||||
final nameController = TextEditingController();
|
||||
final connectionsController = TextEditingController();
|
||||
final pathController = TextEditingController(
|
||||
text: appController.downloaderConfig.value.downloadDir);
|
||||
final downloadController = RoundedLoadingButtonController();
|
||||
@@ -168,6 +268,23 @@ class CreateView extends GetView<CreateController> {
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(child: FileListView(files: files)),
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'rename'.tr,
|
||||
),
|
||||
),
|
||||
TextFormField(
|
||||
controller: connectionsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'connections'.tr,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
NumericalRangeFormatter(min: 1, max: 256),
|
||||
],
|
||||
),
|
||||
DirectorySelector(
|
||||
controller: pathController,
|
||||
),
|
||||
@@ -209,20 +326,18 @@ class CreateView extends GetView<CreateController> {
|
||||
return;
|
||||
}
|
||||
if (createFormKey.currentState!.validate()) {
|
||||
// if (Util.isAndroid()) {
|
||||
// if (!await Permission.storage.request().isGranted) {
|
||||
// Get.snackbar('error'.tr,
|
||||
// 'noStoragePermission'.tr);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
await createTask(CreateTask(
|
||||
rid: rr.id,
|
||||
opts: Options(
|
||||
name: '',
|
||||
name: nameController.text,
|
||||
path: pathController.text,
|
||||
selectFiles: controller.selectedIndexes
|
||||
.cast<int>())));
|
||||
selectFiles:
|
||||
controller.selectedIndexes.cast<int>(),
|
||||
extra: connectionsController.text.isEmpty
|
||||
? null
|
||||
: (OptsExtraHttp()
|
||||
..connections = int.parse(
|
||||
connectionsController.text)))));
|
||||
Get.back();
|
||||
Get.rootDelegate.offNamed(Routes.TASK);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:intl/intl.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../../generated/locales.g.dart';
|
||||
import '../../../../util/input_formatter.dart';
|
||||
import '../../../../util/locale_manager.dart';
|
||||
import '../../../../util/message.dart';
|
||||
import '../../../../util/package_info.dart';
|
||||
@@ -89,7 +90,7 @@ class SettingView extends GetView<SettingController> {
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
_NumericalRangeFormatter(min: 1, max: 256),
|
||||
NumericalRangeFormatter(min: 1, max: 256),
|
||||
],
|
||||
);
|
||||
});
|
||||
@@ -116,7 +117,7 @@ class SettingView extends GetView<SettingController> {
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
_NumericalRangeFormatter(min: 1, max: 256),
|
||||
NumericalRangeFormatter(min: 1, max: 256),
|
||||
],
|
||||
);
|
||||
});
|
||||
@@ -187,7 +188,6 @@ class SettingView extends GetView<SettingController> {
|
||||
(Key key) {
|
||||
final trackersController = TextEditingController(
|
||||
text: btExtConfig.customTrackers.join('\r\n').toString());
|
||||
const ls = LineSplitter();
|
||||
return TextField(
|
||||
key: key,
|
||||
focusNode: FocusNode(),
|
||||
@@ -198,7 +198,7 @@ class SettingView extends GetView<SettingController> {
|
||||
hintText: 'addTrackerHit'.tr,
|
||||
),
|
||||
onChanged: (value) async {
|
||||
btExtConfig.customTrackers = ls.convert(value);
|
||||
btExtConfig.customTrackers = Util.textToLines(value);
|
||||
appController.refreshTrackers();
|
||||
|
||||
await debounceSave();
|
||||
@@ -408,7 +408,7 @@ class SettingView extends GetView<SettingController> {
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
_NumericalRangeFormatter(min: 0, max: 65535),
|
||||
NumericalRangeFormatter(min: 0, max: 65535),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -605,39 +605,3 @@ class SettingView extends GetView<SettingController> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// class _ConfigItem {
|
||||
// late String label;
|
||||
// late String Function() text;
|
||||
// late Widget Function(Key key) inputItem;
|
||||
//
|
||||
// _ConfigItem(this.label, this.text, this.inputItem);
|
||||
// }
|
||||
|
||||
class _NumericalRangeFormatter extends TextInputFormatter {
|
||||
final int min;
|
||||
final int max;
|
||||
|
||||
_NumericalRangeFormatter({required this.min, required this.max});
|
||||
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
if (newValue.text.isEmpty) {
|
||||
return newValue;
|
||||
}
|
||||
var intVal = int.tryParse(newValue.text);
|
||||
if (intVal == null) {
|
||||
return oldValue;
|
||||
}
|
||||
if (intVal < min) {
|
||||
return newValue.copyWith(text: min.toString());
|
||||
} else if (intVal > max) {
|
||||
return oldValue.copyWith(text: max.toString());
|
||||
} else {
|
||||
return newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/model/meta.dart';
|
||||
import 'package:gopeed/api/model/resource.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../../api/api.dart';
|
||||
@@ -190,7 +191,7 @@ class BuildTaskListView extends GetView {
|
||||
: const SizedBox.shrink(),
|
||||
ListTile(
|
||||
// isThreeLine: true,
|
||||
title: Text(task.meta.res.name,
|
||||
title: Text(fileName(task.meta),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Get.textTheme.titleSmall),
|
||||
@@ -199,9 +200,9 @@ class BuildTaskListView extends GetView {
|
||||
style: context.textTheme.bodyLarge
|
||||
?.copyWith(color: Get.theme.disabledColor),
|
||||
),
|
||||
leading: task.meta.res.name.lastIndexOf('.') == -1
|
||||
? const Icon(FaIcons.file)
|
||||
: Icon(FaIcons.allIcons[findIcon(task.meta.res.name)]),
|
||||
leading: (task.meta.res.rootDir.isNotEmpty
|
||||
? const Icon(FaIcons.folder)
|
||||
: Icon(FaIcons.allIcons[findIcon(fileName(task.meta))])),
|
||||
|
||||
trailing: SizedBox(
|
||||
width: 180,
|
||||
@@ -220,4 +221,11 @@ class BuildTaskListView extends GetView {
|
||||
])
|
||||
]));
|
||||
}
|
||||
|
||||
String fileName(Meta meta) {
|
||||
if (meta.res.files.length > 1) {
|
||||
return meta.res.name;
|
||||
}
|
||||
return meta.opts.name.isEmpty ? meta.res.files[0].name : meta.opts.name;
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ class LocaleKeys {
|
||||
static const donate = 'donate';
|
||||
static const exit = 'exit';
|
||||
static const create = 'create';
|
||||
static const advancedOptions = 'advancedOptions';
|
||||
static const downloadLink = 'downloadLink';
|
||||
static const downloadLinkValid = 'downloadLinkValid';
|
||||
static const downloadLinkHit = 'downloadLinkHit';
|
||||
@@ -35,6 +36,7 @@ class LocaleKeys {
|
||||
static const noFileSelected = 'noFileSelected';
|
||||
static const noStoragePermission = 'noStoragePermission';
|
||||
static const selectFile = 'selectFile';
|
||||
static const rename = 'rename';
|
||||
static const basic = 'basic';
|
||||
static const advanced = 'advanced';
|
||||
static const general = 'general';
|
||||
@@ -90,6 +92,7 @@ class Locales {
|
||||
'donate': 'Donate',
|
||||
'exit': 'Exit',
|
||||
'create': 'Create Task',
|
||||
'advancedOptions': 'Advanced Options',
|
||||
'downloadLink': 'Download Link',
|
||||
'downloadLinkValid': 'Please enter the download link',
|
||||
'downloadLinkHit':
|
||||
@@ -99,6 +102,7 @@ class Locales {
|
||||
'noFileSelected': 'Please select at least one file to continue.',
|
||||
'noStoragePermission': 'Storage permission required',
|
||||
'selectFile': 'Select File',
|
||||
'rename': 'Rename',
|
||||
'basic': 'Basic',
|
||||
'advanced': 'Advanced',
|
||||
'general': 'General',
|
||||
@@ -336,6 +340,7 @@ class Locales {
|
||||
'donate': '打赏',
|
||||
'exit': '退出',
|
||||
'create': '创建任务',
|
||||
'advancedOptions': '高级选项',
|
||||
'downloadLink': '下载链接',
|
||||
'downloadLinkValid': '请输入下载链接',
|
||||
'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append',
|
||||
@@ -344,6 +349,7 @@ class Locales {
|
||||
'noFileSelected': '请至少选择一个文件下载',
|
||||
'noStoragePermission': '需要开启存储权限',
|
||||
'selectFile': '选择文件',
|
||||
'rename': '重命名',
|
||||
'basic': '基础',
|
||||
'advanced': '高级',
|
||||
'general': '通用',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
findIcon(String filename) {
|
||||
String res = 'doc';
|
||||
String res = 'file';
|
||||
String ext = filename.substring(filename.lastIndexOf('.') + 1);
|
||||
for (var iconMap in iconMaps) {
|
||||
if (iconMap['extensions'].contains(ext)) {
|
||||
|
||||
@@ -28,6 +28,7 @@ class FaIcons {
|
||||
|
||||
static const _kFontFam = 'FontAwesome';
|
||||
static const Map<String, IconData> allIcons = {
|
||||
'file': file,
|
||||
'doc': doc,
|
||||
'file_image': file_image,
|
||||
'file_video': file_video,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// A [TextInputFormatter] that restricts input to a numerical range between [min] and [max].
|
||||
class NumericalRangeFormatter extends TextInputFormatter {
|
||||
final int min;
|
||||
final int max;
|
||||
|
||||
NumericalRangeFormatter({required this.min, required this.max});
|
||||
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
if (newValue.text.isEmpty) {
|
||||
return newValue;
|
||||
}
|
||||
var intVal = int.tryParse(newValue.text);
|
||||
if (intVal == null) {
|
||||
return oldValue;
|
||||
}
|
||||
if (intVal < min) {
|
||||
return newValue.copyWith(text: min.toString());
|
||||
} else if (intVal > max) {
|
||||
return oldValue.copyWith(text: max.toString());
|
||||
} else {
|
||||
return newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -12,7 +13,9 @@ class Util {
|
||||
}
|
||||
|
||||
static String fmtByte(int byte) {
|
||||
if (byte < 1024) {
|
||||
if (byte < 0) {
|
||||
return "0 B";
|
||||
} else if (byte < 1024) {
|
||||
return "$byte B";
|
||||
} else if (byte < 1024 * 1024) {
|
||||
return "${(byte / 1024).toStringAsFixed(2)} KB";
|
||||
@@ -61,6 +64,14 @@ class Util {
|
||||
return kIsWeb;
|
||||
}
|
||||
|
||||
static List<String> textToLines(String text) {
|
||||
if (text.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
const ls = LineSplitter();
|
||||
return ls.convert(text);
|
||||
}
|
||||
|
||||
// if one future complete, return the result, only all future error, return the last error
|
||||
static anyOk<T>(Iterable<Future<T>> futures) {
|
||||
final completer = Completer<T>();
|
||||
@@ -74,9 +85,7 @@ class Util {
|
||||
}).catchError((e) {
|
||||
lastError = e;
|
||||
count--;
|
||||
print(count);
|
||||
if (count == 0) {
|
||||
print("completeError");
|
||||
completer.completeError(lastError);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,6 +49,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
autoscale_tabbarview:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: autoscale_tabbarview
|
||||
sha256: "29449e8876185acc4763cc6cf26ffcc3f842f42f1502d5964c6d85f489ee1bc4"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
badges:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -57,6 +57,7 @@ dependencies:
|
||||
uri_to_file: ^0.2.0
|
||||
window_manager: ^0.3.4
|
||||
tray_manager: ^0.2.0
|
||||
autoscale_tabbarview: ^1.0.2
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
Reference in new issue
Block a user