mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-05 16:15:43 +00:00
feat: ui improvement
flutter 3.7 compatibility responsive view beautiful progress bar select file in treeview downlading and downloaded page
This commit is contained in:
1 parent
a875a9258e
commit
9b1ed15fea
28 files changed
+932
-439
No files matched your search
+1
-12
@@ -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
|
||||
@@ -7,6 +7,7 @@ class Resource {
|
||||
String name;
|
||||
int size;
|
||||
bool range;
|
||||
String rootDir;
|
||||
List<FileInfo> 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<String, dynamic> json) =>
|
||||
_$FileInfoFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FileInfoToJson(this);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ Resource _$ResourceFromJson(Map<String, dynamic> 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<dynamic>)
|
||||
.map((e) => FileInfo.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
@@ -20,18 +21,19 @@ Map<String, dynamic> _$ResourceToJson(Resource instance) => <String, dynamic>{
|
||||
'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<String, dynamic> json) => FileInfo(
|
||||
name: json['name'] as String,
|
||||
path: json['path'] as String,
|
||||
name: json['name'] as String,
|
||||
size: json['size'] as int,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FileInfoToJson(FileInfo instance) => <String, dynamic>{
|
||||
'name': instance.name,
|
||||
'path': instance.path,
|
||||
'name': instance.name,
|
||||
'size': instance.size,
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -7,6 +7,8 @@ const faIR = {
|
||||
'cancel': 'انصراف',
|
||||
'selectAll': 'انتخاب همه',
|
||||
'home.task': 'کار',
|
||||
'home.downloading': 'downloading',
|
||||
'home.downloaded': 'downloaded',
|
||||
'home.setting': 'تنظیمات',
|
||||
'create.title': 'ایجاد کار',
|
||||
'create.downloadLink': 'لینک دانلود',
|
||||
|
||||
@@ -7,6 +7,8 @@ const ruRU = {
|
||||
'cancel': 'Отмена',
|
||||
'selectAll': 'Выбрать все',
|
||||
'home.task': 'Задачи',
|
||||
'home.downloading': 'downloading',
|
||||
'home.downloaded': 'downloaded',
|
||||
'home.setting': 'Настройки',
|
||||
'create.title': 'Создать задачу',
|
||||
'create.downloadLink': 'Ссылка для скачивание',
|
||||
|
||||
@@ -7,6 +7,8 @@ const zhCN = {
|
||||
'cancel': '取消',
|
||||
'selectAll': '全选',
|
||||
'home.task': '任务',
|
||||
'home.downloading': '下载中',
|
||||
'home.downloaded': '已完成',
|
||||
'home.setting': '设置',
|
||||
'create.title': '创建任务',
|
||||
'create.downloadLink': '下载链接',
|
||||
|
||||
@@ -22,5 +22,6 @@ const fallbackLocale = Locale('en', 'US');
|
||||
class _Messages extends Translations {
|
||||
// just include available locales here
|
||||
@override
|
||||
Map<String, Map<String, String>> get keys => {...zhCN, ...enUS, ...ruRU, ...faIR};
|
||||
Map<String, Map<String, String>> get keys =>
|
||||
{...zhCN, ...enUS, ...ruRU, ...faIR};
|
||||
}
|
||||
@@ -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<AppController> {
|
||||
@@ -14,6 +15,10 @@ class AppView extends GetView<AppController> {
|
||||
Widget build(BuildContext context) {
|
||||
final config = controller.downloaderConfig.value;
|
||||
return GetMaterialApp.router(
|
||||
initialBinding: BindingsBuilder(() {
|
||||
Get.lazyPut<HomeController>(() => HomeController(), fenix: true);
|
||||
}),
|
||||
useInheritedMediaQuery: true,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: GopeedTheme.light,
|
||||
darkTheme: GopeedTheme.dark,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class CreateController extends GetxController {}
|
||||
class CreateController extends GetxController {
|
||||
final files = [].obs;
|
||||
final RxList selectedIndexs = [].obs;
|
||||
}
|
||||
@@ -20,6 +20,7 @@ class CreateView extends GetView<CreateController> {
|
||||
|
||||
final _urlController = TextEditingController();
|
||||
final _confirmController = RoundedLoadingButtonController();
|
||||
|
||||
CreateView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -28,8 +29,8 @@ class CreateView extends GetView<CreateController> {
|
||||
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<CreateController> {
|
||||
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<CreateController> {
|
||||
}
|
||||
|
||||
Future<void> _showResolveDialog(ResolveResult rr) async {
|
||||
final res = rr.res;
|
||||
controller.files.value = rr.res.files;
|
||||
final appController = Get.find<AppController>();
|
||||
final setting = appController.downloaderConfig.value;
|
||||
|
||||
final createFormKey = GlobalKey<FormState>();
|
||||
final pathController = TextEditingController(text: setting.downloadDir);
|
||||
final downloadController = RoundedLoadingButtonController();
|
||||
var fileValues = List.filled(res.files.length, true);
|
||||
|
||||
return await showDialog<void>(
|
||||
return showDialog<void>(
|
||||
context: Get.context!,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
@@ -129,11 +130,7 @@ class CreateView extends GetView<CreateController> {
|
||||
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<CreateController> {
|
||||
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<CreateController> {
|
||||
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<int>())));
|
||||
Get.back();
|
||||
Get.rootDelegate.offNamed(Routes.task);
|
||||
Get.rootDelegate.offNamed(Routes.downloading);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('error'.tr, e.toString());
|
||||
|
||||
@@ -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 = <Task>[].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]);
|
||||
}
|
||||
}
|
||||
@@ -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<DownloadedController> {
|
||||
const DownloadedView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BuildTaskListView(tasks: controller.tasks);
|
||||
}
|
||||
}
|
||||
@@ -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 = <Task>[].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]);
|
||||
}
|
||||
}
|
||||
@@ -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<DownloadingController> {
|
||||
const DownloadingView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BuildTaskListView(tasks: controller.tasks);
|
||||
}
|
||||
}
|
||||
@@ -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<HomeController> {
|
||||
@@ -14,73 +15,127 @@ class HomeView extends GetView<HomeController> {
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -11,14 +11,12 @@ class RootView extends GetView<RootController> {
|
||||
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);
|
||||
// },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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 = <Task>[].obs;
|
||||
final doneTasks = <Task>[].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;
|
||||
}
|
||||
}
|
||||
@@ -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<Widget> buildActions() {
|
||||
final list = <Widget>[];
|
||||
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<void> _showDeleteDialog(String id) {
|
||||
final keep = true.obs;
|
||||
|
||||
final context = Get.context!;
|
||||
|
||||
return showDialog<void>(
|
||||
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<TaskController>().loadTab();
|
||||
Get.back();
|
||||
} catch (e) {
|
||||
Get.snackbar('error'.tr, e.toString());
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class TaskView extends GetView<TaskController> {
|
||||
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<Task> tasks) {
|
||||
return ListView.builder(
|
||||
itemCount: tasks.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Item(task: tasks[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>(() => TaskController());
|
||||
Get.lazyPut<DownloadingController>(
|
||||
() => DownloadingController(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.downloaded,
|
||||
transition: Transition.noTransition,
|
||||
page: () => const DownloadedView(),
|
||||
binding: BindingsBuilder(() {
|
||||
Get.lazyPut<DownloadedController>(
|
||||
() => DownloadedController(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.setting,
|
||||
transition: Transition.noTransition,
|
||||
page: () => const SettingView(),
|
||||
binding: BindingsBuilder(() {
|
||||
Get.lazyPut<SettingController>(() => SettingController());
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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'
|
||||
// },
|
||||
];
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Task> tasks;
|
||||
|
||||
const BuildTaskListView({
|
||||
Key? key,
|
||||
required this.tasks,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actions: <Widget>[
|
||||
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<void> showDeleteDialog(String id) {
|
||||
final keep = true.obs;
|
||||
|
||||
final context = Get.context!;
|
||||
|
||||
return showDialog<void>(
|
||||
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<Widget> buildActions() {
|
||||
final list = <Widget>[];
|
||||
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()
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
])
|
||||
]));
|
||||
}
|
||||
}
|
||||
@@ -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<FileInfo> files;
|
||||
final List<bool> values;
|
||||
|
||||
const FileListView({
|
||||
class FileListView extends GetView {
|
||||
FileListView({
|
||||
Key? key,
|
||||
required this.files,
|
||||
required this.values,
|
||||
}) : super(key: key);
|
||||
final parentController = Get.find<CreateController>();
|
||||
final appController = Get.find<AppController>();
|
||||
|
||||
@override
|
||||
State<FileListView> createState() => _FileListViewState();
|
||||
}
|
||||
List<fluent.TreeViewItem> buildTreeViewItemsRecursive(
|
||||
List fileInfos, int level, List<fluent.TreeViewItem> 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<FileListView> {
|
||||
List<FileInfo> get _files => widget.files;
|
||||
|
||||
List<bool> get _values => widget.values;
|
||||
List<fluent.TreeViewItem> 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<fluent.TreeViewItem> 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<AppController>();
|
||||
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))),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
+26
-2
@@ -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"
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user