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 'responsive_builder.dart'; import 'sort_icon_button.dart'; const _toggleSwitchIcons = [ Gopeed.file_video, Gopeed.file_audio, Gopeed.file_image, ]; const _sizeGapWidth = 72.0; class FileTreeView extends StatefulWidget { final List files; final List initialValues; final Function(List) onSelectionChanged; const FileTreeView( {Key? key, required this.files, required this.initialValues, required this.onSelectionChanged}) : super(key: key); @override State createState() => _FileTreeViewState(); } class _FileTreeViewState extends State { late GlobalKey> key; late int totalSize; int? toggleSwitchIndex; @override void initState() { super.initState(); key = GlobalKey>(); 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 selectedFileSize = calcSelectedSize(null); final filterRow = 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); }, ), ); final countRow = Row( children: [ Text('fileSelectedCount'.tr), Text( selectedFileCount.toString(), style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(width: 12), Text('fileSelectedSize'.tr), Text( selectedFileCount > 0 && selectedFileSize == 0 ? 'unknown'.tr : Util.fmtByte(selectedFileSize), style: Theme.of(context).textTheme.bodySmall, ), ], ); 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), !ResponsiveBuilder.isNarrow(context) ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ filterRow, countRow, ], ) : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ filterRow, const SizedBox(height: 8), countRow, ], ), ], ); } int calcSelectedSize(TreeNode? 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> buildTreeNodes() { final List> rootNodes = []; final Map> dirNodes = {}; for (var i = 0; i < widget.files.length; i++) { final file = widget.files[i]; final parts = file.path.split('/'); String currentPath = ''; TreeNode? parentNode; // Create or get directory nodes for (final part in parts) { if (part.isEmpty) continue; currentPath += '/$part'; if (!dirNodes.containsKey(currentPath)) { final node = TreeNode( 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(width: _sizeGapWidth); }, 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( 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(width: _sizeGapWidth); }, 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; } }