mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-10 02:20:28 +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
@@ -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 @@
|
||||
|
||||
Reference in new issue
Block a user