feat: improve file select tree view (#853)

This commit is contained in:
Levi authored and GitHub committed 2024-12-25 22:55:33 +08:00
1 parent edd189a8a8
commit a3f559c8e5
18 files changed
+651 -3151

No files matched your search

Binary file not shown.
Binary file not shown.
@@ -1,4 +1,5 @@
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart';
import 'package:checkable_treeview/checkable_treeview.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
@@ -16,10 +17,11 @@ import '../../../../database/database.dart';
import '../../../../util/input_formatter.dart';
import '../../../../util/message.dart';
import '../../../../util/util.dart';
import '../../../../util/extensions.dart';
import '../../../routes/app_pages.dart';
import '../../../views/compact_checkbox.dart';
import '../../../views/directory_selector.dart';
import '../../../views/file_list_view.dart';
import '../../../views/file_tree_view.dart';
import '../../app/controllers/app_controller.dart';
import '../../history/views/history_view.dart';
import '../controllers/create_controller.dart';
@@ -40,6 +42,7 @@ class CreateView extends GetView<CreateController> {
final _httpCookieController = TextEditingController();
final _httpRefererController = TextEditingController();
final _btTrackerController = TextEditingController();
final _fileTreeViewKey = GlobalKey<TreeViewState<int>>();
final _availableSchemes = ["http:", "https:", "magnet:"];
@@ -698,7 +701,13 @@ class CreateView extends GetView<CreateController> {
child: Form(
key: createFormKey,
autovalidateMode: AutovalidateMode.always,
child: FileListView(files: rr.res.files)),
child: FileTreeView(
files: rr.res.files,
initialValues: rr.res.files.asMap().keys.toList(),
onSelectionChanged: (List<int> values) {
controller.selectedIndexes.value = values;
},
)),
);
},
),
@@ -7,10 +7,9 @@ import 'package:url_launcher/url_launcher.dart';
import '../../../../api/api.dart' as api;
import '../../../../util/browser_download/browser_download.dart';
import '../../../../util/file_icon.dart';
import '../../../../util/icons.dart';
import '../../../../util/util.dart';
import '../../../views/breadcrumb_view.dart';
import '../../../views/file_icon.dart';
import '../controllers/task_files_controller.dart';
class TaskFilesView extends GetView<TaskFilesController> {
@@ -66,8 +65,8 @@ class TaskFilesView extends GetView<TaskFilesController> {
final fileName = basename(filePath);
return ListTile(
leading: file.isDirectory
? const Icon(Icons.folder)
: Icon(FaIcons.allIcons[findIcon(fileName)]),
? const Icon(folderIcon)
: Icon(fileIcon(fileName)),
title: Text(fileName),
subtitle: file.isDirectory
? Text('items'.trParams({
@@ -4,14 +4,13 @@ import 'package:styled_widget/styled_widget.dart';
import '../../api/api.dart';
import '../../api/model/task.dart';
import '../../util/file_icon.dart';
import '../../util/icons.dart';
import '../../util/message.dart';
import '../../util/util.dart';
import '../modules/app/controllers/app_controller.dart';
import '../modules/task/controllers/task_controller.dart';
import '../modules/task/views/task_view.dart';
import '../routes/app_pages.dart';
import 'file_icon.dart';
class BuildTaskListView extends GetView {
final List<Task> tasks;
@@ -185,8 +184,8 @@ class BuildTaskListView extends GetView {
ListTile(
title: Text(task.name),
leading: isFolderTask()
? const Icon(FaIcons.folder)
: Icon(FaIcons.allIcons[findIcon(task.name)])),
? const Icon(folderIcon)
: Icon(fileIcon(task.name))),
Row(
children: [
Expanded(
+79
View File
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import '../../icon/gopeed_icons.dart';
final Map<IconData, List<String>> iconConfigMap = {
Gopeed.install: ['exe', 'msi', 'dmg', 'deb', 'rpm'],
Gopeed.android: ['apk'],
Gopeed.app_store_ios: ['ipa'],
Gopeed.file_bt: ['torrent'],
Gopeed.cd: ['iso'],
Gopeed.html5: ['html', 'htm'],
Gopeed.file_alt: ['txt', 'md', 'log', 'csv', 'tsv', 'json', 'yaml', 'yml'],
Gopeed.file_pdf: ['pdf'],
Gopeed.file_word: ['doc', 'docx'],
Gopeed.file_excel: ['xls', 'xlsx'],
Gopeed.file_powerpoint: ['ppt', 'pptx'],
Gopeed.file_archive: ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz'],
Gopeed.file_image: [
'jpg',
'jpeg',
'png',
'gif',
'bmp',
'tiff',
'svg',
'webp'
],
Gopeed.file_audio: ['mp3', 'wav', 'flac', 'aac', 'ogg', 'wma', 'm4a'],
Gopeed.file_video: ['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'webm'],
Gopeed.file_code: [
'js',
'css',
'json',
'xml',
'java',
'cpp',
'dart',
'py',
'rb',
'php',
'ts',
'swift',
'go',
'rs'
],
Gopeed.file: [''],
};
final Map<String, IconData> _iconCache = Map.fromEntries(
iconConfigMap.entries.expand(
(entry) => entry.value.map((ext) => MapEntry(ext, entry.key)),
),
);
const folderIcon = Gopeed.folder;
const folderBtIcon = Gopeed.folder_bt;
String fileExt(String? name) {
if (name == null) {
return '';
}
final ext = name.split('.').last;
if (ext.length > 8) {
return '';
}
return ext.toLowerCase();
}
IconData fileIcon(String? name,
{bool isFolder = false, bool isBitTorrent = false}) {
if (isFolder) {
return isBitTorrent ? folderBtIcon : folderIcon;
}
final ext = fileExt(name);
return _iconCache[ext] ?? Gopeed.file;
}
@@ -1,303 +0,0 @@
import 'dart:ui' as ui;
import 'package:fluent_ui/fluent_ui.dart' as fluent;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../api/model/resource.dart';
import 'package:path/path.dart' as path;
import '../../util/file_icon.dart';
import '../../util/icons.dart';
import '../../util/util.dart';
import '../modules/app/controllers/app_controller.dart';
import '../modules/create/controllers/create_controller.dart';
class FileListView extends GetView {
final List<FileInfo> files;
FileListView({
Key? key,
required this.files,
}) : super(key: key);
final parentController = Get.find<CreateController>();
final appController = Get.find<AppController>();
List findChildFileIdsR(List children) {
List res = [];
for (var k in children) {
if (parentController.fileInfos[k]['type'] == 'folder') {
res.addAll(
findChildFileIdsR(parentController.fileInfos[k]['children']));
} else {
res.add(parentController.fileInfos[k]['fileId']);
}
}
return res;
}
void clearDescendantFolderSizeR(int id) {
if (parentController.fileInfos[id]['type'] == 'folder') {
parentController.fileInfos[id]['size'] = 0;
for (var childId in parentController.fileInfos[id]['children']) {
clearDescendantFolderSizeR(childId);
}
}
}
void reduceAncestorFolderSizeR(int id, int size) {
int parentId = parentController.fileInfos[id]['parentId'];
if (parentId != -1) {
parentController.fileInfos[parentId]['size'] -= size;
reduceAncestorFolderSizeR(parentId, size);
}
}
int addDescendantFolderSizeR(int id) {
if (parentController.fileInfos[id]['type'] == 'folder') {
for (var childId in parentController.fileInfos[id]['children']) {
parentController.fileInfos[id]['size'] +=
addDescendantFolderSizeR(childId);
}
}
return parentController.fileInfos[id]['size'];
}
void addAncestorFolderSizeR(int id, int size) {
int parentId = parentController.fileInfos[id]['parentId'];
if (parentId != -1) {
parentController.fileInfos[parentId]['size'] += size;
addAncestorFolderSizeR(parentId, size);
}
}
List<fluent.TreeViewItem> buildTreeViewItemsR(int level, int parentId) {
List<fluent.TreeViewItem> res = [];
List children = parentController.fileInfos
.where((e) => e['level'] == level && e['parentId'] == parentId)
.toList();
for (int j = 0; j < children.length; j++) {
Map fileInfo = children[j];
if (fileInfo['type'] == 'folder') {
// folder
res.add(fluent.TreeViewItem(
// expanded: false, bug on init
value: fileInfo['id'],
onInvoked: (item, reason) async {
if (reason == fluent.TreeViewItemInvokeReason.selectionToggle) {
List childrenIds = findChildFileIdsR(fileInfo['children']);
if (item.selected == true) {
parentController.selectedIndexes.addAll(childrenIds.cast());
parentController.fileInfos[fileInfo['id']]['size'] = 0;
addDescendantFolderSizeR(item.value);
addAncestorFolderSizeR(item.value, fileInfo['size']);
parentController.fileInfos.refresh();
} else {
parentController.selectedIndexes
.removeWhere((index) => childrenIds.contains(index));
reduceAncestorFolderSizeR(item.value, fileInfo['size']);
clearDescendantFolderSizeR(item.value);
parentController.fileInfos.refresh();
}
}
},
leading: Obx(() {
return parentController.openedFolders.contains(fileInfo['id'])
? const Icon(FaIcons.folder_open,
color: Colors.lightBlueAccent)
: const Icon(FaIcons.folder, color: Colors.lightBlueAccent);
}),
onExpandToggle: (fluent.TreeViewItem item, bool getExpanded) async {
getExpanded
? parentController.openedFolders.add(item.value)
: parentController.openedFolders.remove(item.value);
},
content: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Expanded(
child: Text(
fileInfo['name'],
overflow: TextOverflow.ellipsis,
// style: context.textTheme.titleSmall,
),
),
Obx(() {
return parentController.fileInfos[fileInfo['id']]['size'] != 0
? Text(
Util.fmtByte(
parentController.fileInfos[fileInfo['id']]['size']),
// style: context.textTheme.labelMedium,
overflow: TextOverflow.ellipsis,
)
: const SizedBox.shrink();
})
]),
children: buildTreeViewItemsR(
// parentController.fileInfos.where((e) => e['level'] > level).toList(),
level + 1,
fileInfo['id'])));
} else {
// file
res.add(fluent.TreeViewItem(
value: {'id': fileInfo['id'], 'fileId': fileInfo['fileId']},
selected: fileInfo['selected'],
onInvoked: (item, reason) async {
if (reason == fluent.TreeViewItemInvokeReason.selectionToggle) {
if (item.selected == true) {
parentController.selectedIndexes.add(item.value['fileId']);
addAncestorFolderSizeR(item.value['id'], fileInfo['size']);
parentController.fileInfos.refresh();
} else {
parentController.selectedIndexes.remove(item.value['fileId']);
reduceAncestorFolderSizeR(item.value['id'], fileInfo['size']);
parentController.fileInfos.refresh();
}
}
},
collapsable: false,
leading: fileInfo['name'].lastIndexOf('.') == -1
? Icon(
FaIcons.doc,
color: appController.downloaderConfig.value.extra.themeMode ==
'dark'
? Colors.white
: Colors.black45,
)
: Icon(
FaIcons.allIcons[findIcon(fileInfo['name'])],
color: appController.downloaderConfig.value.extra.themeMode ==
'dark'
? Colors.white
: Colors.black45,
),
content: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Expanded(
child: Text(
fileInfo['name'],
overflow: TextOverflow.ellipsis,
// style: context.textTheme.titleSmall,
)),
fileInfo['size'] > 0
? Text(
Util.fmtByte(
fileInfo['size'],
),
// style: context.textTheme.labelMedium,
overflow: TextOverflow.ellipsis,
)
: const SizedBox.shrink(),
]),
));
}
}
return res;
}
List<fluent.TreeViewItem> get items {
List infos = [];
int idNext = 0;
List selectedFileIds = [];
List openedFolders = [];
//make fileInfos list
for (var i = 0; i < files.length; i++) {
//parentId -1 means path root
int parentId = -1;
List folders = path.split(files[i].path);
for (var folder in folders) {
int indexInInfos = infos.lastIndexWhere((fileInfo) =>
fileInfo['name'] == folder && fileInfo['parentId'] == parentId);
if (indexInInfos != -1) {
// folder exists
parentId = infos[indexInInfos]['id'];
// add size
infos[indexInInfos]['size'] += files[i].size;
} else {
// parent not exist
// create one and add index to parent's children
infos.add({
'level': folders.indexOf(folder),
'id': idNext,
'type': 'folder',
'name': folder,
'size': files[i].size,
'parentId': parentId,
'children': [],
});
openedFolders.add(idNext);
if (parentId != -1) {
infos[parentId]['children'].add(idNext);
}
parentId = idNext;
idNext++;
}
}
//add one file, add index to parent
infos.add({
'id': idNext,
'type': 'file',
'fileId': i,
'level': folders.length,
'name': files[i].name,
'size': files[i].size,
'parentId': parentId,
//TODO add unselected logic
'selected': true
});
selectedFileIds.add(i);
if (parentId != -1) {
infos[parentId]['children'].add(idNext);
}
idNext++;
}
parentController.fileInfos.value = infos;
parentController.selectedIndexes.value = selectedFileIds.cast();
parentController.openedFolders.value = openedFolders;
List<fluent.TreeViewItem> treeItems = buildTreeViewItemsR(0, -1);
return treeItems;
}
@override
Widget build(BuildContext context) {
return fluent.FluentTheme(
data: appController.downloaderConfig.value.extra.themeMode == 'system'
? fluent.FluentThemeData(brightness: ui.window.platformBrightness)
: appController.downloaderConfig.value.extra.themeMode == 'light'
? fluent.FluentThemeData(brightness: Brightness.light)
: fluent.FluentThemeData(brightness: Brightness.dark),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(padding: EdgeInsets.only(top: 10)),
Text(
'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(
selectionMode: fluent.TreeViewSelectionMode.multiple,
shrinkWrap: false,
// addRepaintBoundaries: false,
usePrototypeItem: true,
// cacheExtent: 20,
// onItemInvoked: (item, reason) async => {},
onSecondaryTap: (item, details) async {
debugPrint(
'onSecondaryTap $item at ${details.globalPosition}');
},
// onSelectionChanged: (selectedItems) async => {},
narrowSpacing: true,
items: items,
// scrollPrimary: true,
))),
],
));
}
}
@@ -0,0 +1,284 @@
import 'package:checkable_treeview/checkable_treeview.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:toggle_switch/toggle_switch.dart';
import '../../api/model/resource.dart';
import '../../icon/gopeed_icons.dart';
import '../../util/util.dart';
import 'file_icon.dart';
import 'sort_icon_button.dart';
const _toggleSwitchIcons = [
Gopeed.file_video,
Gopeed.file_audio,
Gopeed.file_image,
];
class FileTreeView extends StatefulWidget {
final List<FileInfo> files;
final List<int> initialValues;
final Function(List<int>) onSelectionChanged;
const FileTreeView(
{Key? key,
required this.files,
required this.initialValues,
required this.onSelectionChanged})
: super(key: key);
@override
State<FileTreeView> createState() => _FileTreeViewState();
}
class _FileTreeViewState extends State<FileTreeView> {
late GlobalKey<TreeViewState<int>> key;
late int totalSize;
int? toggleSwitchIndex;
@override
void initState() {
super.initState();
key = GlobalKey<TreeViewState<int>>();
totalSize = widget.files
.fold(0, (previousValue, element) => previousValue + element.size);
widget.onSelectionChanged(widget.initialValues);
}
@override
Widget build(BuildContext context) {
final selectedFileCount =
key.currentState?.getSelectedValues().where((e) => e != null).length ??
widget.files.length;
final selectdFileSize = calcSelectedSize(null);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(4),
),
child: TreeView(
key: key,
nodes: buildTreeNodes(),
showExpandCollapseButton: true,
showSelectAll: true,
onSelectionChanged: (selectedValues) {
setState(() {});
widget.onSelectionChanged(selectedValues
.where((e) => e != null)
.map((e) => e!)
.toList());
},
selectAllTrailing: (context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('name'.tr),
SortIconButton(
onStateChanged: (state) {
switch (state) {
case SortState.asc:
key.currentState?.sort((p0, p1) {
return (p0.label as Text)
.data!
.compareTo((p1.label as Text).data!);
});
break;
case SortState.desc:
key.currentState?.sort((p0, p1) {
return (p1.label as Text)
.data!
.compareTo((p0.label as Text).data!);
});
break;
default:
key.currentState?.sort(null);
break;
}
},
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('size'.tr),
SortIconButton(
onStateChanged: (state) {
switch (state) {
case SortState.asc:
key.currentState?.sort((p0, p1) {
return calcSelectedSize(p0)
.compareTo(calcSelectedSize(p1));
});
break;
case SortState.desc:
key.currentState?.sort((p0, p1) {
return calcSelectedSize(p1)
.compareTo(calcSelectedSize(p0));
});
break;
default:
key.currentState?.sort(null);
break;
}
},
),
],
)
],
);
},
),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
InkWell(
onTap: () {},
child: ToggleSwitch(
minHeight: 32,
cornerRadius: 8,
doubleTapDisable: true,
inactiveBgColor: Theme.of(context).dividerColor,
activeBgColor: [Theme.of(context).colorScheme.primary],
initialLabelIndex: toggleSwitchIndex,
icons: _toggleSwitchIcons,
onToggle: (index) {
toggleSwitchIndex = index;
if (index == null) {
key.currentState?.setSelectedValues(List.empty());
return;
}
final iconFileExtArr =
iconConfigMap[_toggleSwitchIcons[index]] ?? [];
final selectedFileIndexes = widget.files
.asMap()
.entries
.where(
(e) => iconFileExtArr.contains(fileExt(e.value.name)))
.map((e) => e.key)
.toList();
key.currentState?.setSelectedValues(selectedFileIndexes);
},
),
),
Row(
children: [
Text('fileSelectedCount'.tr),
Text(
selectedFileCount.toString(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(width: 16),
Text('fileSelectedSize'.tr),
Text(
selectedFileCount > 0 && selectdFileSize == 0
? 'unknown'.tr
: Util.fmtByte(selectdFileSize),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
],
),
],
);
}
int calcSelectedSize(TreeNode<int>? node) {
if (key.currentState == null) {
return widget.files
.fold(0, (previousValue, element) => previousValue + element.size);
}
final selectedFileIndexes = node == null
? key.currentState?.getSelectedValues()
: (node.value != null
? [node.value]
: key.currentState?.getChildSelectedValues(node));
if (selectedFileIndexes == null) return 0;
return selectedFileIndexes.where((e) => e != null).map((e) => e!).fold(0,
(previousValue, element) => previousValue + widget.files[element].size);
}
List<TreeNode<int>> buildTreeNodes() {
final List<TreeNode<int>> rootNodes = [];
final Map<String, TreeNode<int>> dirNodes = {};
for (var i = 0; i < widget.files.length; i++) {
final file = widget.files[i];
final parts = file.path.split('/');
String currentPath = '';
TreeNode<int>? parentNode;
// Create or get directory nodes
for (final part in parts) {
if (part.isEmpty) continue;
currentPath += '/$part';
if (!dirNodes.containsKey(currentPath)) {
final node = TreeNode<int>(
label: Text(part),
icon: Icon(
fileIcon(part, isFolder: true),
size: 18,
),
trailing: (context, node) {
final size = calcSelectedSize(node);
return size > 0
? Text(Util.fmtByte(calcSelectedSize(node)),
style: Theme.of(context).textTheme.bodySmall)
: const SizedBox();
},
children: [],
);
dirNodes[currentPath] = node;
if (parentNode == null) {
rootNodes.add(node);
} else {
parentNode.children.add(node);
}
}
parentNode = dirNodes[currentPath];
}
// Create file node using file.name
final fileNode = TreeNode<int>(
label: Text(file.name),
value: i,
icon: Icon(fileIcon(file.name, isFolder: false), size: 18),
trailing: (context, node) {
return file.size > 0
? Text(Util.fmtByte(file.size),
style: Theme.of(context).textTheme.bodySmall)
: const SizedBox();
},
isSelected: widget.initialValues.contains(i),
children: [],
);
// Add file node to parent or root
if (parentNode != null) {
parentNode.children.add(fileNode);
} else {
rootNodes.add(fileNode);
}
}
return rootNodes;
}
}
@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import '../../icon/gopeed_icons.dart';
enum SortState { none, asc, desc }
class SortIconButton extends StatefulWidget {
final double size;
final Color? color;
final Color? activeColor;
final SortState initialState;
final Function(SortState) onStateChanged;
const SortIconButton({
Key? key,
this.size = 18.0,
this.color,
this.activeColor,
this.initialState = SortState.none,
required this.onStateChanged,
}) : super(key: key);
@override
State<SortIconButton> createState() => _SortIconState();
}
class _SortIconState extends State<SortIconButton> {
late SortState _currentState;
@override
void initState() {
super.initState();
_currentState = widget.initialState;
}
void _toggleState() {
setState(() {
switch (_currentState) {
case SortState.none:
_currentState = SortState.asc;
break;
case SortState.asc:
_currentState = SortState.desc;
break;
case SortState.desc:
_currentState = SortState.none;
break;
}
});
widget.onStateChanged(_currentState);
}
@override
Widget build(BuildContext context) {
return InkWell(
customBorder: const CircleBorder(),
onTap: _toggleState,
child: SizedBox(
width: widget.size,
height: widget.size,
child: _currentState == SortState.none
? Icon(
Gopeed.sort,
size: widget.size,
color: widget.color,
)
: Column(
children: [
ClipRect(
child: Align(
alignment: Alignment.topCenter,
heightFactor: 0.5,
child: Icon(
Gopeed.sort,
size: widget.size,
color: _currentState == SortState.asc
? (widget.activeColor ??
Theme.of(context).colorScheme.primary)
: widget.color,
),
),
),
ClipRect(
child: Align(
alignment: Alignment.bottomCenter,
heightFactor: 0.5,
child: Icon(
Gopeed.sort,
size: widget.size,
color: _currentState == SortState.desc
? (widget.activeColor ??
Theme.of(context).colorScheme.primary)
: widget.color,
),
),
),
],
),
),
);
}
}
+5
View File
@@ -108,5 +108,10 @@ const enUS = {
'taskUrl': 'Task URL',
'downloadPath': 'Download Path',
'skipVerifyCert': 'Skip Certificate Verification',
'name': 'Name',
'size': 'Size',
'unknown': 'Unknown',
'fileSelectedCount': 'Files: ',
'fileSelectedSize': 'Size: ',
},
};
+5
View File
@@ -105,5 +105,10 @@ const zhCN = {
'taskUrl': '任务链接',
'downloadPath': '下载路径',
'skipVerifyCert': '跳过证书验证',
'name': '名称',
'size': '大小',
'unknown': '未知',
'fileSelectedCount': '文件数:',
'fileSelectedSize': '大小:',
}
};
+5
View File
@@ -105,5 +105,10 @@ const zhTW = {
'taskUrl': '任務連結',
'downloadPath': '下載路徑',
'skipVerifyCert': '跳過憑證驗證',
'name': '名稱',
'size': '大小',
'unknown': '未知',
'fileSelectedCount': '文件數:',
'fileSelectedSize': '大小:',
}
};
+56
View File
@@ -0,0 +1,56 @@
/// Flutter icons Gopeed
/// Copyright (C) 2024 by original authors @ fluttericon.com, fontello.com
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
///
/// To use this font, place it in your fonts/ directory and include the
/// following in your pubspec.yaml
///
/// flutter:
/// fonts:
/// - family: Gopeed
/// fonts:
/// - asset: fonts/Gopeed.ttf
///
///
/// * Entypo, Copyright (C) 2012 by Daniel Bruce
/// Author: Daniel Bruce
/// License: SIL (http://scripts.sil.org/OFL)
/// Homepage: http://www.entypo.com
/// * Zocial, Copyright (C) 2012 by Sam Collins
/// Author: Sam Collins
/// License: MIT (http://opensource.org/licenses/mit-license.php)
/// Homepage: http://zocial.smcllns.com/
/// * Font Awesome 5, Copyright (C) 2016 by Dave Gandy
/// Author: Dave Gandy
/// License: SIL (https://github.com/FortAwesome/Font-Awesome/blob/master/LICENSE.txt)
/// Homepage: http://fortawesome.github.com/Font-Awesome/
///
import 'package:flutter/widgets.dart';
class Gopeed {
Gopeed._();
static const _kFontFam = 'Gopeed';
static const String? _kFontPkg = null;
static const IconData install = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData android = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData cd = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData folder_bt = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData file_bt = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData folder = IconData(0xf07b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData sort = IconData(0xf0dc, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData html5 = IconData(0xf13b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData file = IconData(0xf15b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData file_alt = IconData(0xf15c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData file_pdf = IconData(0xf1c1, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData file_word = IconData(0xf1c2, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData file_excel = IconData(0xf1c3, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData file_powerpoint = IconData(0xf1c4, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData file_image = IconData(0xf1c5, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData file_archive = IconData(0xf1c6, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData file_audio = IconData(0xf1c7, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData file_video = IconData(0xf1c8, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData file_code = IconData(0xf1c9, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData app_store_ios = IconData(0xf370, fontFamily: _kFontFam, fontPackage: _kFontPkg);
}
+76
View File
@@ -0,0 +1,76 @@
import 'dart:core';
import 'package:checkable_treeview/checkable_treeview.dart';
import 'package:flutter/material.dart';
import '../api/model/resource.dart';
import '../app/views/file_icon.dart';
import 'util.dart';
extension ListFileInfoExtension on List<FileInfo> {
List<TreeNode<int>> toTreeNodes() {
final List<TreeNode<int>> rootNodes = [];
final Map<String, TreeNode<int>> dirNodes = {};
var nodeIndex = 0;
for (var i = 0; i < length; i++) {
final file = this[i];
final parts = file.path.split('/');
String currentPath = '';
TreeNode<int>? parentNode;
// Create or get directory nodes
for (final part in parts) {
if (part.isEmpty) continue;
currentPath += '/$part';
if (!dirNodes.containsKey(currentPath)) {
final node = TreeNode<int>(
value: nodeIndex++,
label: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(file.name),
Text(Util.fmtByte(file.size)),
],
),
icon: Icon(
fileIcon(part, isFolder: true),
size: 18,
),
children: [],
);
dirNodes[currentPath] = node;
if (parentNode == null) {
rootNodes.add(node);
} else {
parentNode.children.add(node);
}
}
parentNode = dirNodes[currentPath];
}
// Create file node using file.name
final fileNode = TreeNode<int>(
value: nodeIndex++,
label: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(file.name),
Text(Util.fmtByte(file.size)),
],
),
icon: Icon(fileIcon(file.name, isFolder: false), size: 18),
children: [],
);
// Add file node to parent or root
if (parentNode != null) {
parentNode.children.add(fileNode);
} else {
rootNodes.add(fileNode);
}
}
return rootNodes;
}
}
-296
View File
@@ -1,296 +0,0 @@
findIcon(String filename) {
String res = 'file';
String ext = filename.substring(filename.lastIndexOf('.') + 1);
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',
'm4s',
'ts'
],
'type': 'Video',
'thumbnail': 'file_video'
},
{
'extensions': [
'mp3',
'ogg',
'ogm',
'wav',
'.aac',
'.ape',
'.flac',
'.flav',
'.m4a',
'.wma'
],
'type': 'Audio',
'thumbnail': 'file_audio'
},
//compressed
{
'extensions': [
'7z',
'brotli',
'bzip2',
'gz',
'gzip',
'rar',
'tgz',
'xz',
'zip',
'zstd',
'deb',
'msi',
'snap',
'iso',
'dmg',
'dockerfile',
'dockerignore'
],
'type': 'Archive',
'thumbnail': 'file_archive'
},
//office relative
{
'extensions': ['pdf'],
'type': 'Portable Document Format',
'thumbnail': 'file_pdf'
},
{
'extensions': ['txt', 'docb', 'rtf'],
'type': 'Document',
'thumbnail': 'doc_text'
},
{
'extensions': ['doc', 'docm', 'dot', 'dotm', 'docx'],
'type': 'Word Document',
'thumbnail': 'file_word'
},
{
'extensions': ['xlsx', 'xls', 'xlsb', 'xls', 'ods', 'fods', 'csv'],
'type': 'Excel Document',
'thumbnail': 'file_excel'
},
{
'extensions': [
'pot',
'potm',
'potx',
'ppam',
'pps',
'ppsm',
'ppsx',
'ppt',
'pptn',
'pptx'
],
'type': 'Powerpoint Document',
'thumbnail': 'file_powerpoint'
},
//windows
{
'extensions': ['exe'],
'type': 'Microsoft',
'thumbnail': 'windows'
},
//android
{
'extensions': ['apk'],
'type': 'Android',
'thumbnail': 'android'
},
//other
{
'extensions': ['lnk'],
'type': 'Shortcut',
'thumbnail': 'link_ext'
},
{
'extensions': [
'ini',
'dlc',
'config',
'conf',
'properties',
'prop',
'settings',
'option',
'props',
'toml',
'prefs',
'sln.dotsettings',
'sln.dotsettings.user',
'cfg'
],
'type': 'Settings',
'thumbnail': 'cog_alt'
},
{
'extensions': ['html', 'htm', 'xhtml', 'html_vm'],
'type': 'html',
'thumbnail': 'html5'
},
//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'
// },
];
File diff suppressed because it is too large. Load diff
+16 -72
View File
@@ -145,6 +145,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
checkable_treeview:
dependency: "direct main"
description:
name: checkable_treeview
sha256: e38cf0a1088b803707a1f0b271ac46210108aac81c2dad506d0d56e7932ab690
url: "https://pub.dev"
source: hosted
version: "1.3.0"
checked_yaml:
dependency: transitive
description:
@@ -321,14 +329,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
fluent_ui:
dependency: "direct main"
description:
name: fluent_ui
sha256: a8c76cb501303d108cb9bd33e516da7cfd078031ff427d68eab6069bf4492a2c
url: "https://pub.dev"
source: hosted
version: "4.8.7"
flutter:
dependency: "direct main"
description: flutter
@@ -525,30 +525,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
url: "https://pub.dev"
source: hosted
version: "10.0.0"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
url: "https://pub.dev"
source: hosted
version: "2.0.1"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
url: "https://pub.dev"
source: hosted
version: "2.0.1"
lecle_downloads_path_provider:
dependency: "direct main"
description:
@@ -617,26 +593,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.16"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
math_expressions:
dependency: transitive
description:
name: math_expressions
sha256: e32d803d758ace61cc6c4bdfed1226ff60a6a23646b35685670d28b5616139f8
url: "https://pub.dev"
source: hosted
version: "2.6.0"
version: "0.5.0"
menu_base:
dependency: transitive
description:
@@ -649,10 +617,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.10.0"
mime:
dependency: transitive
description:
@@ -697,10 +665,10 @@ packages:
dependency: "direct main"
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "1.8.3"
path_provider:
dependency: "direct main"
description:
@@ -854,14 +822,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.1"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
rounded_loading_button_plus:
dependency: "direct main"
description:
@@ -886,14 +846,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.9"
scroll_pos:
dependency: transitive
description:
name: scroll_pos
sha256: cebf602b2dd939de6832bb902ffefb574608d1b84f420b82b381a4007d3c1e1b
url: "https://pub.dev"
source: hosted
version: "0.5.0"
share_plus:
dependency: "direct main"
description:
@@ -1203,14 +1155,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
url: "https://pub.dev"
source: hosted
version: "13.0.0"
watcher:
dependency: transitive
description:
+6 -6
View File
@@ -18,7 +18,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.6.4+1
environment:
sdk: '>=3.0.0 <4.0.0'
sdk: ">=3.0.0 <4.0.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@@ -49,7 +49,6 @@ dependencies:
logger: ^1.3.0
desktop_drop: ^0.4.3
package_info_plus: ^4.0.0
fluent_ui: ^4.4.0
path: any
badges: ^3.0.3
app_links: ^3.4.2
@@ -69,6 +68,7 @@ dependencies:
toggle_switch: ^2.3.0
permission_handler: ^11.3.1
device_info_plus: ^9.1.2
checkable_treeview: ^1.3.0
dependency_overrides:
permission_handler_windows:
git:
@@ -118,9 +118,9 @@ flutter:
# list giving the asset and other descriptors for the font. For
# example:
fonts:
- family: FontAwesome
- family: Gopeed
fonts:
- asset: assets/fonts/FontAwesome.ttf
- asset: assets/fonts/Gopeed.ttf
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
@@ -150,7 +150,7 @@ flutter_icons:
ffigen:
name: LibgopeedBind
description: Bindings to gopeed library.
output: 'lib/core/ffi/libgopeed_bind.dart'
output: "lib/core/ffi/libgopeed_bind.dart"
headers:
entry-points:
- 'include/libgopeed.h'
- "include/libgopeed.h"