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:
wuke2022 authored and liwei committed 2023-02-16 11:34:42 +08:00
1 parent a875a9258e
commit 9b1ed15fea
28 files changed
+932 -439

No files matched your search

+1 -12
View File
@@ -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 -2
View File
@@ -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);
}
+4 -2
View File
@@ -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,
};
+2
View File
@@ -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',
+2
View File
@@ -7,6 +7,8 @@ const faIR = {
'cancel': 'انصراف',
'selectAll': 'انتخاب همه',
'home.task': 'کار',
'home.downloading': 'downloading',
'home.downloaded': 'downloaded',
'home.setting': 'تنظیمات',
'create.title': 'ایجاد کار',
'create.downloadLink': 'لینک دانلود',
+2
View File
@@ -7,6 +7,8 @@ const ruRU = {
'cancel': 'Отмена',
'selectAll': 'Выбрать все',
'home.task': 'Задачи',
'home.downloading': 'downloading',
'home.downloaded': 'downloaded',
'home.setting': 'Настройки',
'create.title': 'Создать задачу',
'create.downloadLink': 'Ссылка для скачивание',
+2
View File
@@ -7,6 +7,8 @@ const zhCN = {
'cancel': '取消',
'selectAll': '全选',
'home.task': '任务',
'home.downloading': '下载中',
'home.downloaded': '已完成',
'home.setting': '设置',
'create.title': '创建任务',
'create.downloadLink': '下载链接',
+2 -1
View File
@@ -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
View File
@@ -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;
}
+14 -22
View File
@@ -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);
}
}
+121 -66
View File
@@ -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,
);
},
);
+6 -8
View File
@@ -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;
}
}
-198
View File
@@ -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]);
},
);
}
}
+28 -7
View File
@@ -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());
+8 -4
View File
@@ -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));
}
+182
View File
@@ -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()
],
),
),
)
])
]));
}
}
+161 -46
View File
@@ -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
View File
@@ -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"
+2 -1
View File
@@ -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