From 9b1ed15feaa772fd7f7ed1798ffd19d5eb18a830 Mon Sep 17 00:00:00 2001 From: wuke2022 Date: Tue, 7 Feb 2023 21:45:48 +0800 Subject: [PATCH] feat: ui improvement flutter 3.7 compatibility responsive view beautiful progress bar select file in treeview downlading and downloaded page --- ui/flutter/README.md | 13 +- ui/flutter/lib/api/model/resource.dart | 9 +- ui/flutter/lib/api/model/resource.g.dart | 6 +- ui/flutter/lib/i18n/langs/en_us.dart | 2 + ui/flutter/lib/i18n/langs/fa_ir.dart | 2 + ui/flutter/lib/i18n/langs/ru_RU.dart | 2 + ui/flutter/lib/i18n/langs/zh_cn.dart | 2 + ui/flutter/lib/i18n/messages.dart | 3 +- ui/flutter/lib/pages/app/app_view.dart | 5 + .../lib/pages/create/create_controller.dart | 5 +- ui/flutter/lib/pages/create/create_view.dart | 36 ++- .../downloaded/downloaded_controller.dart | 31 +++ .../lib/pages/downloaded/downloaded_view.dart | 14 ++ .../downloading/downloading_controller.dart | 33 +++ .../pages/downloading/downloading_view.dart | 14 ++ ui/flutter/lib/pages/home/home_view.dart | 187 ++++++++++------ ui/flutter/lib/pages/root/root_view.dart | 14 +- .../lib/pages/task/task_controller.dart | 67 ------ ui/flutter/lib/pages/task/task_view.dart | 198 ----------------- ui/flutter/lib/routes/router.dart | 35 ++- ui/flutter/lib/theme/theme.dart | 12 +- ui/flutter/lib/util/file_icon.dart | 182 +++++++++++++++ ui/flutter/lib/util/responsive_builder.dart | 50 +++++ .../lib/widget/buid_task_list_view.dart | 210 ++++++++++++++++++ ui/flutter/lib/widget/file_list_view.dart | 207 +++++++++++++---- ui/flutter/lib/widget/task_list_view.dart | 1 + ui/flutter/pubspec.lock | 28 ++- ui/flutter/pubspec.yaml | 3 +- 28 files changed, 932 insertions(+), 439 deletions(-) create mode 100644 ui/flutter/lib/pages/downloaded/downloaded_controller.dart create mode 100644 ui/flutter/lib/pages/downloaded/downloaded_view.dart create mode 100644 ui/flutter/lib/pages/downloading/downloading_controller.dart create mode 100644 ui/flutter/lib/pages/downloading/downloading_view.dart delete mode 100644 ui/flutter/lib/pages/task/task_controller.dart delete mode 100644 ui/flutter/lib/pages/task/task_view.dart create mode 100644 ui/flutter/lib/util/file_icon.dart create mode 100644 ui/flutter/lib/util/responsive_builder.dart create mode 100644 ui/flutter/lib/widget/buid_task_list_view.dart create mode 100644 ui/flutter/lib/widget/task_list_view.dart diff --git a/ui/flutter/README.md b/ui/flutter/README.md index e891bba6..9778622f 100644 --- a/ui/flutter/README.md +++ b/ui/flutter/README.md @@ -1,16 +1,5 @@ -# gopeed -A new Flutter project. -## Getting Started -This project is a starting point for a Flutter application. -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +Don't forget to run`dart format .`before you commit to keep your code in standard dart format \ No newline at end of file diff --git a/ui/flutter/lib/api/model/resource.dart b/ui/flutter/lib/api/model/resource.dart index b42a9f70..e31b91f4 100644 --- a/ui/flutter/lib/api/model/resource.dart +++ b/ui/flutter/lib/api/model/resource.dart @@ -7,6 +7,7 @@ class Resource { String name; int size; bool range; + String rootDir; List files; String hash; @@ -14,6 +15,7 @@ class Resource { {required this.name, required this.size, required this.range, + required this.rootDir, required this.files, required this.hash}); @@ -25,17 +27,20 @@ class Resource { @JsonSerializable() class FileInfo { - String name; + // String rootDir; String path; + String name; int size; FileInfo({ - required this.name, + // required this.rootDir, required this.path, + required this.name, required this.size, }); factory FileInfo.fromJson(Map json) => _$FileInfoFromJson(json); + Map toJson() => _$FileInfoToJson(this); } diff --git a/ui/flutter/lib/api/model/resource.g.dart b/ui/flutter/lib/api/model/resource.g.dart index 496fdc1e..9de906e1 100644 --- a/ui/flutter/lib/api/model/resource.g.dart +++ b/ui/flutter/lib/api/model/resource.g.dart @@ -10,6 +10,7 @@ Resource _$ResourceFromJson(Map json) => Resource( name: json['name'] as String, size: json['size'] as int, range: json['range'] as bool, + rootDir: json['rootDir'] as String, files: (json['files'] as List) .map((e) => FileInfo.fromJson(e as Map)) .toList(), @@ -20,18 +21,19 @@ Map _$ResourceToJson(Resource instance) => { 'name': instance.name, 'size': instance.size, 'range': instance.range, + 'rootDir': instance.rootDir, 'files': instance.files.map((e) => e.toJson()).toList(), 'hash': instance.hash, }; FileInfo _$FileInfoFromJson(Map json) => FileInfo( - name: json['name'] as String, path: json['path'] as String, + name: json['name'] as String, size: json['size'] as int, ); Map _$FileInfoToJson(FileInfo instance) => { - 'name': instance.name, 'path': instance.path, + 'name': instance.name, 'size': instance.size, }; diff --git a/ui/flutter/lib/i18n/langs/en_us.dart b/ui/flutter/lib/i18n/langs/en_us.dart index ee2edefd..cd101fd0 100644 --- a/ui/flutter/lib/i18n/langs/en_us.dart +++ b/ui/flutter/lib/i18n/langs/en_us.dart @@ -7,6 +7,8 @@ const enUS = { 'cancel': 'Cancel', 'selectAll': 'Select All', 'home.task': 'Task', + 'home.downloading': 'downloading', + 'home.downloaded': 'downloaded', 'home.setting': 'Setting', 'create.title': 'Create Task', 'create.downloadLink': 'Download Link', diff --git a/ui/flutter/lib/i18n/langs/fa_ir.dart b/ui/flutter/lib/i18n/langs/fa_ir.dart index e3540ef6..1eb8a3aa 100644 --- a/ui/flutter/lib/i18n/langs/fa_ir.dart +++ b/ui/flutter/lib/i18n/langs/fa_ir.dart @@ -7,6 +7,8 @@ const faIR = { 'cancel': 'انصراف', 'selectAll': 'انتخاب همه', 'home.task': 'کار', + 'home.downloading': 'downloading', + 'home.downloaded': 'downloaded', 'home.setting': 'تنظیمات', 'create.title': 'ایجاد کار', 'create.downloadLink': 'لینک دانلود', diff --git a/ui/flutter/lib/i18n/langs/ru_RU.dart b/ui/flutter/lib/i18n/langs/ru_RU.dart index c6010b3c..1f8d51dc 100644 --- a/ui/flutter/lib/i18n/langs/ru_RU.dart +++ b/ui/flutter/lib/i18n/langs/ru_RU.dart @@ -7,6 +7,8 @@ const ruRU = { 'cancel': 'Отмена', 'selectAll': 'Выбрать все', 'home.task': 'Задачи', + 'home.downloading': 'downloading', + 'home.downloaded': 'downloaded', 'home.setting': 'Настройки', 'create.title': 'Создать задачу', 'create.downloadLink': 'Ссылка для скачивание', diff --git a/ui/flutter/lib/i18n/langs/zh_cn.dart b/ui/flutter/lib/i18n/langs/zh_cn.dart index e464f503..d7eebbb0 100644 --- a/ui/flutter/lib/i18n/langs/zh_cn.dart +++ b/ui/flutter/lib/i18n/langs/zh_cn.dart @@ -7,6 +7,8 @@ const zhCN = { 'cancel': '取消', 'selectAll': '全选', 'home.task': '任务', + 'home.downloading': '下载中', + 'home.downloaded': '已完成', 'home.setting': '设置', 'create.title': '创建任务', 'create.downloadLink': '下载链接', diff --git a/ui/flutter/lib/i18n/messages.dart b/ui/flutter/lib/i18n/messages.dart index fab15e52..7756dde3 100644 --- a/ui/flutter/lib/i18n/messages.dart +++ b/ui/flutter/lib/i18n/messages.dart @@ -22,5 +22,6 @@ const fallbackLocale = Locale('en', 'US'); class _Messages extends Translations { // just include available locales here @override - Map> get keys => {...zhCN, ...enUS, ...ruRU, ...faIR}; + Map> get keys => + {...zhCN, ...enUS, ...ruRU, ...faIR}; } diff --git a/ui/flutter/lib/pages/app/app_view.dart b/ui/flutter/lib/pages/app/app_view.dart index 1b95421e..54c14cc5 100644 --- a/ui/flutter/lib/pages/app/app_view.dart +++ b/ui/flutter/lib/pages/app/app_view.dart @@ -5,6 +5,7 @@ import 'package:get/get.dart'; import '../../i18n/messages.dart'; import '../../routes/router.dart'; import '../../theme/theme.dart'; +import '../home/home_controller.dart'; import 'app_controller.dart'; class AppView extends GetView { @@ -14,6 +15,10 @@ class AppView extends GetView { Widget build(BuildContext context) { final config = controller.downloaderConfig.value; return GetMaterialApp.router( + initialBinding: BindingsBuilder(() { + Get.lazyPut(() => HomeController(), fenix: true); + }), + useInheritedMediaQuery: true, debugShowCheckedModeBanner: false, theme: GopeedTheme.light, darkTheme: GopeedTheme.dark, diff --git a/ui/flutter/lib/pages/create/create_controller.dart b/ui/flutter/lib/pages/create/create_controller.dart index ba833f24..08fdae78 100644 --- a/ui/flutter/lib/pages/create/create_controller.dart +++ b/ui/flutter/lib/pages/create/create_controller.dart @@ -1,3 +1,6 @@ import 'package:get/get.dart'; -class CreateController extends GetxController {} +class CreateController extends GetxController { + final files = [].obs; + final RxList selectedIndexs = [].obs; +} diff --git a/ui/flutter/lib/pages/create/create_view.dart b/ui/flutter/lib/pages/create/create_view.dart index 2b162ce3..94bf896c 100644 --- a/ui/flutter/lib/pages/create/create_view.dart +++ b/ui/flutter/lib/pages/create/create_view.dart @@ -20,6 +20,7 @@ class CreateView extends GetView { final _urlController = TextEditingController(); final _confirmController = RoundedLoadingButtonController(); + CreateView({Key? key}) : super(key: key); @override @@ -28,8 +29,8 @@ class CreateView extends GetView { appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back), - onPressed: () => Get.rootDelegate.offNamed(Routes.task)), - actions: [Container()], + onPressed: () => Get.rootDelegate.offNamed(Routes.downloading)), + // actions: [], title: Text('create.title'.tr), ), body: DropTarget( @@ -47,9 +48,10 @@ class CreateView extends GetView { autofocus: true, controller: _urlController, minLines: 1, - maxLines: 15, + maxLines: 30, decoration: InputDecoration( hintText: _hitText(), + hintStyle: const TextStyle(fontSize: 12), labelText: 'create.downloadLink'.tr, icon: const Icon(Icons.link)), validator: (v) { @@ -102,16 +104,15 @@ class CreateView extends GetView { } Future _showResolveDialog(ResolveResult rr) async { - final res = rr.res; + controller.files.value = rr.res.files; final appController = Get.find(); final setting = appController.downloaderConfig.value; final createFormKey = GlobalKey(); final pathController = TextEditingController(text: setting.downloadDir); final downloadController = RoundedLoadingButtonController(); - var fileValues = List.filled(res.files.length, true); - return await showDialog( + return showDialog( context: Get.context!, barrierDismissible: false, builder: (_) => AlertDialog( @@ -129,11 +130,7 @@ class CreateView extends GetView { autovalidateMode: AutovalidateMode.always, child: Column( children: [ - Expanded( - child: FileListView( - files: res.files, - values: fileValues, - )), + Expanded(child: FileListView()), DirectorySelector( controller: pathController, ), @@ -153,7 +150,7 @@ class CreateView extends GetView { ElevatedButton.styleFrom(shape: const StadiumBorder()) .copyWith( backgroundColor: MaterialStateProperty.all( - Get.theme.backgroundColor)), + Get.theme.colorScheme.background)), onPressed: () { Get.back(); }, @@ -183,17 +180,12 @@ class CreateView extends GetView { await createTask(CreateTask( rid: rr.id, opts: Options( - name: '', - path: pathController.text, - selectFiles: fileValues - .asMap() - .entries - .where((e) => e.value) - .map((e) => e.key) - .toList(), - ))); + name: '', + path: pathController.text, + selectFiles: + controller.selectedIndexs.cast()))); Get.back(); - Get.rootDelegate.offNamed(Routes.task); + Get.rootDelegate.offNamed(Routes.downloading); } } catch (e) { Get.snackbar('error'.tr, e.toString()); diff --git a/ui/flutter/lib/pages/downloaded/downloaded_controller.dart b/ui/flutter/lib/pages/downloaded/downloaded_controller.dart new file mode 100644 index 00000000..87c10846 --- /dev/null +++ b/ui/flutter/lib/pages/downloaded/downloaded_controller.dart @@ -0,0 +1,31 @@ +import 'dart:async'; + +import 'package:get/get.dart'; + +import '../../api/api.dart'; +import '../../api/model/task.dart'; + +class DownloadedController extends GetxController { + final tasks = [].obs; + late final Timer _timer; + + @override + void onInit() async { + super.onInit(); + await getTasksState(); + _timer = Timer.periodic(const Duration(milliseconds: 1000), (timer) async { + // debugPrint('timer${DateTime.now()}'); + await getTasksState(); + }); + } + + @override + void onClose() { + super.onClose(); + _timer.cancel(); + } + + getTasksState() async { + tasks.value = await getTasks([Status.done]); + } +} diff --git a/ui/flutter/lib/pages/downloaded/downloaded_view.dart b/ui/flutter/lib/pages/downloaded/downloaded_view.dart new file mode 100644 index 00000000..9f9144cc --- /dev/null +++ b/ui/flutter/lib/pages/downloaded/downloaded_view.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:gopeed/widget/buid_task_list_view.dart'; + +import 'downloaded_controller.dart'; + +class DownloadedView extends GetView { + const DownloadedView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BuildTaskListView(tasks: controller.tasks); + } +} diff --git a/ui/flutter/lib/pages/downloading/downloading_controller.dart b/ui/flutter/lib/pages/downloading/downloading_controller.dart new file mode 100644 index 00000000..f9c8bf0c --- /dev/null +++ b/ui/flutter/lib/pages/downloading/downloading_controller.dart @@ -0,0 +1,33 @@ +import 'dart:async'; + +import 'package:get/get.dart'; + +import '../../api/api.dart'; +import '../../api/model/task.dart'; + +class DownloadingController extends GetxController { + final tasks = [].obs; + + late final Timer _timer; + + @override + void onInit() async { + super.onInit(); + await getTasksState(); + _timer = Timer.periodic(const Duration(milliseconds: 1000), (timer) async { + // debugPrint('timer${DateTime.now()}'); + await getTasksState(); + }); + } + + @override + void onClose() { + super.onClose(); + _timer.cancel(); + } + + getTasksState() async { + tasks.value = await getTasks( + [Status.ready, Status.running, Status.pause, Status.error]); + } +} diff --git a/ui/flutter/lib/pages/downloading/downloading_view.dart b/ui/flutter/lib/pages/downloading/downloading_view.dart new file mode 100644 index 00000000..d861df08 --- /dev/null +++ b/ui/flutter/lib/pages/downloading/downloading_view.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../widget/buid_task_list_view.dart'; +import 'downloading_controller.dart'; + +class DownloadingView extends GetView { + const DownloadingView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BuildTaskListView(tasks: controller.tasks); + } +} diff --git a/ui/flutter/lib/pages/home/home_view.dart b/ui/flutter/lib/pages/home/home_view.dart index 6aa8cc24..acbf14dd 100644 --- a/ui/flutter/lib/pages/home/home_view.dart +++ b/ui/flutter/lib/pages/home/home_view.dart @@ -4,6 +4,7 @@ import 'package:stylish_bottom_bar/model/bar_items.dart'; import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; import '../../routes/router.dart'; +import '../../util/responsive_builder.dart'; import 'home_controller.dart'; class HomeView extends GetView { @@ -14,73 +15,127 @@ class HomeView extends GetView { return GetRouterOutlet.builder( builder: (context, delegate, currentRoute) { //This router outlet handles the appbar and the bottom navigation bar - final currentLocation = currentRoute?.location; - var currentIndex = 0; - if (currentLocation?.startsWith(Routes.setting) == true) { - currentIndex = 1; - } return Scaffold( - extendBody: true, //to make floating action button notch transparent - - //to avoid the floating action button overlapping behavior, - // when a soft keyboard is displayed - // resizeToAvoidBottomInset: false, - body: GetRouterOutlet( - initialRoute: Routes.task, - - //delegate: Get.nestedKey(Routes.HOME), - // key: Get.nestedKey(Routes.HOME), - // key: Get.nestedKey(Routes.home), - ), - - bottomNavigationBar: StylishBottomBar( - option: AnimatedBarOptions( - iconSize: 32, - barAnimation: BarAnimation.fade, - iconStyle: IconStyle.Default, - opacity: 0.3, - ), - items: [ - BottomBarItem( - icon: const Icon(Icons.list), - selectedColor: Get.theme.primaryColor, - title: Text('home.task'.tr)), - BottomBarItem( - icon: const Icon(Icons.settings), - selectedColor: Get.theme.primaryColor, - title: Text('home.setting'.tr)), - ], - hasNotch: true, - currentIndex: currentIndex, - onTap: (index) { - switch (index) { - case 0: - delegate.toNamed(Routes.task); - break; - case 1: - delegate.toNamed(Routes.setting); - break; - } - }, - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - /* Get.isDarkMode - ? { - Get.changeTheme(GopeedTheme.light), - Get.changeThemeMode(ThemeMode.light) - } - : { - Get.changeTheme(GopeedTheme.dark), - Get.changeThemeMode(ThemeMode.dark) - }; */ - Get.rootDelegate.toNamed(Routes.create); - }, - child: const Icon( - Icons.add, - )), - floatingActionButtonLocation: - FloatingActionButtonLocation.centerDocked, + extendBody: true, + body: Row( + // crossAxisAlignment: CrossAxisAlignment.start, + children: [ + !ResponsiveBuilder.isNarrow(context) + ? NavigationRail( + extended: true, + labelType: NavigationRailLabelType.none, + minExtendedWidth: 140, + groupAlignment: 0, + // useIndicator: false, + onDestinationSelected: (int index) { + switch (index) { + case 0: + delegate.toNamed(Routes.downloading); + controller.currentIndex.value = 0; + break; + case 1: + delegate.toNamed(Routes.downloaded); + controller.currentIndex.value = 1; + break; + case 2: + delegate.toNamed(Routes.setting); + controller.currentIndex.value = 2; + break; + } + }, + destinations: [ + NavigationRailDestination( + icon: const Icon(Icons.format_align_left_outlined), + selectedIcon: + const Icon(Icons.format_align_left_outlined), + label: Text('home.downloading'.tr), + ), + NavigationRailDestination( + icon: + const Icon(Icons.format_align_justify_outlined), + selectedIcon: + const Icon(Icons.format_align_justify_outlined), + label: Text('home.downloaded'.tr), + ), + NavigationRailDestination( + icon: const Icon(Icons.settings), + selectedIcon: const Icon(Icons.settings), + label: Text('home.setting'.tr), + ), + ], + selectedIndex: controller.currentIndex.value, + leading: const Icon(Icons.logo_dev), + // trailing: const Icon(Icons.info_outline), + ) + : const SizedBox.shrink(), + Expanded( + child: GetRouterOutlet( + initialRoute: Routes.downloading, + )) + ]), + bottomNavigationBar: ResponsiveBuilder.isNarrow(context) + ? StylishBottomBar( + option: AnimatedBarOptions( + iconSize: 32, + barAnimation: BarAnimation.blink, + iconStyle: IconStyle.Default, + opacity: 0.3, + ), + items: [ + BottomBarItem( + icon: const Icon(Icons.format_align_left_outlined), + selectedColor: Get.theme.primaryColor, + title: Text('home.downloading'.tr)), + BottomBarItem( + icon: const Icon(Icons.format_align_justify_outlined), + selectedColor: Get.theme.primaryColor, + title: Text('home.downloaded'.tr)), + BottomBarItem( + icon: const Icon(Icons.settings), + selectedColor: Get.theme.primaryColor, + title: Text('home.setting'.tr)), + ], + // hasNotch: true, + currentIndex: controller.currentIndex.value, + onTap: (index) { + switch (index) { + case 0: + delegate.toNamed(Routes.downloading); + controller.currentIndex.value = 0; + break; + case 1: + delegate.toNamed(Routes.downloaded); + controller.currentIndex.value = 1; + break; + case 2: + delegate.toNamed(Routes.setting); + controller.currentIndex.value = 2; + break; + } + }, + ) + : const SizedBox.shrink(), + // floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling, + // floatingActionButton: ResponsiveBuilder.isNarrow(context) + // ? FloatingActionButton( + // onPressed: () { + // /* Get.isDarkMode + // ? { + // Get.changeTheme(GopeedTheme.light), + // Get.changeThemeMode(ThemeMode.light) + // } + // : { + // Get.changeTheme(GopeedTheme.dark), + // Get.changeThemeMode(ThemeMode.dark) + // }; */ + // Get.rootDelegate.toNamed(Routes.create); + // }, + // child: const Icon( + // Icons.add, + // )) + // : null, + // floatingActionButtonLocation: + // FloatingActionButtonLocation.centerDocked, ); }, ); diff --git a/ui/flutter/lib/pages/root/root_view.dart b/ui/flutter/lib/pages/root/root_view.dart index b23a1d55..35d9fdbe 100644 --- a/ui/flutter/lib/pages/root/root_view.dart +++ b/ui/flutter/lib/pages/root/root_view.dart @@ -11,14 +11,12 @@ class RootView extends GetView { Widget build(BuildContext context) { return GetRouterOutlet.builder( builder: (context, delegate, current) { - return Scaffold( - body: GetRouterOutlet( - initialRoute: Routes.home, - // anchorRoute: '/', - // filterPages: (afterAnchor) { - // return afterAnchor.take(1); - // }, - ), + return GetRouterOutlet( + initialRoute: Routes.home, + // anchorRoute: '/', + // filterPages: (afterAnchor) { + // return afterAnchor.take(1); + // }, ); }, ); diff --git a/ui/flutter/lib/pages/task/task_controller.dart b/ui/flutter/lib/pages/task/task_controller.dart deleted file mode 100644 index d837b506..00000000 --- a/ui/flutter/lib/pages/task/task_controller.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:async'; - -import 'package:get/get.dart'; - -import '../../api/api.dart'; -import '../../api/model/task.dart'; - -class TaskController extends GetxController { - final tabIndex = 0.obs; - final unDoneTasks = [].obs; - final doneTasks = [].obs; - - var _lastUnDoneSize = 0; - var _lastDoneSize = 0; - - late final Timer _timer; - - loadTab() async { - if (tabIndex.value == 0) { - if (await _loadUnDone()) { - await _loadDone(); - } - } else { - if (await _loadDone()) { - await _loadUnDone(); - } - } - } - - @override - void onClose() { - super.onClose(); - - _timer.cancel(); - } - - @override - void onInit() { - super.onInit(); - - loadTab(); - _timer = Timer.periodic(const Duration(milliseconds: 1000), (timer) async { - loadTab(); - }); - } - - _loadDone() async { - doneTasks.value = await getTasks([Status.done]); - final lastLastSize = _lastDoneSize; - if (lastLastSize != doneTasks.length) { - _lastDoneSize = doneTasks.length; - } - // when doneTasks.length increase, it means some task is done, refresh unDoneTasks - return lastLastSize < _lastDoneSize; - } - - _loadUnDone() async { - unDoneTasks.value = await getTasks( - [Status.ready, Status.running, Status.pause, Status.error]); - final lastLastSize = _lastUnDoneSize; - if (lastLastSize != unDoneTasks.length) { - _lastUnDoneSize = unDoneTasks.length; - } - // when unDoneTasks.length reduce, it means some task is done, refresh doneTasks - return lastLastSize > _lastUnDoneSize; - } -} diff --git a/ui/flutter/lib/pages/task/task_view.dart b/ui/flutter/lib/pages/task/task_view.dart deleted file mode 100644 index 4caece06..00000000 --- a/ui/flutter/lib/pages/task/task_view.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:styled_widget/styled_widget.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import '../../api/api.dart'; -import '../../api/model/task.dart'; -import '../../util/util.dart'; -import 'task_controller.dart'; - -class Item extends StatelessWidget { - final Task task; - - const Item({Key? key, required this.task}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Card( - elevation: 4.0, - child: InkWell( - onTap: () {}, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text(task.meta.res.name), - leading: const Icon(Icons.insert_drive_file), - ), - Row( - children: [ - SizedBox( - width: 180, - child: Text( - "${isDone() ? "" : "${Util.fmtByte(task.progress.downloaded)} / "}${Util.fmtByte(task.size)}", - style: context.textTheme.bodyText1 - ?.copyWith(color: Get.theme.disabledColor), - ).padding(left: 18)), - Flexible( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text("${Util.fmtByte(task.progress.speed)} / s", - style: context.textTheme.subtitle2), - ...buildActions() - ], - )), - ], - ), - LinearProgressIndicator( - value: getProgress(), - ), - ], - ), - )).padding(horizontal: 8, top: 8); - } - - List buildActions() { - final list = []; - if (isDone()) { - if (Util.isDesktop()) { - list.add(IconButton( - icon: const Icon(Icons.folder_open), - onPressed: () async { - final file = File(Util.buildAbsPath(task.meta.opts.path, - task.meta.res.files[0].path, task.meta.res.files[0].name)); - await launchUrl(file.parent.uri); - }, - )); - } - } else { - if (isRunning()) { - list.add(IconButton( - icon: const Icon(Icons.pause), - onPressed: () async { - await pauseTask(task.id); - }, - )); - } else { - list.add(IconButton( - icon: const Icon(Icons.play_arrow), - onPressed: () async { - await continueTask(task.id); - }, - )); - } - } - list.add(IconButton( - icon: const Icon(Icons.delete), - onPressed: () { - _showDeleteDialog(task.id); - }, - )); - return list; - } - - double getProgress() { - return task.size <= 0 ? 1 : task.progress.downloaded / task.size; - } - - bool isDone() { - return task.status == Status.done; - } - - bool isRunning() { - return task.status == Status.running; - } - - Future _showDeleteDialog(String id) { - final keep = true.obs; - - final context = Get.context!; - - return showDialog( - context: context, - barrierDismissible: false, - builder: (_) => AlertDialog( - title: Text('task.deleteTask'.tr), - content: Obx(() => CheckboxListTile( - value: keep.value, - title: Text('task.deleteTaskTip'.tr, - style: context.textTheme.bodyText1), - onChanged: (v) { - keep.value = v!; - })), - actions: [ - TextButton( - child: Text('cancel'.tr), - onPressed: () => Get.back(), - ), - TextButton( - child: Text( - 'task.delete'.tr, - style: const TextStyle(color: Colors.redAccent), - ), - onPressed: () async { - try { - await deleteTask(id, !keep.value); - Get.find().loadTab(); - Get.back(); - } catch (e) { - Get.snackbar('error'.tr, e.toString()); - } - }, - ), - ], - )); - } -} - -class TaskView extends GetView { - const TaskView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return DefaultTabController( - length: 2, - child: Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(56), - child: AppBar( - bottom: TabBar( - tabs: const [ - Tab( - icon: Icon(Icons.file_download), - ), - Tab( - icon: Icon(Icons.done), - ), - ], - onTap: (index) { - if (controller.tabIndex.value != index) { - controller.tabIndex.value = index; - controller.loadTab(); - } - }, - ), - )), - body: TabBarView( - children: [ - Obx(() => buildTaskList(controller.unDoneTasks)), - Obx(() => buildTaskList(controller.doneTasks)), - ], - ), - ), - ); - } - - Widget buildTaskList(List tasks) { - return ListView.builder( - itemCount: tasks.length, - itemBuilder: (context, index) { - return Item(task: tasks[index]); - }, - ); - } -} diff --git a/ui/flutter/lib/routes/router.dart b/ui/flutter/lib/routes/router.dart index ba593753..eae3944d 100644 --- a/ui/flutter/lib/routes/router.dart +++ b/ui/flutter/lib/routes/router.dart @@ -2,14 +2,16 @@ import 'package:get/get.dart'; import '../pages/create/create_controller.dart'; import '../pages/create/create_view.dart'; +import '../pages/downloaded/downloaded_controller.dart'; +import '../pages/downloaded/downloaded_view.dart'; +import '../pages/downloading/downloading_controller.dart'; +import '../pages/downloading/downloading_view.dart'; import '../pages/home/home_controller.dart'; import '../pages/home/home_view.dart'; import '../pages/root/root_controller.dart'; import '../pages/root/root_view.dart'; import '../pages/setting/setting_controller.dart'; import '../pages/setting/setting_view.dart'; -import '../pages/task/task_controller.dart'; -import '../pages/task/task_view.dart'; abstract class _Paths { static const root = '/'; @@ -17,7 +19,8 @@ abstract class _Paths { static const create = '/create'; static const home = '/home'; - static const task = '/task'; + static const downloading = '/downloading'; + static const downloaded = '/downloaded'; static const setting = '/setting'; } @@ -27,13 +30,16 @@ class Routes { static const create = _Paths.create; static const home = _Paths.home; - static const task = _Paths.home + _Paths.task; + static const downloading = _Paths.home + _Paths.downloading; + static const downloaded = _Paths.home + _Paths.downloaded; + static const setting = _Paths.home + _Paths.setting; static final routes = [ GetPage( name: _Paths.root, participatesInRootNavigator: true, + transition: Transition.topLevel, // preventDuplicates: true, page: () => const RootView(), binding: BindingsBuilder(() { @@ -52,6 +58,7 @@ class Routes { ), GetPage( name: _Paths.home, + // transition: Transition.upToDown, // preventDuplicates: true, page: () => const HomeView(), binding: BindingsBuilder(() { @@ -60,14 +67,28 @@ class Routes { }), children: [ GetPage( - name: _Paths.task, - page: () => const TaskView(), + name: _Paths.downloading, + transition: Transition.noTransition, + page: () => const DownloadingView(), binding: BindingsBuilder(() { - Get.lazyPut(() => TaskController()); + Get.lazyPut( + () => DownloadingController(), + ); + }), + ), + GetPage( + name: _Paths.downloaded, + transition: Transition.noTransition, + page: () => const DownloadedView(), + binding: BindingsBuilder(() { + Get.lazyPut( + () => DownloadedController(), + ); }), ), GetPage( name: _Paths.setting, + transition: Transition.noTransition, page: () => const SettingView(), binding: BindingsBuilder(() { Get.lazyPut(() => SettingController()); diff --git a/ui/flutter/lib/theme/theme.dart b/ui/flutter/lib/theme/theme.dart index f102f018..6c0e2078 100644 --- a/ui/flutter/lib/theme/theme.dart +++ b/ui/flutter/lib/theme/theme.dart @@ -25,13 +25,17 @@ class GopeedTheme { 700: Color(0xFF7FFF7A), }); - static final _light = - ThemeData(brightness: Brightness.light, primarySwatch: _gopeedreen); + static final _light = ThemeData( + //TODO useMaterial3: true, + brightness: Brightness.light, + primarySwatch: _gopeedreen); static final light = _light.copyWith( colorScheme: _light.colorScheme.copyWith(secondary: _gopeedreenAccent)); - static final _dark = - ThemeData(brightness: Brightness.dark, primarySwatch: _gopeedreen); + static final _dark = ThemeData( + //TODO useMaterial3: true, + brightness: Brightness.dark, + primarySwatch: _gopeedreen); static final dark = _dark.copyWith( colorScheme: _dark.colorScheme.copyWith(secondary: _gopeedreenAccent)); } diff --git a/ui/flutter/lib/util/file_icon.dart b/ui/flutter/lib/util/file_icon.dart new file mode 100644 index 00000000..40c64caa --- /dev/null +++ b/ui/flutter/lib/util/file_icon.dart @@ -0,0 +1,182 @@ +findIcon(String ext) { + String res = 'document'; + for (var iconMap in iconMaps) { + if (iconMap['extensions'].contains(ext)) { + res = iconMap['thumbnail']; + break; + } + } + return res; +} + +const List iconMaps = [ + //media + { + 'extensions': ['jpg', 'png', 'gif', 'bmp', 'jpeg', 'jpe', 'jif', 'jfif', 'jfi', 'webp', 'tiff', 'tif', 'ico', 'svg', 'webp'], + 'type': 'Image', + 'thumbnail': 'file_image' + }, + { + 'extensions': ['mp4', 'webm', 'mpg', 'mp2', 'mpeg', 'mpe', 'mpv', 'ocg', 'm4p', 'm4v', 'avi', 'wmv', 'mov', 'qt', 'flv', 'swf', 'mkv', 'rmvb', 'rm', 'vob', '3gp'], + 'type': 'Video', + 'thumbnail': 'my_movies_t_v' + }, + { + 'extensions': ['mp3', 'ogg', 'ogm', 'wav', '.aac', '.ape', '.flac', '.flav', '.m4a', '.wma'], + 'type': 'Audio', + 'thumbnail': 'music_in_collection_fill' + }, + //compressed + { + 'extensions': ['7z', 'brotli', 'bzip2', 'gz', 'gzip', 'rar', 'tgz', 'xz', 'zip', 'zstd', 'deb', 'msi', 'snap', 'iso', 'dmg', 'dockerfile', 'dockerignore'], + 'type': 'Archive', + 'thumbnail': 'package' + }, + //office relative + { + 'extensions': ['pdf'], + 'type': 'Portable Document Format', + 'thumbnail': 'pdf' + }, + { + 'extensions': ['txt', 'doc', 'docb', 'docm', 'dot', 'dotm', 'docx', 'rtf'], + 'type': 'Word Document', + 'thumbnail': 'text_document_edit' + }, + { + 'extensions': ['xlsx', 'xls', 'xlsb', 'xls', 'ods', 'fods', 'csv'], + 'type': 'Excel Document', + 'thumbnail': 'excel_document' + }, + { + 'extensions': ['pot', 'potm', 'potx', 'ppam', 'pps', 'ppsm', 'ppsx', 'ppt', 'pptn', 'pptx'], + 'type': 'Powerpoint Document', + 'thumbnail': 'power_point_document' + }, + //other + { + 'extensions': ['lnk'], + 'type': 'Shortcut', + 'thumbnail': 'file_symlink' + }, + { + 'extensions': ['ini', 'dlc', 'config', 'conf', 'properties', 'prop', 'settings', 'option', 'props', 'toml', 'prefs', 'sln.dotsettings', 'sln.dotsettings.user', 'cfg'], + 'type': 'Settings', + 'thumbnail': 'text_document_settings' + }, + { + 'extensions': ['html', 'htm', 'xhtml', 'html_vm'], + 'type': 'html', + 'thumbnail': 'file_h_t_m_l' + }, + //code relative + // { + // 'extensions': ['sh', 'bat'], + // 'type': 'Batch', + // 'thumbnail': 'power_shell' + // }, + // { + // 'extensions': [ + // 'js', + // 'jsx', + // 'json', + // 'tsbuildinfo', + // 'json5', + // 'jsonl', + // 'ndjson' + // ], + // 'type': 'JavaScript', + // 'thumbnail': 'js' + // }, + // { + // 'extensions': ['ts', 'tsx'], + // 'type': 'TypeScript', + // 'thumbnail': 'type_script_language' + // }, + // + // { + // 'extensions': ['asp', 'aspx', 'php', 'jsp'], + // 'type': 'HyperText Markup Language', + // 'thumbnail': 'file_a_s_p_x' + // }, + // { + // 'extensions': [ + // 'accdb', + // 'db', + // 'db3', + // 'mdb', + // 'pdb', + // 'pgsql', + // 'pkb', + // 'pks', + // 'postgres', + // 'psql', + // 'sql', + // 'sqlite', + // 'sqlite3' + // ], + // 'type': 'Database', + // 'thumbnail': 'database' + // }, + // + // { + // 'extensions': ['exe'], + // 'type': 'Executable', + // 'thumbnail': 'product' + // }, + // { + // 'extensions': [ + // 'c', + // 'h', + // 'cc', + // 'cpp', + // 'cxx', + // 'c++', + // 'cp', + // 'mm', + // 'mii', + // 'ii', + // 'dart', + // 'go' + // ], + // 'type': 'other Program language', + // 'thumbnail': 'file_code' + // }, + // { + // 'extensions': ['py', 'py3', 'pyc', 'pylintrc', 'python-version'], + // 'type': 'Python Program', + // 'thumbnail': 'py' + // }, + // { + // 'extensions': ['md', 'markdown', 'rst'], + // 'type': 'Markdown', + // 'thumbnail': 'mark_down_language' + // }, + // { + // 'extensions': ['yml', 'yaml'], + // 'type': 'Yet Anoter Markup Language', + // 'thumbnail': 'file_y_m_l' + // }, + // + // { + // 'type': 'Visual Studio', + // 'extensions': [ + // 'csproj', + // 'ruleset', + // 'sln', + // 'suo', + // 'vb', + // 'vbs', + // 'vcxitems', + // 'vcxitems.filters', + // 'vcxproj', + // 'vcxproj.filters' + // ], + // 'thumbnail': 'visual_studio_for_windows' + // }, + // { + // 'extensions': ['java'], + // 'type': 'Java Program', + // 'thumbnail': 'file_j_a_v_a' + // }, +]; diff --git a/ui/flutter/lib/util/responsive_builder.dart b/ui/flutter/lib/util/responsive_builder.dart new file mode 100644 index 00000000..0cf4ffd6 --- /dev/null +++ b/ui/flutter/lib/util/responsive_builder.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +class ResponsiveBuilder extends StatelessWidget { + const ResponsiveBuilder({ + required this.narrowBuilder, + required this.mediumBuilder, + required this.wideBuilder, + Key? key, + }) : super(key: key); + + final Widget Function( + BuildContext context, + BoxConstraints constraints, + ) narrowBuilder; + + final Widget Function( + BuildContext context, + BoxConstraints constraints, + ) mediumBuilder; + + final Widget Function( + BuildContext context, + BoxConstraints constraints, + ) wideBuilder; + + static bool isNarrow(BuildContext context) => + MediaQuery.of(context).size.width < 768; + + static bool isMedium(BuildContext context) => + MediaQuery.of(context).size.width < 992 && + MediaQuery.of(context).size.width >= 768; + + static bool isWide(BuildContext context) => + MediaQuery.of(context).size.width >= 992; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth >= 992) { + return wideBuilder(context, constraints); + } else if (constraints.maxWidth >= 768) { + return mediumBuilder(context, constraints); + } else { + return narrowBuilder(context, constraints); + } + }, + ); + } +} diff --git a/ui/flutter/lib/widget/buid_task_list_view.dart b/ui/flutter/lib/widget/buid_task_list_view.dart new file mode 100644 index 00000000..2cd9af03 --- /dev/null +++ b/ui/flutter/lib/widget/buid_task_list_view.dart @@ -0,0 +1,210 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../api/api.dart'; +import '../../api/model/task.dart'; +import '../../routes/router.dart'; +import '../../util/util.dart'; + +class BuildTaskListView extends GetView { + final List tasks; + + const BuildTaskListView({ + Key? key, + required this.tasks, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + actions: [ + IconButton( + icon: const Icon(Icons.add), + tooltip: 'create.title'.tr, + onPressed: () { + Get.rootDelegate.toNamed(Routes.create); + }, + ), + //TODO appBar toggleALl/start selected/delete selected/ + // IconButton( + // icon: const Icon(Icons.pause), + // tooltip: 'create.title'.tr, + // onPressed: () { + // // pause all + // }, + // ), + // IconButton( + // icon: const Icon(Icons.delete), + // tooltip: 'create.title'.tr, + // onPressed: () { + // // delete all + // }, + // ) + ], + ), + body: Obx(() { + return buildTaskList(context, tasks); + })); + } + + Widget buildTaskList(BuildContext context, tasks) { + return ListView.builder( + itemCount: tasks.length, + itemBuilder: (context, index) { + return item(context, tasks[index]); + }, + ); + } + + Widget item(BuildContext context, Task task) { + bool isDone() { + return task.status == Status.done; + } + + bool isRunning() { + return task.status == Status.running; + } + + Future showDeleteDialog(String id) { + final keep = true.obs; + + final context = Get.context!; + + return showDialog( + context: context, + barrierDismissible: false, + builder: (_) => AlertDialog( + title: Text('task.deleteTask'.tr), + content: Obx(() => CheckboxListTile( + value: keep.value, + title: Text('task.deleteTaskTip'.tr, + style: context.textTheme.bodyLarge), + onChanged: (v) { + keep.value = v!; + })), + actions: [ + TextButton( + child: Text('cancel'.tr), + onPressed: () => Get.back(), + ), + TextButton( + child: Text( + 'task.delete'.tr, + style: const TextStyle(color: Colors.redAccent), + ), + onPressed: () async { + try { + await deleteTask(id, !keep.value); + Get.back(); + } catch (e) { + Get.snackbar('error'.tr, e.toString()); + } + }, + ), + ], + )); + } + + List buildActions() { + final list = []; + if (isDone()) { + if (Util.isDesktop()) { + list.add(IconButton( + icon: const Icon(Icons.folder_open), + onPressed: () async { + final file = File(Util.buildAbsPath(task.meta.opts.path, + task.meta.res.files[0].path, task.meta.res.files[0].name)); + await launchUrl(file.parent.uri); + }, + )); + } + } else { + if (isRunning()) { + list.add(IconButton( + icon: const Icon(Icons.pause), + onPressed: () async { + await pauseTask(task.id); + }, + )); + } else { + list.add(IconButton( + icon: const Icon(Icons.play_arrow), + onPressed: () async { + await continueTask(task.id); + }, + )); + } + } + list.add(IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + showDeleteDialog(task.id); + }, + )); + return list; + } + + double getProgress() { + return task.size <= 0 ? 1 : task.progress.downloaded / task.size; + } + + Color pickColor() { + switch (task.status) { + // ready, running, pause, error, done + case Status.running: + return Get.theme.colorScheme.primary; + // case Status.pause: + // return Get.theme.colorScheme.secondary; + case Status.error: + return Get.theme.colorScheme.error; + default: + return Get.theme.colorScheme.primary; + } + } + + return InkWell( + // onTap: () {}, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Stack(children: [ + !isDone() + ? Opacity( + opacity: 0.6, + child: LinearProgressIndicator( + backgroundColor: Colors.transparent, + color: pickColor(), + minHeight: 66, + value: getProgress(), + ), + ) + : const SizedBox.shrink(), + ListTile( + title: Text(task.meta.res.name, + maxLines: 2, overflow: TextOverflow.ellipsis), + subtitle: Text( + "${isDone() ? "" : "${Util.fmtByte(task.progress.downloaded)} / "}${Util.fmtByte(task.size)}", + style: context.textTheme.bodyLarge + ?.copyWith(color: Get.theme.disabledColor), + ), + leading: const Icon(Icons.insert_drive_file), + trailing: SizedBox( + width: 180, + child: Row( + // crossAxisAlignment: CrossAxisAlignment.baseline, + // textBaseline: DefaultTextStyle.of(context).style.textBaseline, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text("${Util.fmtByte(task.progress.speed)} / s", + style: context.textTheme.titleSmall), + ...buildActions() + ], + ), + ), + ) + ]) + ])); + } +} diff --git a/ui/flutter/lib/widget/file_list_view.dart b/ui/flutter/lib/widget/file_list_view.dart index 7293d876..4556681e 100644 --- a/ui/flutter/lib/widget/file_list_view.dart +++ b/ui/flutter/lib/widget/file_list_view.dart @@ -1,60 +1,175 @@ +import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:path/path.dart' as path; -import '../api/model/resource.dart'; +import '../pages/app/app_controller.dart'; +import '../pages/create/create_controller.dart'; +import '../util/file_icon.dart'; import '../util/util.dart'; -class FileListView extends StatefulWidget { - final List files; - final List values; - - const FileListView({ +class FileListView extends GetView { + FileListView({ Key? key, - required this.files, - required this.values, }) : super(key: key); + final parentController = Get.find(); + final appController = Get.find(); - @override - State createState() => _FileListViewState(); -} + List buildTreeViewItemsRecursive( + List fileInfos, int level, List treeViewItems) { + List children = fileInfos.where((e) => e['level'] == level).toList(); + for (int i = 0; i < children.length; i++) { + Map fileInfo = children[i]; + if (fileInfo['size'] == null) { + // folder + treeViewItems.add(fluent.TreeViewItem( + // expanded: false, bug on init + leading: const Icon(Icons.folder_open), + content: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + Expanded( + child: Text( + fileInfo['name'], + overflow: TextOverflow.ellipsis, + // style: context.textTheme.titleSmall, + )), + ]), + // onExpandToggle: (Fluent.TreeViewItem item, bool getsExpanded) { + //TODO + // item.expanded = getsExpanded; + // }, + children: buildTreeViewItemsRecursive( + fileInfos.where((e) => e['level'] > level).toList(), + level + 1, []))); + } else { + // file + if (fileInfo['selected']) { + parentController.selectedIndexs.add(fileInfo['fileId']); + } + treeViewItems.add(fluent.TreeViewItem( + value: fileInfo['fileId'], + selected: fileInfo['selected'], + collapsable: false, + leading: fileInfo['name'].lastIndexOf('.') == -1 + ? const Icon(fluent.FluentIcons.document) + : Icon(fluent.FluentIcons.allIcons[findIcon(fileInfo['name'] + .substring(fileInfo['name'].lastIndexOf('.') + 1))]), + content: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + Expanded( + child: Text( + fileInfo['name'], + overflow: TextOverflow.ellipsis, + // style: context.textTheme.titleSmall, + )), + Text( + Util.fmtByte( + fileInfo['size'], + ), + // style: context.textTheme.labelMedium, + overflow: TextOverflow.ellipsis, + ), + ]), + )); + } + } + return treeViewItems; + } -class _FileListViewState extends State { - List get _files => widget.files; - - List get _values => widget.values; + List get items { + List fileInfos = []; + int idNext = 0; + //make fileInfos list + for (var i = 0; i < parentController.files.length; i++) { + //parentId -1 means path root + int parentId = -1; + List folders = path.split(parentController.files[i].path); + for (var folder in folders) { + int indexInfileInfos = fileInfos.lastIndexWhere((fileInfo) => + fileInfo['name'] == folder && fileInfo['parentId'] == parentId); + //TODO calculate folder size + if (indexInfileInfos != -1) { + // folder exists + parentId = fileInfos[indexInfileInfos]['id']; + } else { + // parent not exist + // create one and add index to parent's children + fileInfos.add({ + 'id': idNext, + 'type': 'folder', + 'name': folder, + 'parentId': parentId, + 'level': folders.indexOf(folder), + 'children': [], + }); + if (parentId != -1) { + fileInfos[parentId]['children'].add(idNext); + } + parentId = idNext; + idNext++; + } + } + //add one file, add index to parent + fileInfos.add({ + 'id': idNext, + 'type': 'file', + 'fileId': i, + 'level': folders.length, + 'name': parentController.files[i].name, + 'size': parentController.files[i].size, + 'parentId': parentId, + //TODO add unselected logic + 'selected': true + }); + if (parentId != -1) { + fileInfos[parentId]['children'].add(idNext); + } + idNext++; + } + List treeItems = + buildTreeViewItemsRecursive(fileInfos, 0, []); + return treeItems; + } @override Widget build(BuildContext context) { - final themeData = Theme.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding(padding: EdgeInsets.only(top: 10)), - Text('create.selectFile'.tr, - style: TextStyle(color: themeData.hintColor)), - Expanded( - child: Container( - margin: const EdgeInsets.only(top: 10), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey, width: 1), - borderRadius: BorderRadius.circular(5)), - child: ListView.builder( - itemCount: _files.length, - itemBuilder: (context, index) { - var file = _files[index]; - return CheckboxListTile( - value: _values[index], - onChanged: (value) { - setState(() { - _values[index] = value!; - }); - }, - title: Text(Util.buildPath(file.path, file.name)), - subtitle: Text(Util.fmtByte(file.size)), - secondary: const Icon(Icons.description), - ); - }))), - ], - ); + final appController = Get.find(); + return fluent.FluentTheme( + data: appController.downloaderConfig.value.extra.themeMode == 'dark' + ? fluent.ThemeData(brightness: Brightness.dark) + : fluent.ThemeData(brightness: Brightness.light), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding(padding: EdgeInsets.only(top: 10)), + Text( + 'create.selectFile'.tr, + // style: TextStyle(color: themeData.hintColor), + ), + Expanded( + child: Container( + margin: const EdgeInsets.only(top: 10), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey, width: 1), + borderRadius: BorderRadius.circular(5)), + child: fluent.TreeView( + //changes all when folder selector is changed + onSelectionChanged: (selectedItems) async => { + parentController.selectedIndexs.value = + selectedItems.map((j) => j.value).toList() + // for (var i = 0; i < parentController.files.length; i++) + // { + // selectedItems + // .map((j) => j.value) + // .toList() + // .contains(i) + // ? values[i] = true + // : values[i] = false + // } + }, + narrowSpacing: true, + scrollPrimary: true, + selectionMode: fluent.TreeViewSelectionMode.multiple, + items: items))), + ], + )); } } diff --git a/ui/flutter/lib/widget/task_list_view.dart b/ui/flutter/lib/widget/task_list_view.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/ui/flutter/lib/widget/task_list_view.dart @@ -0,0 +1 @@ + diff --git a/ui/flutter/pubspec.lock b/ui/flutter/pubspec.lock index 83babf51..072068bb 100644 --- a/ui/flutter/pubspec.lock +++ b/ui/flutter/pubspec.lock @@ -321,6 +321,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" + fluent_ui: + dependency: "direct main" + description: + name: fluent_ui + sha256: f9d443f9181900cfab9f38793195c7c4221cf05eb86918ac3517e67c024da867 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.2.0" flutter: dependency: "direct main" description: flutter @@ -575,7 +583,7 @@ packages: source: hosted version: "2.0.1" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b @@ -702,6 +710,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.0" rounded_loading_button: dependency: "direct main" description: @@ -718,6 +734,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.27.5" + scroll_pos: + dependency: transitive + description: + name: scroll_pos + sha256: cfca311b6b8d51538ff90e206fbe6ce3b36e7125ea6da4a40eb626c7f9f083b1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.0" shared_preferences: dependency: "direct main" description: @@ -1028,5 +1052,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.18.2 <3.0.0" + dart: ">=2.18.2 <4.0.0" flutter: ">=3.3.0" diff --git a/ui/flutter/pubspec.yaml b/ui/flutter/pubspec.yaml index e3447032..cec6d0c9 100644 --- a/ui/flutter/pubspec.yaml +++ b/ui/flutter/pubspec.yaml @@ -52,7 +52,8 @@ dependencies: desktop_drop: ^0.3.3 intl: ^0.17.0 package_info_plus: ^3.0.2 - + fluent_ui: ^4.2.0 + path: any dev_dependencies: flutter_test: sdk: flutter