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

@@ -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 @@