diff --git a/internal/protocol/bt/fetcher.go b/internal/protocol/bt/fetcher.go index ea8b91295..6806c952a 100644 --- a/internal/protocol/bt/fetcher.go +++ b/internal/protocol/bt/fetcher.go @@ -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) diff --git a/internal/protocol/bt/fetcher_test.go b/internal/protocol/bt/fetcher_test.go index 0ecff6207..7b529533b 100644 --- a/internal/protocol/bt/fetcher_test.go +++ b/internal/protocol/bt/fetcher_test.go @@ -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) diff --git a/pkg/protocol/bt/model.go b/pkg/protocol/bt/model.go new file mode 100644 index 000000000..5296d515b --- /dev/null +++ b/pkg/protocol/bt/model.go @@ -0,0 +1,5 @@ +package bt + +type ReqExtra struct { + Trackers []string `json:"trackers"` +} diff --git a/ui/flutter/android/build.gradle b/ui/flutter/android/build.gradle index a1f42e746..e394c2eb6 100644 --- a/ui/flutter/android/build.gradle +++ b/ui/flutter/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/ui/flutter/assets/locales/en_US.json b/ui/flutter/assets/locales/en_US.json index 47f642796..55b492125 100644 --- a/ui/flutter/assets/locales/en_US.json +++ b/ui/flutter/assets/locales/en_US.json @@ -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", diff --git a/ui/flutter/assets/locales/zh_CN.json b/ui/flutter/assets/locales/zh_CN.json index 16f5e78d2..4ea7e65f2 100644 --- a/ui/flutter/assets/locales/zh_CN.json +++ b/ui/flutter/assets/locales/zh_CN.json @@ -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": "通用", diff --git a/ui/flutter/lib/api/model/options.dart b/ui/flutter/lib/api/model/options.dart index 1b50d37d6..2cc82f239 100644 --- a/ui/flutter/lib/api/model/options.dart +++ b/ui/flutter/lib/api/model/options.dart @@ -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 selectFiles; - Map? extra; + Object? extra; Options({ required this.name, required this.path, required this.selectFiles, + this.extra, }); factory Options.fromJson(Map json) => diff --git a/ui/flutter/lib/api/model/options.g.dart b/ui/flutter/lib/api/model/options.g.dart index 6562554ef..5465943fe 100644 --- a/ui/flutter/lib/api/model/options.g.dart +++ b/ui/flutter/lib/api/model/options.g.dart @@ -11,7 +11,8 @@ Options _$OptionsFromJson(Map json) => Options( path: json['path'] as String, selectFiles: (json['selectFiles'] as List).map((e) => e as int).toList(), - )..extra = json['extra'] as Map?; + extra: json['extra'], + ); Map _$OptionsToJson(Options instance) { final val = { diff --git a/ui/flutter/lib/api/model/request.dart b/ui/flutter/lib/api/model/request.dart index 03b1de2b0..27eeef712 100644 --- a/ui/flutter/lib/api/model/request.dart +++ b/ui/flutter/lib/api/model/request.dart @@ -5,10 +5,11 @@ part 'request.g.dart'; @JsonSerializable(explicitToJson: true) class Request { String url; - Map? extra; + Object? extra; Request({ required this.url, + this.extra, }); factory Request.fromJson(Map json) => @@ -20,7 +21,7 @@ class Request { @JsonSerializable() class ReqExtraHttp { String method = 'GET'; - Map headers = {}; + Map header = {}; String body = ''; ReqExtraHttp(); @@ -30,3 +31,15 @@ class ReqExtraHttp { Map toJson() => _$ReqExtraHttpToJson(this); } + +@JsonSerializable() +class ReqExtraBt { + List trackers = []; + + ReqExtraBt(); + + factory ReqExtraBt.fromJson(Map json) => + _$ReqExtraBtFromJson(json); + + Map toJson() => _$ReqExtraBtToJson(this); +} diff --git a/ui/flutter/lib/api/model/request.g.dart b/ui/flutter/lib/api/model/request.g.dart index 8e221e841..d87c0de91 100644 --- a/ui/flutter/lib/api/model/request.g.dart +++ b/ui/flutter/lib/api/model/request.g.dart @@ -8,7 +8,8 @@ part of 'request.dart'; Request _$RequestFromJson(Map json) => Request( url: json['url'] as String, - )..extra = json['extra'] as Map?; + extra: json['extra'], + ); Map _$RequestToJson(Request instance) { final val = { @@ -27,12 +28,21 @@ Map _$RequestToJson(Request instance) { ReqExtraHttp _$ReqExtraHttpFromJson(Map json) => ReqExtraHttp() ..method = json['method'] as String - ..headers = Map.from(json['headers'] as Map) + ..header = Map.from(json['header'] as Map) ..body = json['body'] as String; Map _$ReqExtraHttpToJson(ReqExtraHttp instance) => { 'method': instance.method, - 'headers': instance.headers, + 'header': instance.header, 'body': instance.body, }; + +ReqExtraBt _$ReqExtraBtFromJson(Map json) => ReqExtraBt() + ..trackers = + (json['trackers'] as List).map((e) => e as String).toList(); + +Map _$ReqExtraBtToJson(ReqExtraBt instance) => + { + 'trackers': instance.trackers, + }; diff --git a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart index 5fac6e3cf..bd5de7b36 100644 --- a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart +++ b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart @@ -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); 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 7cee16f18..59be4ddd2 100644 --- a/ui/flutter/lib/app/modules/create/controllers/create_controller.dart +++ b/ui/flutter/lib/app/modules/create/controllers/create_controller.dart @@ -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(); + } } 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 0c28a18b9..6bbad62c1 100644 --- a/ui/flutter/lib/app/modules/create/views/create_view.dart +++ b/ui/flutter/lib/app/modules/create/views/create_view.dart @@ -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 { 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 { _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( - width: 150, - height: 40, ), - child: RoundedLoadingButton( - color: Get.theme.colorScheme.secondary, - onPressed: _doResolve, - controller: _confirmController, - child: Text('confirm'.tr), + SizedBox( + width: 150, + child: RoundedLoadingButton( + color: Get.theme.colorScheme.secondary, + onPressed: _doResolve, + controller: _confirmController, + child: Text('confirm'.tr), + ), ), - )), - ), - ], - ), - ), - ), + ], + ), + )), + ]))), ), ); } @@ -122,8 +204,24 @@ class CreateView extends GetView { 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 { final appController = Get.find(); final createFormKey = GlobalKey(); + 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 { 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 { 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()))); + selectFiles: + controller.selectedIndexes.cast(), + extra: connectionsController.text.isEmpty + ? null + : (OptsExtraHttp() + ..connections = int.parse( + connectionsController.text))))); Get.back(); Get.rootDelegate.offNamed(Routes.TASK); } 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 11739f7fc..33b362f00 100644 --- a/ui/flutter/lib/app/modules/setting/views/setting_view.dart +++ b/ui/flutter/lib/app/modules/setting/views/setting_view.dart @@ -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 { keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, - _NumericalRangeFormatter(min: 1, max: 256), + NumericalRangeFormatter(min: 1, max: 256), ], ); }); @@ -116,7 +117,7 @@ class SettingView extends GetView { keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, - _NumericalRangeFormatter(min: 1, max: 256), + NumericalRangeFormatter(min: 1, max: 256), ], ); }); @@ -187,7 +188,6 @@ class SettingView extends GetView { (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 { 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 { keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, - _NumericalRangeFormatter(min: 0, max: 65535), + NumericalRangeFormatter(min: 0, max: 65535), ], ), ), @@ -605,39 +605,3 @@ class SettingView extends GetView { } } } - -// 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; - } - } -} diff --git a/ui/flutter/lib/app/views/buid_task_list_view.dart b/ui/flutter/lib/app/views/buid_task_list_view.dart index 2005dd035..03a64291a 100644 --- a/ui/flutter/lib/app/views/buid_task_list_view.dart +++ b/ui/flutter/lib/app/views/buid_task_list_view.dart @@ -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; + } } diff --git a/ui/flutter/lib/generated/locales.g.dart b/ui/flutter/lib/generated/locales.g.dart index e15a18409..109ebbd5c 100644 --- a/ui/flutter/lib/generated/locales.g.dart +++ b/ui/flutter/lib/generated/locales.g.dart @@ -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': '通用', diff --git a/ui/flutter/lib/util/file_icon.dart b/ui/flutter/lib/util/file_icon.dart index dde76f710..bc321eff6 100644 --- a/ui/flutter/lib/util/file_icon.dart +++ b/ui/flutter/lib/util/file_icon.dart @@ -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)) { diff --git a/ui/flutter/lib/util/icons.dart b/ui/flutter/lib/util/icons.dart index ae7f8b788..2bceee6b5 100644 --- a/ui/flutter/lib/util/icons.dart +++ b/ui/flutter/lib/util/icons.dart @@ -28,6 +28,7 @@ class FaIcons { static const _kFontFam = 'FontAwesome'; static const Map allIcons = { + 'file': file, 'doc': doc, 'file_image': file_image, 'file_video': file_video, diff --git a/ui/flutter/lib/util/input_formatter.dart b/ui/flutter/lib/util/input_formatter.dart new file mode 100644 index 000000000..640005ce4 --- /dev/null +++ b/ui/flutter/lib/util/input_formatter.dart @@ -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; + } + } +} diff --git a/ui/flutter/lib/util/util.dart b/ui/flutter/lib/util/util.dart index d4fec500a..c8411cd80 100644 --- a/ui/flutter/lib/util/util.dart +++ b/ui/flutter/lib/util/util.dart @@ -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 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(Iterable> futures) { final completer = Completer(); @@ -74,9 +85,7 @@ class Util { }).catchError((e) { lastError = e; count--; - print(count); if (count == 0) { - print("completeError"); completer.completeError(lastError); } }); diff --git a/ui/flutter/pubspec.lock b/ui/flutter/pubspec.lock index 8cd8ed34a..93928ad2c 100644 --- a/ui/flutter/pubspec.lock +++ b/ui/flutter/pubspec.lock @@ -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: diff --git a/ui/flutter/pubspec.yaml b/ui/flutter/pubspec.yaml index 9d6c74c0c..db40a85da 100644 --- a/ui/flutter/pubspec.yaml +++ b/ui/flutter/pubspec.yaml @@ -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